@synthetic-ai/premiere-mcp 2.0.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 (104) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/cep-plugin/.debug +8 -0
  5. package/cep-plugin/CSInterface.js +70 -0
  6. package/cep-plugin/CSXS/manifest.xml +49 -0
  7. package/cep-plugin/host.jsx +7 -0
  8. package/cep-plugin/index.html +419 -0
  9. package/cep-plugin/main.js +327 -0
  10. package/cep-plugin/premiere.jsx +24947 -0
  11. package/cep-plugin/synthetic-logo.png +0 -0
  12. package/dist/bridge/file-bridge.d.ts +29 -0
  13. package/dist/bridge/file-bridge.js +145 -0
  14. package/dist/bridge/script-builder.d.ts +19 -0
  15. package/dist/bridge/script-builder.js +408 -0
  16. package/dist/http-server.d.ts +21 -0
  17. package/dist/http-server.js +140 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +70 -0
  20. package/dist/resources/extendscript-reference.d.ts +2 -0
  21. package/dist/resources/extendscript-reference.js +219 -0
  22. package/dist/server.d.ts +4 -0
  23. package/dist/server.js +276 -0
  24. package/dist/tools/advanced.d.ts +530 -0
  25. package/dist/tools/advanced.js +923 -0
  26. package/dist/tools/audio-advanced.d.ts +847 -0
  27. package/dist/tools/audio-advanced.js +1653 -0
  28. package/dist/tools/audio.d.ts +84 -0
  29. package/dist/tools/audio.js +123 -0
  30. package/dist/tools/captions.d.ts +30 -0
  31. package/dist/tools/captions.js +73 -0
  32. package/dist/tools/clipboard.d.ts +153 -0
  33. package/dist/tools/clipboard.js +381 -0
  34. package/dist/tools/color-advanced.d.ts +821 -0
  35. package/dist/tools/color-advanced.js +772 -0
  36. package/dist/tools/discovery.d.ts +128 -0
  37. package/dist/tools/discovery.js +445 -0
  38. package/dist/tools/effects.d.ts +211 -0
  39. package/dist/tools/effects.js +554 -0
  40. package/dist/tools/export-presets.d.ts +333 -0
  41. package/dist/tools/export-presets.js +490 -0
  42. package/dist/tools/export.d.ts +320 -0
  43. package/dist/tools/export.js +618 -0
  44. package/dist/tools/health.d.ts +9 -0
  45. package/dist/tools/health.js +25 -0
  46. package/dist/tools/inspection.d.ts +179 -0
  47. package/dist/tools/inspection.js +1089 -0
  48. package/dist/tools/keyframes.d.ts +253 -0
  49. package/dist/tools/keyframes.js +611 -0
  50. package/dist/tools/markers.d.ts +117 -0
  51. package/dist/tools/markers.js +206 -0
  52. package/dist/tools/media.d.ts +297 -0
  53. package/dist/tools/media.js +493 -0
  54. package/dist/tools/metadata.d.ts +178 -0
  55. package/dist/tools/metadata.js +278 -0
  56. package/dist/tools/multicam.d.ts +199 -0
  57. package/dist/tools/multicam.js +212 -0
  58. package/dist/tools/playback.d.ts +34 -0
  59. package/dist/tools/playback.js +63 -0
  60. package/dist/tools/playhead.d.ts +77 -0
  61. package/dist/tools/playhead.js +185 -0
  62. package/dist/tools/project-manager.d.ts +65 -0
  63. package/dist/tools/project-manager.js +81 -0
  64. package/dist/tools/project.d.ts +390 -0
  65. package/dist/tools/project.js +596 -0
  66. package/dist/tools/scripting.d.ts +98 -0
  67. package/dist/tools/scripting.js +434 -0
  68. package/dist/tools/selection.d.ts +120 -0
  69. package/dist/tools/selection.js +334 -0
  70. package/dist/tools/sequence-advanced.d.ts +84 -0
  71. package/dist/tools/sequence-advanced.js +471 -0
  72. package/dist/tools/sequence-settings.d.ts +397 -0
  73. package/dist/tools/sequence-settings.js +967 -0
  74. package/dist/tools/sequence.d.ts +211 -0
  75. package/dist/tools/sequence.js +378 -0
  76. package/dist/tools/source-monitor.d.ts +95 -0
  77. package/dist/tools/source-monitor.js +180 -0
  78. package/dist/tools/text.d.ts +97 -0
  79. package/dist/tools/text.js +159 -0
  80. package/dist/tools/timeline.d.ts +265 -0
  81. package/dist/tools/timeline.js +575 -0
  82. package/dist/tools/track-management.d.ts +482 -0
  83. package/dist/tools/track-management.js +787 -0
  84. package/dist/tools/track-targeting.d.ts +666 -0
  85. package/dist/tools/track-targeting.js +1318 -0
  86. package/dist/tools/tracks.d.ts +90 -0
  87. package/dist/tools/tracks.js +183 -0
  88. package/dist/tools/transitions-motion.d.ts +788 -0
  89. package/dist/tools/transitions-motion.js +680 -0
  90. package/dist/tools/transitions.d.ts +124 -0
  91. package/dist/tools/transitions.js +267 -0
  92. package/dist/tools/utility.d.ts +469 -0
  93. package/dist/tools/utility.js +1118 -0
  94. package/dist/tools/workspace.d.ts +25 -0
  95. package/dist/tools/workspace.js +61 -0
  96. package/package.json +66 -0
  97. package/scripts/build-chat-dmg.sh +210 -0
  98. package/scripts/build-chat-zip.sh +291 -0
  99. package/scripts/install-cep-windows.ps1 +99 -0
  100. package/scripts/install-cep.sh +66 -0
  101. package/scripts/install-chat-plugin.bat +67 -0
  102. package/scripts/install-chat-plugin.sh +85 -0
  103. package/scripts/install-windows.bat +95 -0
  104. package/scripts/package.mjs +218 -0
