@synthetic-ai/premiere-mcp 2.1.0 → 2.3.0-dev.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +135 -123
  2. package/LICENSE +21 -21
  3. package/README.md +242 -242
  4. package/cep-plugin/.debug +8 -8
  5. package/cep-plugin/CSInterface.js +92 -92
  6. package/cep-plugin/host.jsx +7 -7
  7. package/cep-plugin/index.html +472 -472
  8. package/cep-plugin/main.js +6 -0
  9. package/cep-plugin/premiere.jsx +24947 -24947
  10. package/dist/bridge/script-builder.js +378 -378
  11. package/dist/index.js +103 -4
  12. package/dist/license.js +38 -0
  13. package/dist/server.js +60 -60
  14. package/dist/tools/advanced.js +357 -357
  15. package/dist/tools/audio-advanced.js +1049 -1049
  16. package/dist/tools/audio.js +42 -42
  17. package/dist/tools/captions.js +27 -27
  18. package/dist/tools/clipboard.js +214 -214
  19. package/dist/tools/color-advanced.js +253 -253
  20. package/dist/tools/discovery.js +291 -291
  21. package/dist/tools/effects.js +285 -285
  22. package/dist/tools/export-presets.js +206 -206
  23. package/dist/tools/export.js +255 -255
  24. package/dist/tools/health.js +10 -10
  25. package/dist/tools/inspection.js +885 -885
  26. package/dist/tools/keyframes.js +369 -369
  27. package/dist/tools/markers.js +89 -89
  28. package/dist/tools/media.js +178 -178
  29. package/dist/tools/metadata.js +94 -94
  30. package/dist/tools/multicam.js +50 -50
  31. package/dist/tools/playback.js +15 -15
  32. package/dist/tools/playhead.js +95 -95
  33. package/dist/tools/project-manager.js +21 -21
  34. package/dist/tools/project.js +149 -149
  35. package/dist/tools/scripting.js +319 -319
  36. package/dist/tools/selection.js +202 -202
  37. package/dist/tools/sequence-advanced.js +379 -379
  38. package/dist/tools/sequence-settings.js +564 -564
  39. package/dist/tools/sequence.js +167 -167
  40. package/dist/tools/source-monitor.js +68 -68
  41. package/dist/tools/text.js +58 -58
  42. package/dist/tools/timeline.js +312 -312
  43. package/dist/tools/track-management.js +446 -446
  44. package/dist/tools/track-targeting.js +635 -635
  45. package/dist/tools/tracks.js +87 -87
  46. package/dist/tools/transitions-motion.js +174 -174
  47. package/dist/tools/transitions.js +111 -111
  48. package/dist/tools/utility.js +606 -606
  49. package/dist/tools/workspace.js +29 -29
  50. package/package.json +66 -66
  51. package/scripts/build-chat-dmg.sh +210 -210
  52. package/scripts/build-chat-zip.sh +291 -291
  53. package/scripts/install-cep-windows.ps1 +99 -99
  54. package/scripts/install-cep.sh +66 -66
  55. package/scripts/install-chat-plugin.bat +67 -67
  56. package/scripts/install-chat-plugin.sh +85 -85
  57. package/scripts/install-windows.bat +95 -95
  58. package/scripts/package.mjs +254 -254
