@synthetic-ai/premiere-mcp 1.1.8

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 (106) hide show
  1. package/CHANGELOG.md +100 -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 +92 -0
  6. package/cep-plugin/CSXS/manifest.xml +49 -0
  7. package/cep-plugin/host.jsx +7 -0
  8. package/cep-plugin/index.html +472 -0
  9. package/cep-plugin/main.js +683 -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/license.d.ts +22 -0
  21. package/dist/license.js +89 -0
  22. package/dist/resources/extendscript-reference.d.ts +2 -0
  23. package/dist/resources/extendscript-reference.js +219 -0
  24. package/dist/server.d.ts +4 -0
  25. package/dist/server.js +282 -0
  26. package/dist/tools/advanced.d.ts +530 -0
  27. package/dist/tools/advanced.js +923 -0
  28. package/dist/tools/audio-advanced.d.ts +847 -0
  29. package/dist/tools/audio-advanced.js +1653 -0
  30. package/dist/tools/audio.d.ts +84 -0
  31. package/dist/tools/audio.js +123 -0
  32. package/dist/tools/captions.d.ts +30 -0
  33. package/dist/tools/captions.js +73 -0
  34. package/dist/tools/clipboard.d.ts +153 -0
  35. package/dist/tools/clipboard.js +381 -0
  36. package/dist/tools/color-advanced.d.ts +821 -0
  37. package/dist/tools/color-advanced.js +772 -0
  38. package/dist/tools/discovery.d.ts +128 -0
  39. package/dist/tools/discovery.js +445 -0
  40. package/dist/tools/effects.d.ts +211 -0
  41. package/dist/tools/effects.js +554 -0
  42. package/dist/tools/export-presets.d.ts +333 -0
  43. package/dist/tools/export-presets.js +490 -0
  44. package/dist/tools/export.d.ts +320 -0
  45. package/dist/tools/export.js +618 -0
  46. package/dist/tools/health.d.ts +9 -0
  47. package/dist/tools/health.js +25 -0
  48. package/dist/tools/inspection.d.ts +179 -0
  49. package/dist/tools/inspection.js +1089 -0
  50. package/dist/tools/keyframes.d.ts +253 -0
  51. package/dist/tools/keyframes.js +611 -0
  52. package/dist/tools/markers.d.ts +117 -0
  53. package/dist/tools/markers.js +206 -0
  54. package/dist/tools/media.d.ts +297 -0
  55. package/dist/tools/media.js +493 -0
  56. package/dist/tools/metadata.d.ts +178 -0
  57. package/dist/tools/metadata.js +278 -0
  58. package/dist/tools/multicam.d.ts +199 -0
  59. package/dist/tools/multicam.js +212 -0
  60. package/dist/tools/playback.d.ts +34 -0
  61. package/dist/tools/playback.js +63 -0
  62. package/dist/tools/playhead.d.ts +77 -0
  63. package/dist/tools/playhead.js +185 -0
  64. package/dist/tools/project-manager.d.ts +65 -0
  65. package/dist/tools/project-manager.js +81 -0
  66. package/dist/tools/project.d.ts +390 -0
  67. package/dist/tools/project.js +596 -0
  68. package/dist/tools/scripting.d.ts +98 -0
  69. package/dist/tools/scripting.js +434 -0
  70. package/dist/tools/selection.d.ts +120 -0
  71. package/dist/tools/selection.js +334 -0
  72. package/dist/tools/sequence-advanced.d.ts +84 -0
  73. package/dist/tools/sequence-advanced.js +476 -0
  74. package/dist/tools/sequence-settings.d.ts +397 -0
  75. package/dist/tools/sequence-settings.js +975 -0
  76. package/dist/tools/sequence.d.ts +211 -0
  77. package/dist/tools/sequence.js +397 -0
  78. package/dist/tools/source-monitor.d.ts +95 -0
  79. package/dist/tools/source-monitor.js +180 -0
  80. package/dist/tools/text.d.ts +97 -0
  81. package/dist/tools/text.js +159 -0
  82. package/dist/tools/timeline.d.ts +265 -0
  83. package/dist/tools/timeline.js +575 -0
  84. package/dist/tools/track-management.d.ts +482 -0
  85. package/dist/tools/track-management.js +787 -0
  86. package/dist/tools/track-targeting.d.ts +666 -0
  87. package/dist/tools/track-targeting.js +1318 -0
  88. package/dist/tools/tracks.d.ts +90 -0
  89. package/dist/tools/tracks.js +183 -0
  90. package/dist/tools/transitions-motion.d.ts +788 -0
  91. package/dist/tools/transitions-motion.js +680 -0
  92. package/dist/tools/transitions.d.ts +124 -0
  93. package/dist/tools/transitions.js +267 -0
  94. package/dist/tools/utility.d.ts +469 -0
  95. package/dist/tools/utility.js +1118 -0
  96. package/dist/tools/workspace.d.ts +25 -0
  97. package/dist/tools/workspace.js +61 -0
  98. package/package.json +66 -0
  99. package/scripts/build-chat-dmg.sh +210 -0
  100. package/scripts/build-chat-zip.sh +291 -0
  101. package/scripts/install-cep-windows.ps1 +99 -0
  102. package/scripts/install-cep.sh +66 -0
  103. package/scripts/install-chat-plugin.bat +67 -0
  104. package/scripts/install-chat-plugin.sh +85 -0
  105. package/scripts/install-windows.bat +95 -0
  106. package/scripts/package.mjs +254 -0