@@ -0,0 +1,575 @@
1
+ import { buildToolScript, escapeForExtendScript } from "../bridge/script-builder.js";
2
+ import { sendCommand } from "../bridge/file-bridge.js";
3
+ export function getTimelineTools(bridgeOptions) {
4
+ return {
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
+
18
+ Example: add_to_timeline({ item_id: "interview.mp4", track_index: 0, start_seconds: 30, audio_track_index: 0 })`,
19
+ parameters: {
20
+ type: "object",
21
+ properties: {
22
+ item_id: {
23
+ type: "string",
24
+ description: "Node ID or name of the project item to add",
25
+ },
26
+ track_index: {
27
+ type: "number",
28
+ description: "Video track index (0-based, default: 0)",
29
+ },
30
+ start_seconds: {
31
+ type: "number",
32
+ description: "Start time in seconds on the timeline (default: 0)",
33
+ },
34
+ audio_track_index: {
35
+ type: "number",
36
+ description: "Audio track index for the audio portion (default: 0)",
37
+ },
38
+ },
39
+ required: ["item_id"],
40
+ },
41
+ handler: async (args) => {
42
+ const trackIndex = args.track_index ?? 0;
43
+ const startSeconds = args.start_seconds ?? 0;
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
+ });
61
+ `);
62
+ return sendCommand(script, bridgeOptions);
63
+ },
64
+ },
65
+ remove_from_timeline: {
66
+ description: "Remove a clip from the timeline",
67
+ parameters: {
68
+ type: "object",
69
+ properties: {
70
+ node_id: {
71
+ type: "string",
72
+ description: "Node ID of the clip to remove",
73
+ },
74
+ ripple: {
75
+ type: "boolean",
76
+ description: "Whether to ripple delete (close the gap). Default: false",
77
+ },
78
+ },
79
+ required: ["node_id"],
80
+ },
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 });
89
+ `);
90
+ return sendCommand(script, bridgeOptions);
91
+ },
92
+ },
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 })
103
+ Example: move_clip({ node_id: "abc123", new_start_seconds: 10.0, new_track_index: 1 })`,
104
+ parameters: {
105
+ type: "object",
106
+ properties: {
107
+ node_id: {
108
+ type: "string",
109
+ description: "Node ID of the clip to move",
110
+ },
111
+ new_start_seconds: {
112
+ type: "number",
113
+ description: "New start time in seconds",
114
+ },
115
+ new_track_index: {
116
+ type: "number",
117
+ description: "Optional new track index to move the clip to",
118
+ },
119
+ },
120
+ required: ["node_id", "new_start_seconds"],
121
+ },
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
+ });
154
+ `);
155
+ return sendCommand(script, bridgeOptions);
156
+ },
157
+ },
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
169
+ Example: trim_clip({ node_id: "abc123", new_in_seconds: 1.0, new_out_seconds: 59.0 }) — trim both ends`,
170
+ parameters: {
171
+ type: "object",
172
+ properties: {
173
+ node_id: {
174
+ type: "string",
175
+ description: "Node ID of the clip to trim",
176
+ },
177
+ new_in_seconds: {
178
+ type: "number",
179
+ description: "New in-point in seconds (relative to clip's source media)",
180
+ },
181
+ new_out_seconds: {
182
+ type: "number",
183
+ description: "New out-point in seconds (relative to clip's source media)",
184
+ },
185
+ },
186
+ required: ["node_id"],
187
+ },
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
+ });
226
+ `);
227
+ return sendCommand(script, bridgeOptions);
228
+ },
229
+ },
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
238
+ Example: split_clip({ time_seconds: 15.5, track_index: 1, track_type: "video" })`,
239
+ parameters: {
240
+ type: "object",
241
+ properties: {
242
+ time_seconds: {
243
+ type: "number",
244
+ description: "Time position in seconds where to split",
245
+ },
246
+ track_index: {
247
+ type: "number",
248
+ description: "Track index (0-based)",
249
+ },
250
+ track_type: {
251
+ type: "string",
252
+ enum: ["video", "audio"],
253
+ description: "Track type (default: video)",
254
+ },
255
+ },
256
+ required: ["time_seconds"],
257
+ },
258
+ handler: async (args) => {
259
+ const trackType = args.track_type || "video";
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}" });
273
+ `);
274
+ return sendCommand(script, bridgeOptions);
275
+ },
276
+ },
277
+ duplicate_clip: {
278
+ description: "Duplicate a clip on the timeline (copy to same position on next available track)",
279
+ parameters: {
280
+ type: "object",
281
+ properties: {
282
+ node_id: {
283
+ type: "string",
284
+ description: "Node ID of the clip to duplicate",
285
+ },
286
+ },
287
+ required: ["node_id"],
288
+ },
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 });
306
+ `);
307
+ return sendCommand(script, bridgeOptions);
308
+ },
309
+ },
310
+ enable_disable_clip: {
311
+ description: "Enable or disable a clip on the timeline",
312
+ parameters: {
313
+ type: "object",
314
+ properties: {
315
+ node_id: {
316
+ type: "string",
317
+ description: "Node ID of the clip",
318
+ },
319
+ enabled: {
320
+ type: "boolean",
321
+ description: "Set to true to enable, false to disable",
322
+ },
323
+ },
324
+ required: ["node_id", "enabled"],
325
+ },
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} });
338
+ `);
339
+ return sendCommand(script, bridgeOptions);
340
+ },
341
+ },
342
+ set_clip_properties: {
343
+ description: "Set properties on a clip (opacity, speed, etc.)",
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ node_id: {
348
+ type: "string",
349
+ description: "Node ID of the clip",
350
+ },
351
+ opacity: {
352
+ type: "number",
353
+ description: "Opacity value (0-100)",
354
+ },
355
+ speed: {
356
+ type: "number",
357
+ description: "Playback speed multiplier (1.0 = normal, 2.0 = double speed)",
358
+ },
359
+ scale: {
360
+ type: "number",
361
+ description: "Scale percentage (100 = original size)",
362
+ },
363
+ position_x: {
364
+ type: "number",
365
+ description: "Horizontal position",
366
+ },
367
+ position_y: {
368
+ type: "number",
369
+ description: "Vertical position",
370
+ },
371
+ rotation: {
372
+ type: "number",
373
+ description: "Rotation in degrees",
374
+ },
375
+ },
376
+ required: ["node_id"],
377
+ },
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 });
441
+ `);
442
+ return sendCommand(script, bridgeOptions);
443
+ },
444
+ },
445
+ replace_clip: {
446
+ description: "Replace a clip on the timeline with a different project item, preserving position and duration",
447
+ parameters: {
448
+ type: "object",
449
+ properties: {
450
+ node_id: {
451
+ type: "string",
452
+ description: "Node ID of the clip to replace",
453
+ },
454
+ new_item_id: {
455
+ type: "string",
456
+ description: "Node ID or name of the new project item to replace with",
457
+ },
458
+ },
459
+ required: ["node_id", "new_item_id"],
460
+ },
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
+ });
495
+ `);
496
+ return sendCommand(script, bridgeOptions);
497
+ },
498
+ },
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
515
+ Example: speed_change({ node_id: "abc123", speed_percent: 50, reverse: true }) — half speed, reversed`,
516
+ parameters: {
517
+ type: "object",
518
+ properties: {
519
+ node_id: {
520
+ type: "string",
521
+ description: "Node ID of the clip",
522
+ },
523
+ speed_percent: {
524
+ type: "number",
525
+ description: "Speed as percentage (100 = normal, 200 = double, 50 = half)",
526
+ },
527
+ reverse: {
528
+ type: "boolean",
529
+ description: "Reverse playback direction (default: false)",
530
+ },
531
+ },
532
+ required: ["node_id", "speed_percent"],
533
+ },
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 });
569
+ `);
570
+ return sendCommand(script, bridgeOptions);
571
+ },
572
+ },
573
+ };
574
+ }
575
+ //# sourceMappingURL=timeline.js.map