@@ -3,18 +3,18 @@ import { sendCommand } from "../bridge/file-bridge.js";
3
3
  export function getTimelineTools(bridgeOptions) {
4
4
  return {
5
5
  add_to_timeline: {
6
- description: `Add a project item (clip, image, or audio file) to the active sequence timeline at a specific position.
7
-
8
- Preconditions: An active sequence must exist (use create_sequence or set_active_sequence first). The item must already be imported into the project (use import_media first).
9
-
10
- The clip is inserted using insert-edit semantics — content after the insertion point shifts forward. For overwrite-edit behavior, use overwrite_clip instead.
11
-
12
- Parameters:
13
- - item_id: The node ID from list_project_items, or the item's display name (partial match supported).
14
- - track_index: Video track index (0 = V1, 1 = V2, etc.). Higher tracks composite on top.
15
- - start_seconds: Timeline position in seconds where the clip should start (e.g., 10.5 = 10 seconds 12 frames at 24fps).
16
- - audio_track_index: Audio track index for the linked audio (default matches track_index).
17
-
6
+ description: `Add a project item (clip, image, or audio file) to the active sequence timeline at a specific position.
7
+
8
+ Preconditions: An active sequence must exist (use create_sequence or set_active_sequence first). The item must already be imported into the project (use import_media first).
9
+
10
+ The clip is inserted using insert-edit semantics — content after the insertion point shifts forward. For overwrite-edit behavior, use overwrite_clip instead.
11
+
12
+ Parameters:
13
+ - item_id: The node ID from list_project_items, or the item's display name (partial match supported).
14
+ - track_index: Video track index (0 = V1, 1 = V2, etc.). Higher tracks composite on top.
15
+ - start_seconds: Timeline position in seconds where the clip should start (e.g., 10.5 = 10 seconds 12 frames at 24fps).
16
+ - audio_track_index: Audio track index for the linked audio (default matches track_index).
17
+
18
18
  Example: add_to_timeline({ item_id: "interview.mp4", track_index: 0, start_seconds: 30, audio_track_index: 0 })`,
19
19
  parameters: {
20
20
  type: "object",
@@ -42,22 +42,22 @@ Example: add_to_timeline({ item_id: "interview.mp4", track_index: 0, start_secon
42
42
  const trackIndex = args.track_index ?? 0;
43
43
  const startSeconds = args.start_seconds ?? 0;
44
44
  const audioTrackIndex = args.audio_track_index ?? 0;
45
- const script = buildToolScript(`
46
- var seq = app.project.activeSequence;
47
- if (!seq) return __error("No active sequence");
48
-
49
- var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
50
- if (!item) return __error("Project item not found: ${escapeForExtendScript(args.item_id)}");
51
-
52
- var startTicks = __secondsToTicks(${startSeconds}).toString();
53
- seq.insertClip(item, startTicks, ${trackIndex}, ${audioTrackIndex});
54
-
55
- return __result({
56
- added: true,
57
- item: item.name,
58
- trackIndex: ${trackIndex},
59
- startSeconds: ${startSeconds}
60
- });
45
+ const script = buildToolScript(`
46
+ var seq = app.project.activeSequence;
47
+ if (!seq) return __error("No active sequence");
48
+
49
+ var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
50
+ if (!item) return __error("Project item not found: ${escapeForExtendScript(args.item_id)}");
51
+
52
+ var startTicks = __secondsToTicks(${startSeconds}).toString();
53
+ seq.insertClip(item, startTicks, ${trackIndex}, ${audioTrackIndex});
54
+
55
+ return __result({
56
+ added: true,
57
+ item: item.name,
58
+ trackIndex: ${trackIndex},
59
+ startSeconds: ${startSeconds}
60
+ });
61
61
  `);
62
62
  return sendCommand(script, bridgeOptions);
63
63
  },
@@ -79,27 +79,27 @@ Example: add_to_timeline({ item_id: "interview.mp4", track_index: 0, start_secon
79
79
  required: ["node_id"],
80
80
  },
81
81
  handler: async (args) => {
82
- const script = buildToolScript(`
83
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
84
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
85
-
86
- var clip = result.clip;
87
- clip.remove(${args.ripple ? "true" : "false"}, ${args.ripple ? "true" : "false"});
88
- return __result({ removed: true, clipName: clip.name });
82
+ const script = buildToolScript(`
83
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
84
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
85
+
86
+ var clip = result.clip;
87
+ clip.remove(${args.ripple ? "true" : "false"}, ${args.ripple ? "true" : "false"});
88
+ return __result({ removed: true, clipName: clip.name });
89
89
  `);
90
90
  return sendCommand(script, bridgeOptions);
91
91
  },
92
92
  },
93
93
  move_clip: {
94
- description: `Move a clip to a new timeline position, optionally changing its track.
95
-
96
- Preconditions: An active sequence must exist and the clip's node_id must be valid (get node IDs from get_active_sequence or list_sequence_tracks).
97
-
98
- The clip moves to the exact start time specified in seconds. If new_track_index is provided, the clip is also moved to that track. The audio/video link is maintained during the move.
99
-
100
- Note: This uses clip.moveToTime() which is the correct ExtendScript API — clip.start is read-only and cannot be assigned directly.
101
-
102
- Example: move_clip({ node_id: "abc123", new_start_seconds: 45.5 })
94
+ description: `Move a clip to a new timeline position, optionally changing its track.
95
+
96
+ Preconditions: An active sequence must exist and the clip's node_id must be valid (get node IDs from get_active_sequence or list_sequence_tracks).
97
+
98
+ The clip moves to the exact start time specified in seconds. If new_track_index is provided, the clip is also moved to that track. The audio/video link is maintained during the move.
99
+
100
+ Note: This uses clip.moveToTime() which is the correct ExtendScript API — clip.start is read-only and cannot be assigned directly.
101
+
102
+ Example: move_clip({ node_id: "abc123", new_start_seconds: 45.5 })
103
103
  Example: move_clip({ node_id: "abc123", new_start_seconds: 10.0, new_track_index: 1 })`,
104
104
  parameters: {
105
105
  type: "object",
@@ -120,52 +120,52 @@ Example: move_clip({ node_id: "abc123", new_start_seconds: 10.0, new_track_index
120
120
  required: ["node_id", "new_start_seconds"],
121
121
  },
122
122
  handler: async (args) => {
123
- const script = buildToolScript(`
124
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
125
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
126
-
127
- var clip = result.clip;
128
-
129
- // clip.start is read-only; clip.move(newInPointTime) is the documented DOM reposition.
130
- var newTime = new Time();
131
- newTime.seconds = ${args.new_start_seconds};
132
- clip.move(newTime);
133
-
134
- var trackChangeNote = null;
135
- ${args.new_track_index !== undefined ? `
136
- // DOM TrackItem has no track-change method; route via QE if available.
137
- try {
138
- app.enableQE();
139
- var qeSeq = qe.project.getActiveSequence();
140
- var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
141
- var qeClip = qeTrack.getItemAt(result.clipIndex);
142
- qeClip.moveToTrack(${args.new_track_index});
143
- } catch (e) {
144
- trackChangeNote = "DOM track-change isn't supported; QE move failed: " + e.toString();
145
- }
146
- ` : ""}
147
-
148
- return __result({
149
- moved: true,
150
- clipName: clip.name,
151
- newStart: ${args.new_start_seconds},
152
- trackChangeNote: trackChangeNote
153
- });
123
+ const script = buildToolScript(`
124
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
125
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
126
+
127
+ var clip = result.clip;
128
+
129
+ // clip.start is read-only; clip.move(newInPointTime) is the documented DOM reposition.
130
+ var newTime = new Time();
131
+ newTime.seconds = ${args.new_start_seconds};
132
+ clip.move(newTime);
133
+
134
+ var trackChangeNote = null;
135
+ ${args.new_track_index !== undefined ? `
136
+ // DOM TrackItem has no track-change method; route via QE if available.
137
+ try {
138
+ app.enableQE();
139
+ var qeSeq = qe.project.getActiveSequence();
140
+ var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
141
+ var qeClip = qeTrack.getItemAt(result.clipIndex);
142
+ qeClip.moveToTrack(${args.new_track_index});
143
+ } catch (e) {
144
+ trackChangeNote = "DOM track-change isn't supported; QE move failed: " + e.toString();
145
+ }
146
+ ` : ""}
147
+
148
+ return __result({
149
+ moved: true,
150
+ clipName: clip.name,
151
+ newStart: ${args.new_start_seconds},
152
+ trackChangeNote: trackChangeNote
153
+ });
154
154
  `);
155
155
  return sendCommand(script, bridgeOptions);
156
156
  },
157
157
  },
158
158
  trim_clip: {
159
- description: `Trim a clip's in-point and/or out-point on the timeline.
160
-
161
- Preconditions: Clip must exist on the active sequence (use node_id from get_active_sequence).
162
-
163
- Time values are in seconds relative to the clip's source media (not the timeline position). For example, if a 60-second clip starts at timeline position 10s, setting new_in_seconds=5 trims the first 5 seconds of the source, making the clip start 5s into the original footage.
164
-
165
- Note: Uses clip.setInPoint(time, 2) and clip.setOutPoint(time, 2) — the alignment constant 2 (kTime) is required. Direct assignment to clip.inPoint / clip.outPoint does not work in ExtendScript.
166
-
167
- Example: trim_clip({ node_id: "abc123", new_in_seconds: 2.0 }) — trim 2s off the start
168
- Example: trim_clip({ node_id: "abc123", new_out_seconds: 58.5 }) — trim the end to 58.5s
159
+ description: `Trim a clip's in-point and/or out-point on the timeline.
160
+
161
+ Preconditions: Clip must exist on the active sequence (use node_id from get_active_sequence).
162
+
163
+ Time values are in seconds relative to the clip's source media (not the timeline position). For example, if a 60-second clip starts at timeline position 10s, setting new_in_seconds=5 trims the first 5 seconds of the source, making the clip start 5s into the original footage.
164
+
165
+ Note: Uses clip.setInPoint(time, 2) and clip.setOutPoint(time, 2) — the alignment constant 2 (kTime) is required. Direct assignment to clip.inPoint / clip.outPoint does not work in ExtendScript.
166
+
167
+ Example: trim_clip({ node_id: "abc123", new_in_seconds: 2.0 }) — trim 2s off the start
168
+ Example: trim_clip({ node_id: "abc123", new_out_seconds: 58.5 }) — trim the end to 58.5s
169
169
  Example: trim_clip({ node_id: "abc123", new_in_seconds: 1.0, new_out_seconds: 59.0 }) — trim both ends`,
170
170
  parameters: {
171
171
  type: "object",
@@ -186,55 +186,55 @@ Example: trim_clip({ node_id: "abc123", new_in_seconds: 1.0, new_out_seconds: 59
186
186
  required: ["node_id"],
187
187
  },
188
188
  handler: async (args) => {
189
- const script = buildToolScript(`
190
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
191
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
192
-
193
- var clip = result.clip;
194
-
195
- // DOM TrackItem has no setInPoint/setOutPoint; route the trim via QE if available.
196
- var qeClip = null;
197
- try {
198
- app.enableQE();
199
- var qeSeq = qe.project.getActiveSequence();
200
- var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
201
- qeClip = qeTrack.getItemAt(result.clipIndex);
202
- } catch (e) {
203
- qeClip = null;
204
- }
205
-
206
- if (!qeClip) {
207
- return __error("Timeline trim isn't supported via this path (DOM TrackItem has no setInPoint/setOutPoint, and QE is unavailable)");
208
- }
209
-
210
- var didTrim = false;
211
- ${args.new_in_seconds !== undefined ? `
212
- qeClip.setInPoint(__secondsToTicks(${args.new_in_seconds}).toString());
213
- didTrim = true;
214
- ` : ""}
215
- ${args.new_out_seconds !== undefined ? `
216
- qeClip.setOutPoint(__secondsToTicks(${args.new_out_seconds}).toString());
217
- didTrim = true;
218
- ` : ""}
219
-
220
- return __result({
221
- trimmed: didTrim,
222
- clipName: clip.name,
223
- newInSeconds: ${args.new_in_seconds !== undefined ? args.new_in_seconds : "null"},
224
- newOutSeconds: ${args.new_out_seconds !== undefined ? args.new_out_seconds : "null"}
225
- });
189
+ const script = buildToolScript(`
190
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
191
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
192
+
193
+ var clip = result.clip;
194
+
195
+ // DOM TrackItem has no setInPoint/setOutPoint; route the trim via QE if available.
196
+ var qeClip = null;
197
+ try {
198
+ app.enableQE();
199
+ var qeSeq = qe.project.getActiveSequence();
200
+ var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
201
+ qeClip = qeTrack.getItemAt(result.clipIndex);
202
+ } catch (e) {
203
+ qeClip = null;
204
+ }
205
+
206
+ if (!qeClip) {
207
+ return __error("Timeline trim isn't supported via this path (DOM TrackItem has no setInPoint/setOutPoint, and QE is unavailable)");
208
+ }
209
+
210
+ var didTrim = false;
211
+ ${args.new_in_seconds !== undefined ? `
212
+ qeClip.setInPoint(__secondsToTicks(${args.new_in_seconds}).toString());
213
+ didTrim = true;
214
+ ` : ""}
215
+ ${args.new_out_seconds !== undefined ? `
216
+ qeClip.setOutPoint(__secondsToTicks(${args.new_out_seconds}).toString());
217
+ didTrim = true;
218
+ ` : ""}
219
+
220
+ return __result({
221
+ trimmed: didTrim,
222
+ clipName: clip.name,
223
+ newInSeconds: ${args.new_in_seconds !== undefined ? args.new_in_seconds : "null"},
224
+ newOutSeconds: ${args.new_out_seconds !== undefined ? args.new_out_seconds : "null"}
225
+ });
226
226
  `);
227
227
  return sendCommand(script, bridgeOptions);
228
228
  },
229
229
  },
230
230
  split_clip: {
231
- description: `Split (razor cut) a clip at a specific timeline position. Uses QE DOM.
232
-
233
- Preconditions: An active sequence must exist with at least one clip on the specified track. The time_seconds must fall within a clip on the specified track — splitting empty space has no effect.
234
-
235
- After the split, the original clip becomes two separate clips that can be independently moved, trimmed, or deleted.
236
-
237
- Example: split_clip({ time_seconds: 30.0 }) — split V1 at 30 seconds
231
+ description: `Split (razor cut) a clip at a specific timeline position. Uses QE DOM.
232
+
233
+ Preconditions: An active sequence must exist with at least one clip on the specified track. The time_seconds must fall within a clip on the specified track — splitting empty space has no effect.
234
+
235
+ After the split, the original clip becomes two separate clips that can be independently moved, trimmed, or deleted.
236
+
237
+ Example: split_clip({ time_seconds: 30.0 }) — split V1 at 30 seconds
238
238
  Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })`,
239
239
  parameters: {
240
240
  type: "object",
@@ -258,18 +258,18 @@ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })
258
258
  handler: async (args) => {
259
259
  const trackType = args.track_type || "video";
260
260
  const trackIndex = args.track_index ?? 0;
261
- const script = buildToolScript(`
262
- app.enableQE();
263
- var seq = qe.project.getActiveSequence();
264
- if (!seq) return __error("No active sequence (QE)");
265
-
266
- var track = ${trackType === "video" ? `seq.getVideoTrackAt(${trackIndex})` : `seq.getAudioTrackAt(${trackIndex})`};
267
- if (!track) return __error("Track not found");
268
-
269
- var timeTicks = __secondsToTicks(${args.time_seconds}).toString();
270
- track.razor(timeTicks);
271
-
272
- return __result({ split: true, atSeconds: ${args.time_seconds}, trackIndex: ${trackIndex}, trackType: "${trackType}" });
261
+ const script = buildToolScript(`
262
+ app.enableQE();
263
+ var seq = qe.project.getActiveSequence();
264
+ if (!seq) return __error("No active sequence (QE)");
265
+
266
+ var track = ${trackType === "video" ? `seq.getVideoTrackAt(${trackIndex})` : `seq.getAudioTrackAt(${trackIndex})`};
267
+ if (!track) return __error("Track not found");
268
+
269
+ var timeTicks = __secondsToTicks(${args.time_seconds}).toString();
270
+ track.razor(timeTicks);
271
+
272
+ return __result({ split: true, atSeconds: ${args.time_seconds}, trackIndex: ${trackIndex}, trackType: "${trackType}" });
273
273
  `);
274
274
  return sendCommand(script, bridgeOptions);
275
275
  },
@@ -287,22 +287,22 @@ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })
287
287
  required: ["node_id"],
288
288
  },
289
289
  handler: async (args) => {
290
- const script = buildToolScript(`
291
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
292
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
293
-
294
- var clip = result.clip;
295
- var seq = app.project.activeSequence;
296
- var projectItem = clip.projectItem;
297
-
298
- if (!projectItem) return __error("Cannot find source project item for clip");
299
-
300
- var newTrackIndex = result.trackIndex + 1;
301
- var startTicks = clip.start.ticks;
302
-
303
- seq.insertClip(projectItem, startTicks, newTrackIndex, newTrackIndex);
304
-
305
- return __result({ duplicated: true, clipName: clip.name, newTrackIndex: newTrackIndex });
290
+ const script = buildToolScript(`
291
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
292
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
293
+
294
+ var clip = result.clip;
295
+ var seq = app.project.activeSequence;
296
+ var projectItem = clip.projectItem;
297
+
298
+ if (!projectItem) return __error("Cannot find source project item for clip");
299
+
300
+ var newTrackIndex = result.trackIndex + 1;
301
+ var startTicks = clip.start.ticks;
302
+
303
+ seq.insertClip(projectItem, startTicks, newTrackIndex, newTrackIndex);
304
+
305
+ return __result({ duplicated: true, clipName: clip.name, newTrackIndex: newTrackIndex });
306
306
  `);
307
307
  return sendCommand(script, bridgeOptions);
308
308
  },
@@ -324,17 +324,17 @@ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })
324
324
  required: ["node_id", "enabled"],
325
325
  },
326
326
  handler: async (args) => {
327
- const script = buildToolScript(`
328
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
329
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
330
-
331
- // PP 2026: clip.disabled is a property, clip.setDisabled() does not exist.
332
- try {
333
- result.clip.disabled = ${args.enabled ? "false" : "true"};
334
- } catch(e1) {
335
- try { result.clip.setDisabled(${args.enabled ? "false" : "true"}); } catch(e2) {}
336
- }
337
- return __result({ clipName: result.clip.name, enabled: ${args.enabled} });
327
+ const script = buildToolScript(`
328
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
329
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
330
+
331
+ // PP 2026: clip.disabled is a property, clip.setDisabled() does not exist.
332
+ try {
333
+ result.clip.disabled = ${args.enabled ? "false" : "true"};
334
+ } catch(e1) {
335
+ try { result.clip.setDisabled(${args.enabled ? "false" : "true"}); } catch(e2) {}
336
+ }
337
+ return __result({ clipName: result.clip.name, enabled: ${args.enabled} });
338
338
  `);
339
339
  return sendCommand(script, bridgeOptions);
340
340
  },
@@ -376,68 +376,68 @@ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })
376
376
  required: ["node_id"],
377
377
  },
378
378
  handler: async (args) => {
379
- const script = buildToolScript(`
380
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
381
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
382
-
383
- var clip = result.clip;
384
- var changes = {};
385
-
386
- ${args.opacity !== undefined ? `
387
- // Set opacity via the intrinsic Opacity component (locale-independent)
388
- var opacityComp = __findIntrinsic(clip, "opacity");
389
- if (opacityComp) {
390
- var opacityProp = __findProp(opacityComp, ["Opacity", "Opacidad", "Deckkraft", "Opacité"]);
391
- if (opacityProp) {
392
- opacityProp.setValue(${args.opacity}, true);
393
- changes.opacity = ${args.opacity};
394
- }
395
- }
396
- ` : ""}
397
-
398
- ${args.speed !== undefined ? `
399
- // PP 2026 setSpeed: (percent, frameInterp, ripple, reverse, maintainAudioPitch).
400
- app.enableQE();
401
- var qeSeq = qe.project.getActiveSequence();
402
- var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
403
- var qeClip = qeTrack.getItemAt(result.clipIndex);
404
- var speedOk = false;
405
- try { qeClip.setSpeed(${args.speed * 100}, "", false, false, false); speedOk = true; } catch (e1) {}
406
- if (!speedOk) { try { qeClip.setSpeed(${args.speed * 100}, false, false); speedOk = true; } catch (e2) {} }
407
- if (!speedOk) { try { qeClip.setSpeed(${args.speed * 100}); speedOk = true; } catch (e3) {} }
408
- if (speedOk) { changes.speed = ${args.speed}; } else { changes.speedError = "setSpeed failed for all known signatures"; }
409
- ` : ""}
410
-
411
- ${args.scale !== undefined || args.position_x !== undefined || args.position_y !== undefined || args.rotation !== undefined ? `
412
- // Set transform via the intrinsic Motion component (locale-independent)
413
- var motionComp = __findIntrinsic(clip, "motion");
414
- if (motionComp) {
415
- ${args.scale !== undefined ? `
416
- var scaleProp = __findProp(motionComp, ["Scale", "Escala", "Skalierung", "Échelle"]);
417
- if (scaleProp) {
418
- scaleProp.setValue(${args.scale}, true);
419
- changes.scale = ${args.scale};
420
- }` : ""}
421
- ${args.position_x !== undefined || args.position_y !== undefined ? `
422
- var posProp = __findProp(motionComp, ["Position", "Posición", "Position"]);
423
- if (posProp) {
424
- var posVal = posProp.getValue();
425
- var px = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[0] : 0;
426
- var py = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[1] : 0;
427
- ${args.position_x !== undefined ? `px = ${args.position_x}; changes.position_x = ${args.position_x};` : ""}
428
- ${args.position_y !== undefined ? `py = ${args.position_y}; changes.position_y = ${args.position_y};` : ""}
429
- posProp.setValue([px, py], true);
430
- }` : ""}
431
- ${args.rotation !== undefined ? `
432
- var rotProp = __findProp(motionComp, ["Rotation", "Rotación", "Drehung"]);
433
- if (rotProp) {
434
- rotProp.setValue(${args.rotation}, true);
435
- changes.rotation = ${args.rotation};
436
- }` : ""}
437
- }
438
- ` : ""}
439
-
440
- return __result({ updated: true, clipName: clip.name, changes: changes });
379
+ const script = buildToolScript(`
380
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
381
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
382
+
383
+ var clip = result.clip;
384
+ var changes = {};
385
+
386
+ ${args.opacity !== undefined ? `
387
+ // Set opacity via the intrinsic Opacity component (locale-independent)
388
+ var opacityComp = __findIntrinsic(clip, "opacity");
389
+ if (opacityComp) {
390
+ var opacityProp = __findProp(opacityComp, ["Opacity", "Opacidad", "Deckkraft", "Opacité"]);
391
+ if (opacityProp) {
392
+ opacityProp.setValue(${args.opacity}, true);
393
+ changes.opacity = ${args.opacity};
394
+ }
395
+ }
396
+ ` : ""}
397
+
398
+ ${args.speed !== undefined ? `
399
+ // PP 2026 setSpeed: (percent, frameInterp, ripple, reverse, maintainAudioPitch).
400
+ app.enableQE();
401
+ var qeSeq = qe.project.getActiveSequence();
402
+ var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
403
+ var qeClip = qeTrack.getItemAt(result.clipIndex);
404
+ var speedOk = false;
405
+ try { qeClip.setSpeed(${args.speed * 100}, "", false, false, false); speedOk = true; } catch (e1) {}
406
+ if (!speedOk) { try { qeClip.setSpeed(${args.speed * 100}, false, false); speedOk = true; } catch (e2) {} }
407
+ if (!speedOk) { try { qeClip.setSpeed(${args.speed * 100}); speedOk = true; } catch (e3) {} }
408
+ if (speedOk) { changes.speed = ${args.speed}; } else { changes.speedError = "setSpeed failed for all known signatures"; }
409
+ ` : ""}
410
+
411
+ ${args.scale !== undefined || args.position_x !== undefined || args.position_y !== undefined || args.rotation !== undefined ? `
412
+ // Set transform via the intrinsic Motion component (locale-independent)
413
+ var motionComp = __findIntrinsic(clip, "motion");
414
+ if (motionComp) {
415
+ ${args.scale !== undefined ? `
416
+ var scaleProp = __findProp(motionComp, ["Scale", "Escala", "Skalierung", "Échelle"]);
417
+ if (scaleProp) {
418
+ scaleProp.setValue(${args.scale}, true);
419
+ changes.scale = ${args.scale};
420
+ }` : ""}
421
+ ${args.position_x !== undefined || args.position_y !== undefined ? `
422
+ var posProp = __findProp(motionComp, ["Position", "Posición", "Position"]);
423
+ if (posProp) {
424
+ var posVal = posProp.getValue();
425
+ var px = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[0] : 0;
426
+ var py = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[1] : 0;
427
+ ${args.position_x !== undefined ? `px = ${args.position_x}; changes.position_x = ${args.position_x};` : ""}
428
+ ${args.position_y !== undefined ? `py = ${args.position_y}; changes.position_y = ${args.position_y};` : ""}
429
+ posProp.setValue([px, py], true);
430
+ }` : ""}
431
+ ${args.rotation !== undefined ? `
432
+ var rotProp = __findProp(motionComp, ["Rotation", "Rotación", "Drehung"]);
433
+ if (rotProp) {
434
+ rotProp.setValue(${args.rotation}, true);
435
+ changes.rotation = ${args.rotation};
436
+ }` : ""}
437
+ }
438
+ ` : ""}
439
+
440
+ return __result({ updated: true, clipName: clip.name, changes: changes });
441
441
  `);
442
442
  return sendCommand(script, bridgeOptions);
443
443
  },
@@ -459,59 +459,59 @@ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })
459
459
  required: ["node_id", "new_item_id"],
460
460
  },
461
461
  handler: async (args) => {
462
- const script = buildToolScript(`
463
- var seq = app.project.activeSequence;
464
- if (!seq) return __error("No active sequence");
465
-
466
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
467
- if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
468
-
469
- var newItem = __findProjectItem("${escapeForExtendScript(args.new_item_id)}");
470
- if (!newItem) return __error("Replacement project item not found: ${escapeForExtendScript(args.new_item_id)}");
471
-
472
- var clip = result.clip;
473
- var oldName = clip.name;
474
- var startTicks = clip.start.ticks;
475
- var trackIndex = result.trackIndex;
476
- var trackType = result.trackType;
477
-
478
- // Remove old clip
479
- clip.remove(false, false);
480
-
481
- // Insert new clip at same position
482
- if (trackType === "video") {
483
- seq.insertClip(newItem, startTicks, trackIndex, trackIndex);
484
- } else {
485
- seq.insertClip(newItem, startTicks, 0, trackIndex);
486
- }
487
-
488
- return __result({
489
- replaced: true,
490
- oldClip: oldName,
491
- newClip: newItem.name,
492
- trackIndex: trackIndex,
493
- trackType: trackType
494
- });
462
+ const script = buildToolScript(`
463
+ var seq = app.project.activeSequence;
464
+ if (!seq) return __error("No active sequence");
465
+
466
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
467
+ if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
468
+
469
+ var newItem = __findProjectItem("${escapeForExtendScript(args.new_item_id)}");
470
+ if (!newItem) return __error("Replacement project item not found: ${escapeForExtendScript(args.new_item_id)}");
471
+
472
+ var clip = result.clip;
473
+ var oldName = clip.name;
474
+ var startTicks = clip.start.ticks;
475
+ var trackIndex = result.trackIndex;
476
+ var trackType = result.trackType;
477
+
478
+ // Remove old clip
479
+ clip.remove(false, false);
480
+
481
+ // Insert new clip at same position
482
+ if (trackType === "video") {
483
+ seq.insertClip(newItem, startTicks, trackIndex, trackIndex);
484
+ } else {
485
+ seq.insertClip(newItem, startTicks, 0, trackIndex);
486
+ }
487
+
488
+ return __result({
489
+ replaced: true,
490
+ oldClip: oldName,
491
+ newClip: newItem.name,
492
+ trackIndex: trackIndex,
493
+ trackType: trackType
494
+ });
495
495
  `);
496
496
  return sendCommand(script, bridgeOptions);
497
497
  },
498
498
  },
499
499
  speed_change: {
500
- description: `Change the playback speed of a clip on the timeline.
501
-
502
- Preconditions: Active sequence required. Clip node_id must be valid.
503
-
504
- Speed is specified as a percentage number (NOT a multiplier):
505
- - 100 = normal speed
506
- - 200 = double speed (2x faster)
507
- - 50 = half speed (slow motion)
508
- - 25 = quarter speed
509
-
510
- Use reverse=true to play the clip backwards (negative speed). This internally uses a negative percentage.
511
-
512
- Note: setSpeed() expects a numeric percentage, not a string and not a decimal multiplier. Passing "50" as a string or 0.5 as a multiplier would fail.
513
-
514
- Example: speed_change({ node_id: "abc123", speed_percent: 200 }) — 2x speed
500
+ description: `Change the playback speed of a clip on the timeline.
501
+
502
+ Preconditions: Active sequence required. Clip node_id must be valid.
503
+
504
+ Speed is specified as a percentage number (NOT a multiplier):
505
+ - 100 = normal speed
506
+ - 200 = double speed (2x faster)
507
+ - 50 = half speed (slow motion)
508
+ - 25 = quarter speed
509
+
510
+ Use reverse=true to play the clip backwards (negative speed). This internally uses a negative percentage.
511
+
512
+ Note: setSpeed() expects a numeric percentage, not a string and not a decimal multiplier. Passing "50" as a string or 0.5 as a multiplier would fail.
513
+
514
+ Example: speed_change({ node_id: "abc123", speed_percent: 200 }) — 2x speed
515
515
  Example: speed_change({ node_id: "abc123", speed_percent: 50, reverse: true }) — half speed, reversed`,
516
516
  parameters: {
517
517
  type: "object",
@@ -532,40 +532,40 @@ Example: speed_change({ node_id: "abc123", speed_percent: 50, reverse: true })
532
532
  required: ["node_id", "speed_percent"],
533
533
  },
534
534
  handler: async (args) => {
535
- const script = buildToolScript(`
536
- var result = __findClip("${escapeForExtendScript(args.node_id)}");
537
- if (!result) return __error("Clip not found");
538
-
539
- var clip = result.clip;
540
-
541
- // setSpeed expects a numeric percentage (50 = 50%, NOT 0.5 and NOT the string "50").
542
- // Validate and ensure it is a positive number.
543
- var speedPercent = parseFloat("${args.speed_percent}");
544
- if (isNaN(speedPercent) || speedPercent <= 0) {
545
- return __error("Invalid speed_percent: must be a positive number (e.g. 100 = normal, 50 = half, 200 = double)");
546
- }
547
-
548
- // qeClip.setSpeed signature in PP 2026: (percent, frameInterpolation:string,
549
- // ripple:bool, reverse:bool, maintainAudioPitch:bool). Verified at runtime
550
- // by exhaustive probing — older PP versions used (percent, ripple, reverse).
551
- // Try PP 2026 signature first, then fall back to older 3-arg form.
552
- app.enableQE();
553
- var qeSeq = qe.project.getActiveSequence();
554
- var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
555
- var qeClip = qeTrack.getItemAt(result.clipIndex);
556
- var rev = ${args.reverse ? "true" : "false"};
557
- var ok = false, lastErr = "";
558
- // PP 2026: (percent, frameInterpolation, ripple, reverse, maintainAudioPitch)
559
- // empty string accepts default frame sampling
560
- try { qeClip.setSpeed(speedPercent, "", false, rev, false); ok = true; } catch (e1) { lastErr = e1.toString(); }
561
- // Older PP: (percent, ripple, reverse)
562
- if (!ok) { try { qeClip.setSpeed(speedPercent, false, rev); ok = true; } catch (e2) { lastErr = e2.toString(); } }
563
- if (!ok) { try { qeClip.setSpeed(speedPercent, false); ok = true; } catch (e3) { lastErr = e3.toString(); } }
564
- if (!ok) {
565
- return __error("speed_change failed (last error: " + lastErr + "). Workaround: select the clip and use Clip > Speed/Duration in the UI.");
566
- }
567
-
568
- return __result({ speedChanged: true, clipName: clip.name, speedPercent: speedPercent, reverse: ${!!args.reverse}, currentSpeed: qeClip.speed });
535
+ const script = buildToolScript(`
536
+ var result = __findClip("${escapeForExtendScript(args.node_id)}");
537
+ if (!result) return __error("Clip not found");
538
+
539
+ var clip = result.clip;
540
+
541
+ // setSpeed expects a numeric percentage (50 = 50%, NOT 0.5 and NOT the string "50").
542
+ // Validate and ensure it is a positive number.
543
+ var speedPercent = parseFloat("${args.speed_percent}");
544
+ if (isNaN(speedPercent) || speedPercent <= 0) {
545
+ return __error("Invalid speed_percent: must be a positive number (e.g. 100 = normal, 50 = half, 200 = double)");
546
+ }
547
+
548
+ // qeClip.setSpeed signature in PP 2026: (percent, frameInterpolation:string,
549
+ // ripple:bool, reverse:bool, maintainAudioPitch:bool). Verified at runtime
550
+ // by exhaustive probing — older PP versions used (percent, ripple, reverse).
551
+ // Try PP 2026 signature first, then fall back to older 3-arg form.
552
+ app.enableQE();
553
+ var qeSeq = qe.project.getActiveSequence();
554
+ var qeTrack = result.trackType === "video" ? qeSeq.getVideoTrackAt(result.trackIndex) : qeSeq.getAudioTrackAt(result.trackIndex);
555
+ var qeClip = qeTrack.getItemAt(result.clipIndex);
556
+ var rev = ${args.reverse ? "true" : "false"};
557
+ var ok = false, lastErr = "";
558
+ // PP 2026: (percent, frameInterpolation, ripple, reverse, maintainAudioPitch)
559
+ // empty string accepts default frame sampling
560
+ try { qeClip.setSpeed(speedPercent, "", false, rev, false); ok = true; } catch (e1) { lastErr = e1.toString(); }
561
+ // Older PP: (percent, ripple, reverse)
562
+ if (!ok) { try { qeClip.setSpeed(speedPercent, false, rev); ok = true; } catch (e2) { lastErr = e2.toString(); } }
563
+ if (!ok) { try { qeClip.setSpeed(speedPercent, false); ok = true; } catch (e3) { lastErr = e3.toString(); } }
564
+ if (!ok) {
565
+ return __error("speed_change failed (last error: " + lastErr + "). Workaround: select the clip and use Clip > Speed/Duration in the UI.");
566
+ }
567
+
568
+ return __result({ speedChanged: true, clipName: clip.name, speedPercent: speedPercent, reverse: ${!!args.reverse}, currentSpeed: qeClip.speed });
569
569
  `);
570
570
  return sendCommand(script, bridgeOptions);
571
571
  },