@@ -0,0 +1,683 @@
1
+ /* MCP Bridge - CEP Plugin Main Script
2
+ * Polls a temp directory for command files (.jsx), executes them
3
+ * in Premiere Pro's ExtendScript engine, and writes results back.
4
+ *
5
+ * Improvements over the original:
6
+ * - isRunning flag guards the poll loop so concurrent executions can't pile up.
7
+ * - executeScript wraps cs.evalScript with a 25-second timeout to prevent hangs.
8
+ * - Status indicator updates on every state change (idle / running / error).
9
+ * - Reconnect logic: if Premiere Pro is unresponsive, the bridge backs off and retries. */
10
+
11
+ var cs = new CSInterface();
12
+ var bridgeRunning = false;
13
+ var isRunning = false; // true while a script is actively executing (prevents concurrent runs)
14
+ var pollInterval = null;
15
+ var commandCount = 0;
16
+ var errorCount = 0;
17
+ var consecutiveErrors = 0;
18
+ var tempDir = "";
19
+ var POLL_MS = 200;
20
+ var EXEC_TIMEOUT_MS = 25000; // 25-second hard timeout for ExtendScript execution
21
+ var PANEL_VERSION = "1.1.6"; // stamped by scripts/package.mjs on every build — see step 4b2
22
+
23
+ // ── Auto-update config ────────────────────────────────────────────────────
24
+ // The panel checks this URL at startup and offers in-app updates when a
25
+ // newer version is published. Files come back through /api/v1/panel/file
26
+ // gated by the user's license key. Self-installs only the hot-swappable
27
+ // JS/JSX/HTML files — manifest changes still need a fresh .zxp.
28
+ //
29
+ // Ported from Synthetic AE MCP's cep-plugin/main.js (checkForUpdates /
30
+ // showUpdateBanner / applyUpdate). One deliberate difference: AE MCP's Node
31
+ // server SPAWNS AfterFX.exe, so AE_MCP_LICENSE (set in the MCP client config)
32
+ // is inherited by the child process and read via $.getenv() inside
33
+ // ExtendScript. Premiere MCP's Node server does NOT spawn Premiere Pro — it
34
+ // talks to the already-running CEP panel as an HTTP client — so there is no
35
+ // env-var-inheritance path. Instead the license key is pasted into this
36
+ // panel's own Settings field and persisted to a local license.json (see
37
+ // readLicenseKeyLocal / saveLicenseKey below).
38
+ var UPDATE_CHECK_URL = "https://synthetic.com.ar/api/v1/panel/version";
39
+ var UPDATE_FILE_URL = "https://synthetic.com.ar/api/v1/panel/file";
40
+ var UPDATE_PRODUCT = "premiere-mcp";
41
+ var pendingUpdate = null;
42
+ var lastDismissedVersion = null;
43
+
44
+ // ---- Logging ----
45
+ function log(msg, cls) {
46
+ var el = document.getElementById("log");
47
+ var entry = document.createElement("div");
48
+ entry.className = "log-entry " + (cls || "");
49
+ var ts = new Date().toLocaleTimeString();
50
+ entry.textContent = "[" + ts + "] " + msg;
51
+ el.appendChild(entry);
52
+ el.scrollTop = el.scrollHeight;
53
+ // Keep max 100 entries
54
+ while (el.children.length > 100) el.removeChild(el.firstChild);
55
+ }
56
+
57
+ // ---- Status ----
58
+ function setStatus(state, text) {
59
+ var dot = document.getElementById("statusDot");
60
+ dot.className = "status-dot " + state;
61
+ document.getElementById("statusText").textContent = text;
62
+
63
+ // Mirror the same state onto the small dot inside the product block, so the
64
+ // "is the panel OK?" cue is visible up-top without scanning the status row.
65
+ // Map: connected -> ok, warning -> warn, error -> err, waiting/* -> grey.
66
+ var ps = document.getElementById("productStatus");
67
+ if (ps) {
68
+ ps.className = "product-status" +
69
+ (state === "connected" ? " ok" :
70
+ state === "warning" ? " warn" :
71
+ state === "error" ? " err" : "");
72
+ ps.title = text || "Panel ready";
73
+ }
74
+ }
75
+
76
+ // ---- File I/O via Node.js (CEP has access to Node) ----
77
+ var fs = require("fs");
78
+ var path = require("path");
79
+ var os = require("os");
80
+ tempDir = path.join(os.tmpdir(), "premiere-mcp-bridge");
81
+
82
+ function ensureDir(dir) {
83
+ try {
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
86
+ }
87
+ } catch (e) {
88
+ log("Error creating dir: " + e.message, "err");
89
+ }
90
+ }
91
+
92
+ function listCommandFiles() {
93
+ try {
94
+ if (!fs.existsSync(tempDir)) return [];
95
+ var files = fs.readdirSync(tempDir);
96
+ return files
97
+ .filter(function (f) { return f.indexOf("cmd_") === 0 && f.indexOf(".jsx") > 0; })
98
+ .sort(); // process in order
99
+ } catch (e) {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ function readFile(filePath) {
105
+ try {
106
+ return fs.readFileSync(filePath, "utf-8");
107
+ } catch (e) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function writeFile(filePath, content) {
113
+ try {
114
+ // Atomic write: write to a temp file the MCP server's poller ignores
115
+ // (it waits for the exact res_<id>.json name), then rename into place.
116
+ // Prevents the server from reading a half-written response file.
117
+ var tmpPath = filePath + ".part";
118
+ fs.writeFileSync(tmpPath, content, "utf-8");
119
+ fs.renameSync(tmpPath, filePath);
120
+ return true;
121
+ } catch (e) {
122
+ log("Error writing " + filePath + ": " + e.message, "err");
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function deleteFile(filePath) {
128
+ try {
129
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
130
+ } catch (e) {}
131
+ }
132
+
133
+ // cs.getSystemPath() returns a file:// URI, not a plain OS path — observed
134
+ // on Windows as "file:\C:\Users\..." (single backslash, not the POSIX
135
+ // "file:///C:/..."). Passing that straight to fs/path calls tries to
136
+ // create/read a path actually named "file:\C:\Users\...", which fails with
137
+ // ENOENT since its own parent doesn't exist. Strip whatever "file:" prefix
138
+ // shows up and URL-decode any %xx escapes before treating it as a path.
139
+ function systemPathToFsPath(p) {
140
+ var m = /^file:[\\/]*/i.exec(p || "");
141
+ if (m) p = p.slice(m[0].length);
142
+ try { p = decodeURIComponent(p); } catch (e) {}
143
+ return p;
144
+ }
145
+
146
+ // ---- License key storage (local file, no env-var inheritance path) ------
147
+ // Premiere Pro is already running when this panel loads — the MCP server
148
+ // never spawns it — so there's no child-process env var to read the license
149
+ // key from (unlike AE MCP's AE_MCP_LICENSE). Instead the user pastes the key
150
+ // into the Settings field below and it's persisted next to the extension.
151
+ function getLicensePath() {
152
+ try {
153
+ return path.join(systemPathToFsPath(cs.getSystemPath(SystemPath.EXTENSION)), "license.json");
154
+ } catch (e) {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function readLicenseKeyLocal() {
160
+ try {
161
+ var p = getLicensePath();
162
+ if (!p || !fs.existsSync(p)) return "";
163
+ var raw = fs.readFileSync(p, "utf-8");
164
+ var parsed = JSON.parse(raw);
165
+ return (parsed && parsed.licenseKey) ? String(parsed.licenseKey).trim() : "";
166
+ } catch (e) {
167
+ return "";
168
+ }
169
+ }
170
+
171
+ function writeLicenseKeyLocal(key) {
172
+ var p = getLicensePath();
173
+ if (!p) return false;
174
+ try {
175
+ fs.writeFileSync(p, JSON.stringify({ licenseKey: key }, null, 2), "utf-8");
176
+ return true;
177
+ } catch (e) {
178
+ log("Error saving license key: " + e.message, "err");
179
+ return false;
180
+ }
181
+ }
182
+
183
+ // Settings UI: load saved key into the input + status line on panel init.
184
+ function loadLicenseUI() {
185
+ try {
186
+ var key = readLicenseKeyLocal();
187
+ var input = document.getElementById("licenseKeyInput");
188
+ if (input) input.value = key;
189
+ var statusEl = document.getElementById("licenseStatus");
190
+ if (statusEl) statusEl.textContent = key ? "License key saved." : "No license key saved yet — required for updates.";
191
+ } catch (e) {}
192
+ }
193
+
194
+ // Settings UI: Save button handler.
195
+ function saveLicenseKey() {
196
+ var input = document.getElementById("licenseKeyInput");
197
+ var key = input ? input.value.trim() : "";
198
+ var statusEl = document.getElementById("licenseStatus");
199
+ if (!key) {
200
+ if (statusEl) statusEl.textContent = "Enter a license key before saving.";
201
+ return;
202
+ }
203
+ if (writeLicenseKeyLocal(key)) {
204
+ log("License key saved.", "ok");
205
+ if (statusEl) statusEl.textContent = "License key saved.";
206
+ } else if (statusEl) {
207
+ statusEl.textContent = "Failed to save license key (see log).";
208
+ }
209
+ }
210
+
211
+ // Settings is just the license field for now — no need for AE Agent's full
212
+ // Subscription/API Keys/Data section system. Hidden by default so the key
213
+ // isn't sitting in plain view on the main panel; click the ⚙ to reveal it.
214
+ function toggleSettings() {
215
+ var panel = document.getElementById("settingsPanel");
216
+ if (!panel) return;
217
+ panel.style.display = panel.style.display === "none" ? "block" : "none";
218
+ }
219
+
220
+ // ---- Script Execution with timeout ----
221
+ function executeScript(script, callback) {
222
+ // Script is already wrapped in an IIFE by the MCP server's buildScript(),
223
+ // so we pass it directly to avoid double-wrapping.
224
+ var timedOut = false;
225
+ var settled = false;
226
+
227
+ var timeoutHandle = setTimeout(function () {
228
+ if (settled) return;
229
+ timedOut = true;
230
+ settled = true;
231
+ log("Execution timed out after " + (EXEC_TIMEOUT_MS / 1000) + "s", "err");
232
+ consecutiveErrors++;
233
+ updateStatusFromErrors();
234
+ callback(JSON.stringify({ success: false, error: "ExtendScript execution timed out after " + (EXEC_TIMEOUT_MS / 1000) + " seconds. Premiere Pro may be busy or unresponsive." }));
235
+ }, EXEC_TIMEOUT_MS);
236
+
237
+ cs.evalScript(script, function (result) {
238
+ if (settled) return; // already timed out
239
+ settled = true;
240
+ clearTimeout(timeoutHandle);
241
+ consecutiveErrors = 0; // reset on success
242
+ callback(result);
243
+ });
244
+ }
245
+
246
+ // ---- Update status dot based on error state ----
247
+ function updateStatusFromErrors() {
248
+ if (!bridgeRunning) return;
249
+ if (consecutiveErrors >= 3) {
250
+ setStatus("error", "Running — Premiere Pro unresponsive (" + consecutiveErrors + " errors)");
251
+ } else if (consecutiveErrors > 0) {
252
+ setStatus("warning", "Running — " + consecutiveErrors + " consecutive error(s)");
253
+ } else {
254
+ setStatus("connected", "Running — " + commandCount + " commands processed");
255
+ }
256
+ }
257
+
258
+ // ---- Command Processing ----
259
+ function processCommands() {
260
+ // Prevent concurrent executions: if a script is still running, skip this poll tick.
261
+ if (isRunning) return;
262
+
263
+ var cmdFiles = listCommandFiles();
264
+ if (cmdFiles.length === 0) return;
265
+
266
+ // Process one command at a time (FIFO). The next tick picks up the next file.
267
+ isRunning = true;
268
+ processOneCommand(cmdFiles[0]);
269
+ }
270
+
271
+ function processOneCommand(cmdFileName) {
272
+ var cmdFilePath = path.join(tempDir, cmdFileName);
273
+ var script = readFile(cmdFilePath);
274
+ if (!script) {
275
+ log("Failed to read: " + cmdFileName, "err");
276
+ deleteFile(cmdFilePath);
277
+ isRunning = false; // release the lock — AE MCP's sibling file already does this;
278
+ // without it a single transient read failure (e.g. a race with
279
+ // the writer) wedges the bridge forever: isRunning stays true,
280
+ // so every future processCommands() tick bails at its first
281
+ // check and the panel looks "Running" while doing nothing.
282
+ // Reproduced live 2026-09-23.
283
+ return;
284
+ }
285
+
286
+ // Derive response filename: cmd_12345.jsx -> res_12345.json
287
+ var id = cmdFileName.replace("cmd_", "").replace(".jsx", "");
288
+ var resFilePath = path.join(tempDir, "res_" + id + ".json");
289
+
290
+ log("Executing: " + cmdFileName + " (" + script.length + " chars)", "cmd");
291
+
292
+ // Delete the command file immediately to avoid re-processing
293
+ deleteFile(cmdFilePath);
294
+
295
+ executeScript(script, function (result) {
296
+ isRunning = false; // release the lock so the next command can be processed
297
+ commandCount++;
298
+ document.getElementById("cmdCount").textContent = commandCount;
299
+
300
+ var response;
301
+ try {
302
+ // ExtendScript returns a string; try to parse it as JSON
303
+ if (result && result !== "undefined" && result !== "null") {
304
+ // Check if it's already valid JSON
305
+ var parsed = JSON.parse(result);
306
+ response = JSON.stringify(parsed);
307
+ // If the parsed result contains an error field, count it
308
+ if (parsed && parsed.success === false) {
309
+ errorCount++;
310
+ consecutiveErrors++;
311
+ log("Result: Error — " + (parsed.error || "unknown"), "err");
312
+ } else {
313
+ consecutiveErrors = 0;
314
+ log("Result: OK", "ok");
315
+ }
316
+ } else {
317
+ response = JSON.stringify({ success: true, data: result || null });
318
+ consecutiveErrors = 0;
319
+ log("Result: OK (null/undefined)", "ok");
320
+ }
321
+ } catch (e) {
322
+ // If result isn't JSON, wrap it
323
+ if (result && result.indexOf("Error") === 0) {
324
+ response = JSON.stringify({ success: false, error: result });
325
+ errorCount++;
326
+ consecutiveErrors++;
327
+ log("Result: " + result, "err");
328
+ } else {
329
+ response = JSON.stringify({ success: true, data: result });
330
+ consecutiveErrors = 0;
331
+ log("Result: OK (raw)", "ok");
332
+ }
333
+ }
334
+
335
+ updateStatusFromErrors();
336
+ writeFile(resFilePath, response);
337
+ });
338
+ }
339
+
340
+ // ---- Auto-update --------------------------------------------------------
341
+ // Ported verbatim (logic-wise) from Synthetic AE MCP's cep-plugin/main.js.
342
+ // Compare two semver strings (x.y.z). Returns 1 if a > b, -1 if a < b, 0 if =.
343
+ // Hand-rolled because ExtendScript / CEP-Node don't ship semver.
344
+ function semverCmp(a, b) {
345
+ var pa = String(a || "0.0.0").split(".").map(function (n) { return parseInt(n, 10) || 0; });
346
+ var pb = String(b || "0.0.0").split(".").map(function (n) { return parseInt(n, 10) || 0; });
347
+ for (var i = 0; i < 3; i++) {
348
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
349
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
350
+ }
351
+ return 0;
352
+ }
353
+
354
+ // One-shot https GET that buffers the response into a string. CEP ships
355
+ // the Node 'https' module already.
356
+ function httpsGetJson(url, cb) {
357
+ try {
358
+ var https = require("https");
359
+ https.get(url, function (res) {
360
+ var chunks = [];
361
+ // Follow one redirect. Our own /api/v1/panel/file endpoint 302s to R2
362
+ // (Supabase signed URLs went through 302 too), but Next's
363
+ // NextResponse.redirect() defaults to 307 — handle the whole 3xx
364
+ // range so this doesn't silently break again on a framework default.
365
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
366
+ return httpsGetJson(res.headers.location, cb);
367
+ }
368
+ res.on("data", function (c) { chunks.push(c); });
369
+ res.on("end", function () {
370
+ var body = Buffer.concat(chunks).toString("utf-8");
371
+ if (res.statusCode !== 200) {
372
+ return cb(new Error("HTTP " + res.statusCode + ": " + body.slice(0, 200)));
373
+ }
374
+ try { cb(null, JSON.parse(body)); }
375
+ catch (e) { cb(new Error("Bad JSON: " + e.message)); }
376
+ });
377
+ }).on("error", function (err) { cb(err); });
378
+ } catch (e) { cb(e); }
379
+ }
380
+
381
+ // Download a single file as a Buffer. Follows one redirect.
382
+ function httpsDownload(url, cb) {
383
+ try {
384
+ var https = require("https");
385
+ https.get(url, function (res) {
386
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
387
+ return httpsDownload(res.headers.location, cb);
388
+ }
389
+ if (res.statusCode !== 200) {
390
+ return cb(new Error("HTTP " + res.statusCode));
391
+ }
392
+ var chunks = [];
393
+ res.on("data", function (c) { chunks.push(c); });
394
+ res.on("end", function () { cb(null, Buffer.concat(chunks)); });
395
+ }).on("error", function (err) { cb(err); });
396
+ } catch (e) { cb(e); }
397
+ }
398
+
399
+ // SHA256 of a Buffer using Node's crypto. Used to verify integrity of
400
+ // downloaded files before overwriting the panel's own code.
401
+ function sha256(buf) {
402
+ var crypto = require("crypto");
403
+ return crypto.createHash("sha256").update(buf).digest("hex");
404
+ }
405
+
406
+ // On panel startup. Fetches the manifest, compares versions, surfaces a
407
+ // banner if newer. Never throws — if anything goes wrong we silently skip
408
+ // the check so the panel still starts cleanly even offline.
409
+ function checkForUpdates() {
410
+ try {
411
+ var url = UPDATE_CHECK_URL + "?product=" + encodeURIComponent(UPDATE_PRODUCT) +
412
+ "&from=" + encodeURIComponent(PANEL_VERSION);
413
+ httpsGetJson(url, function (err, manifest) {
414
+ if (err || !manifest || !manifest.version) return;
415
+ if (semverCmp(manifest.version, PANEL_VERSION) <= 0) return;
416
+ if (lastDismissedVersion === manifest.version) return;
417
+ pendingUpdate = manifest;
418
+ showUpdateBanner(manifest);
419
+ });
420
+ } catch (_) {}
421
+ }
422
+
423
+ function showUpdateBanner(m) {
424
+ var banner = document.getElementById("updateBanner");
425
+ if (!banner) return;
426
+ document.getElementById("updateVersionTag").textContent = "v" + PANEL_VERSION + " -> v" + m.version;
427
+ var notes = Array.isArray(m.releaseNotes) ? m.releaseNotes.slice(0, 3).join("\n") : "";
428
+ document.getElementById("updateNotes").textContent = notes;
429
+ banner.style.display = "block";
430
+ }
431
+
432
+ function dismissUpdate() {
433
+ if (pendingUpdate) lastDismissedVersion = pendingUpdate.version;
434
+ pendingUpdate = null;
435
+ var banner = document.getElementById("updateBanner");
436
+ if (banner) banner.style.display = "none";
437
+ }
438
+
439
+ function openInstaller() {
440
+ try {
441
+ if (window.cep && cep.util && cep.util.openURLInDefaultBrowser) {
442
+ cep.util.openURLInDefaultBrowser("https://synthetic.com.ar/dashboard/downloads");
443
+ }
444
+ } catch (_) {}
445
+ }
446
+
447
+ // Apply the update: for each file in the manifest, download via the
448
+ // license-gated endpoint, verify SHA256, back up the current file to
449
+ // .bak, write the new one. If ANY file fails verification, roll back all
450
+ // of them — never leave the panel in a half-updated state.
451
+ function applyUpdate() {
452
+ if (!pendingUpdate || !Array.isArray(pendingUpdate.files)) {
453
+ log("No update to apply", "err");
454
+ return;
455
+ }
456
+ var btn = document.getElementById("btnUpdateNow");
457
+ if (btn) { btn.disabled = true; btn.textContent = "Updating..."; }
458
+
459
+ var licenseKey = readLicenseKeyLocal();
460
+ if (!licenseKey) {
461
+ log("Update failed: no license key saved. Open Settings and paste your Premiere MCP license key.", "err");
462
+ var statusEl = document.getElementById("licenseStatus");
463
+ if (statusEl) statusEl.textContent = "License key required before updating.";
464
+ if (btn) { btn.disabled = false; btn.textContent = "Update now"; }
465
+ return;
466
+ }
467
+
468
+ // CEP can ask its own extension folder via cs.getSystemPath.
469
+ var extDir = systemPathToFsPath(cs.getSystemPath(SystemPath.EXTENSION));
470
+ var files = pendingUpdate.files;
471
+ var version = pendingUpdate.version;
472
+ var staged = []; // {targetPath, backupPath, buf}
473
+ var i = 0;
474
+
475
+ function fetchNext() {
476
+ if (i >= files.length) { commit(); return; }
477
+ var f = files[i++];
478
+ var url = UPDATE_FILE_URL +
479
+ "?product=" + encodeURIComponent(UPDATE_PRODUCT) +
480
+ "&name=" + encodeURIComponent(f.name) +
481
+ "&licenseKey=" + encodeURIComponent(licenseKey) +
482
+ "&version=" + encodeURIComponent(version);
483
+ log("Downloading " + f.name + "...");
484
+ httpsDownload(url, function (err, buf) {
485
+ if (err) return abort("Download failed for " + f.name + ": " + err.message);
486
+ // Optional SHA256 check — only enforced if the manifest provides one.
487
+ if (f.sha256 && f.sha256.length === 64) {
488
+ var got = sha256(buf);
489
+ if (got !== f.sha256) return abort("SHA256 mismatch for " + f.name + " (got " + got.slice(0, 12) + "..., expected " + f.sha256.slice(0, 12) + "...)");
490
+ }
491
+ // Optional size sanity check.
492
+ if (f.size && Math.abs(buf.length - f.size) > 1024) {
493
+ log("Size differs from manifest for " + f.name + " (" + buf.length + " vs " + f.size + ") — continuing", "");
494
+ }
495
+ var target = path.join(extDir, f.name);
496
+ // The "CSXS" subfolder gets resolved separately; everything in our
497
+ // panel lives at the extension root, so a flat join is correct.
498
+ staged.push({ target: target, backup: target + ".bak", buf: buf });
499
+ fetchNext();
500
+ });
501
+ }
502
+
503
+ function commit() {
504
+ try {
505
+ // Phase 1: backup. If any backup write fails, abort before touching real files.
506
+ for (var k = 0; k < staged.length; k++) {
507
+ var s = staged[k];
508
+ if (fs.existsSync(s.target)) {
509
+ // Best effort — rename can fail if source is locked; copy as fallback.
510
+ try { fs.copyFileSync(s.target, s.backup); }
511
+ catch (eCopy) { return abort("Backup failed for " + s.target + ": " + eCopy.message); }
512
+ }
513
+ }
514
+ // Phase 2: write new files.
515
+ for (var j = 0; j < staged.length; j++) {
516
+ var ss = staged[j];
517
+ var tmpPath = ss.target + ".new";
518
+ fs.writeFileSync(tmpPath, ss.buf);
519
+ // Replace via rename so the swap is atomic per-file.
520
+ if (fs.existsSync(ss.target)) fs.unlinkSync(ss.target);
521
+ fs.renameSync(tmpPath, ss.target);
522
+ }
523
+ log("Update v" + version + " applied. RESTART PREMIERE PRO to load the new panel.", "ok");
524
+ setStatus("warning", "Update applied — restart Premiere Pro");
525
+ var b = document.getElementById("updateBanner");
526
+ if (b) {
527
+ b.style.background = "rgba(40,180,120,0.10)";
528
+ b.style.borderColor = "#28b478";
529
+ document.getElementById("updateNotes").innerHTML =
530
+ "<strong style='color:#9af3c4'>Update applied.</strong> Close and reopen Premiere Pro to activate v" + version + ".";
531
+ var bt = document.getElementById("btnUpdateNow");
532
+ if (bt) { bt.style.display = "none"; }
533
+ var bt2 = document.getElementById("btnUpdateLater");
534
+ if (bt2) bt2.textContent = "OK";
535
+ var bt3 = document.getElementById("btnUpdateInstaller");
536
+ if (bt3) bt3.style.display = "none";
537
+ }
538
+ } catch (eCommit) {
539
+ // Rollback: restore any .bak we wrote.
540
+ for (var r = 0; r < staged.length; r++) {
541
+ try {
542
+ var rs = staged[r];
543
+ if (fs.existsSync(rs.backup)) {
544
+ if (fs.existsSync(rs.target)) fs.unlinkSync(rs.target);
545
+ fs.renameSync(rs.backup, rs.target);
546
+ }
547
+ } catch (_) {}
548
+ }
549
+ abort("Commit failed (rolled back): " + eCommit.message);
550
+ }
551
+ }
552
+
553
+ function abort(msg) {
554
+ log("Update aborted: " + msg, "err");
555
+ if (btn) { btn.disabled = false; btn.textContent = "Update now"; }
556
+ setStatus("error", "Update failed");
557
+ return;
558
+ }
559
+
560
+ fetchNext();
561
+ }
562
+
563
+ // ---- Bridge Control ----
564
+ function startBridge() {
565
+ tempDir = document.getElementById("tempDir").value.trim();
566
+ if (!tempDir) {
567
+ log("Please set a temp directory", "err");
568
+ return;
569
+ }
570
+
571
+ ensureDir(tempDir);
572
+ bridgeRunning = true;
573
+ setStatus("connected", "Running — polling " + tempDir);
574
+ log("Bridge started. Temp dir: " + tempDir);
575
+
576
+ document.getElementById("btnStart").disabled = true;
577
+ document.getElementById("btnStop").disabled = false;
578
+
579
+ // Verify Premiere Pro connection
580
+ cs.evalScript("app.version", function (version) {
581
+ if (version && version !== "undefined") {
582
+ log("Premiere Pro: " + version, "ok");
583
+ } else {
584
+ log("Warning: Could not detect Premiere Pro version", "err");
585
+ }
586
+ });
587
+
588
+ pollInterval = setInterval(function () {
589
+ if (bridgeRunning) processCommands();
590
+ }, POLL_MS);
591
+ }
592
+
593
+ function stopBridge() {
594
+ bridgeRunning = false;
595
+ if (pollInterval) clearInterval(pollInterval);
596
+ pollInterval = null;
597
+
598
+ setStatus("", "Stopped");
599
+ log("Bridge stopped");
600
+
601
+ document.getElementById("btnStart").disabled = false;
602
+ document.getElementById("btnStop").disabled = true;
603
+ }
604
+
605
+ function saveTempDir() {
606
+ tempDir = document.getElementById("tempDir").value.trim();
607
+ log("Temp directory saved: " + tempDir);
608
+ // Persist via localStorage
609
+ try {
610
+ localStorage.setItem("mcp_bridge_temp_dir", tempDir);
611
+ } catch (e) {}
612
+ }
613
+
614
+ // ---- Init ----
615
+ (function init() {
616
+ // ORDERING RULE (ported from AE MCP): schedule the auto-update check
617
+ // FIRST, before any statement that could throw, so a future regression
618
+ // elsewhere in init() can't strand users on a buggy version with no way
619
+ // to receive the fix from the panel itself.
620
+ try {
621
+ setTimeout(checkForUpdates, 500);
622
+ } catch (e) {
623
+ try { console.error("[panel init] scheduling auto-update failed:", e); } catch (_) {}
624
+ }
625
+
626
+ // Set the default temp dir in the input field
627
+ document.getElementById("tempDir").value = tempDir;
628
+
629
+ // Populate the Settings license field from local storage.
630
+ try { loadLicenseUI(); } catch (e) {}
631
+
632
+ // Load premiere.jsx library (extended function library from AdobePremiereProMCP).
633
+ // Same file:// URI cleanup as getLicensePath()/applyUpdate() — wasn't the
634
+ // cause of the "EvalScript error." bug (that was premiere.jsx's own
635
+ // obfuscation, see scripts/package.mjs), but a raw file:// URI here is
636
+ // still wrong on principle, so clean it the same way everywhere.
637
+ var premiereJsxPath = systemPathToFsPath(cs.getSystemPath(SystemPath.EXTENSION)).replace(/\\/g, "/");
638
+ cs.evalScript('$.evalFile("' + premiereJsxPath + '/premiere.jsx")', function(result) {
639
+ log("premiere.jsx loaded: " + (result === "true" || result === "undefined" ? "OK" : result));
640
+ });
641
+
642
+ // Restore saved temp dir
643
+ try {
644
+ var saved = localStorage.getItem("mcp_bridge_temp_dir");
645
+ if (saved) {
646
+ tempDir = saved;
647
+ document.getElementById("tempDir").value = tempDir;
648
+ }
649
+ } catch (e) {}
650
+
651
+ // Wire any element with [data-href] to open the URL in the user's default
652
+ // browser (CEP intercepts <a href> navigation; we have to bridge through
653
+ // cep.util.openURLInDefaultBrowser to escape the WebView).
654
+ function openExternal(url) {
655
+ try {
656
+ if (window.cep && cep.util && cep.util.openURLInDefaultBrowser) {
657
+ cep.util.openURLInDefaultBrowser(url);
658
+ } else if (window.__adobe_cep__ && window.__adobe_cep__.openURLInDefaultBrowser) {
659
+ window.__adobe_cep__.openURLInDefaultBrowser(url);
660
+ } else {
661
+ window.open(url, "_blank");
662
+ }
663
+ } catch (e) { /* swallow — non-fatal */ }
664
+ }
665
+ var links = document.querySelectorAll("[data-href]");
666
+ for (var i = 0; i < links.length; i++) {
667
+ (function (el) {
668
+ el.addEventListener("click", function (ev) {
669
+ ev.preventDefault();
670
+ openExternal(el.getAttribute("data-href"));
671
+ });
672
+ })(links[i]);
673
+ }
674
+
675
+ log("Synthetic Premiere MCP loaded");
676
+ setStatus("waiting", "Ready — click Start Bridge");
677
+
678
+ // Auto-start if temp dir exists
679
+ if (fs.existsSync(tempDir)) {
680
+ log("Temp directory exists, auto-starting...");
681
+ setTimeout(startBridge, 500);
682
+ }
683
+ })();