brainferno-mcp-bridge 0.1.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 (111) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +27 -0
  4. package/README.md +410 -0
  5. package/THIRD_PARTY_NOTICES.md +52 -0
  6. package/dist/bridge/handshake.d.ts +34 -0
  7. package/dist/bridge/handshake.js +36 -0
  8. package/dist/bridge/handshake.js.map +1 -0
  9. package/dist/bridge/script-escape.d.ts +16 -0
  10. package/dist/bridge/script-escape.js +47 -0
  11. package/dist/bridge/script-escape.js.map +1 -0
  12. package/dist/bridge/socket.d.ts +79 -0
  13. package/dist/bridge/socket.js +424 -0
  14. package/dist/bridge/socket.js.map +1 -0
  15. package/dist/bridge/types.d.ts +49 -0
  16. package/dist/bridge/types.js +38 -0
  17. package/dist/bridge/types.js.map +1 -0
  18. package/dist/config.d.ts +87 -0
  19. package/dist/config.js +172 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/drivers/ame-webservice.d.ts +118 -0
  22. package/dist/drivers/ame-webservice.js +356 -0
  23. package/dist/drivers/ame-webservice.js.map +1 -0
  24. package/dist/drivers/illustrator-delegate.d.ts +71 -0
  25. package/dist/drivers/illustrator-delegate.js +120 -0
  26. package/dist/drivers/illustrator-delegate.js.map +1 -0
  27. package/dist/drivers/osscript.d.ts +60 -0
  28. package/dist/drivers/osscript.js +230 -0
  29. package/dist/drivers/osscript.js.map +1 -0
  30. package/dist/http.d.ts +26 -0
  31. package/dist/http.js +126 -0
  32. package/dist/http.js.map +1 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +42 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/install/cli.d.ts +2 -0
  37. package/dist/install/cli.js +310 -0
  38. package/dist/install/cli.js.map +1 -0
  39. package/dist/install/lib.d.ts +76 -0
  40. package/dist/install/lib.js +194 -0
  41. package/dist/install/lib.js.map +1 -0
  42. package/dist/jobs.d.ts +117 -0
  43. package/dist/jobs.js +196 -0
  44. package/dist/jobs.js.map +1 -0
  45. package/dist/logging.d.ts +8 -0
  46. package/dist/logging.js +33 -0
  47. package/dist/logging.js.map +1 -0
  48. package/dist/server.d.ts +38 -0
  49. package/dist/server.js +90 -0
  50. package/dist/server.js.map +1 -0
  51. package/dist/tools/after-effects.d.ts +100 -0
  52. package/dist/tools/after-effects.js +695 -0
  53. package/dist/tools/after-effects.js.map +1 -0
  54. package/dist/tools/audio.d.ts +118 -0
  55. package/dist/tools/audio.js +356 -0
  56. package/dist/tools/audio.js.map +1 -0
  57. package/dist/tools/audition.d.ts +15 -0
  58. package/dist/tools/audition.js +329 -0
  59. package/dist/tools/audition.js.map +1 -0
  60. package/dist/tools/diagnostics.d.ts +17 -0
  61. package/dist/tools/diagnostics.js +61 -0
  62. package/dist/tools/diagnostics.js.map +1 -0
  63. package/dist/tools/illustrator-delegate.d.ts +9 -0
  64. package/dist/tools/illustrator-delegate.js +61 -0
  65. package/dist/tools/illustrator-delegate.js.map +1 -0
  66. package/dist/tools/illustrator.d.ts +36 -0
  67. package/dist/tools/illustrator.js +299 -0
  68. package/dist/tools/illustrator.js.map +1 -0
  69. package/dist/tools/jobs.d.ts +39 -0
  70. package/dist/tools/jobs.js +83 -0
  71. package/dist/tools/jobs.js.map +1 -0
  72. package/dist/tools/media-encoder.d.ts +17 -0
  73. package/dist/tools/media-encoder.js +180 -0
  74. package/dist/tools/media-encoder.js.map +1 -0
  75. package/dist/tools/photoshop.d.ts +17 -0
  76. package/dist/tools/photoshop.js +202 -0
  77. package/dist/tools/photoshop.js.map +1 -0
  78. package/dist/tools/pipelines.d.ts +23 -0
  79. package/dist/tools/pipelines.js +254 -0
  80. package/dist/tools/pipelines.js.map +1 -0
  81. package/dist/tools/premiere.d.ts +26 -0
  82. package/dist/tools/premiere.js +396 -0
  83. package/dist/tools/premiere.js.map +1 -0
  84. package/dist/tools/result.d.ts +17 -0
  85. package/dist/tools/result.js +51 -0
  86. package/dist/tools/result.js.map +1 -0
  87. package/package.json +64 -0
  88. package/panels/panel-cep/CSXS/manifest.xml +53 -0
  89. package/panels/panel-cep/bridge-client.js +204 -0
  90. package/panels/panel-cep/host.jsx +62 -0
  91. package/panels/panel-cep/icons/logo.svg +61 -0
  92. package/panels/panel-cep/index.html +48 -0
  93. package/panels/panel-cep/main.js +173 -0
  94. package/panels/panel-uxp/README.md +36 -0
  95. package/panels/panel-uxp/bridge-client.js +204 -0
  96. package/panels/panel-uxp/commands.js +375 -0
  97. package/panels/panel-uxp/icons/icon.png +0 -0
  98. package/panels/panel-uxp/icons/icon@2x.png +0 -0
  99. package/panels/panel-uxp/icons/logo.svg +61 -0
  100. package/panels/panel-uxp/index.html +47 -0
  101. package/panels/panel-uxp/main.js +109 -0
  102. package/panels/panel-uxp/manifest.json +89 -0
  103. package/panels/panel-uxp-ppro/README.md +11 -0
  104. package/panels/panel-uxp-ppro/bridge-client.js +204 -0
  105. package/panels/panel-uxp-ppro/commands.js +797 -0
  106. package/panels/panel-uxp-ppro/icons/icon.png +0 -0
  107. package/panels/panel-uxp-ppro/icons/icon@2x.png +0 -0
  108. package/panels/panel-uxp-ppro/icons/logo.svg +61 -0
  109. package/panels/panel-uxp-ppro/index.html +64 -0
  110. package/panels/panel-uxp-ppro/main.js +113 -0
  111. package/panels/panel-uxp-ppro/manifest.json +65 -0
@@ -0,0 +1,695 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { z } from "zod";
7
+ import { jsStringLiteral } from "../bridge/script-escape.js";
8
+ import { log } from "../logging.js";
9
+ import { runOrQueue } from "./jobs.js";
10
+ import { guard, imageResult, jsonResult } from "./result.js";
11
+ /**
12
+ * After Effects is ExtendScript-only (ES3): the scripts below deliberately use
13
+ * `var`, avoid arrow functions, and build plain objects for the panel to
14
+ * serialize (host.jsx's __acmEval turns the value into JSON). Do not
15
+ * modernize them. Every mutation is wrapped in an undo group so one tool call
16
+ * is one Ctrl-Z for the operator.
17
+ *
18
+ * Renders: `ae_render_frame` uses CompItem.saveFrameToPng on a temporary
19
+ * downscaled comp; `ae_render_comp` saves the project and spawns aerender
20
+ * (headless, never freezes the UI). Layers are addressed by comp id + layer
21
+ * index (1-based, top of the timeline = 1), as After Effects does.
22
+ */
23
+ const lit = jsStringLiteral;
24
+ const num = (n) => String(n);
25
+ const opt = (v) => (v === undefined || v === null ? "null" : lit(v));
26
+ const optNum = (v) => (v === undefined || v === null ? "null" : num(v));
27
+ const rgb = (hex) => {
28
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
29
+ return `[${parseInt(m[1], 16) / 255}, ${parseInt(m[2], 16) / 255}, ${parseInt(m[3], 16) / 255}]`;
30
+ };
31
+ /** ES3 helpers prepended inside every script's IIFE. */
32
+ const HELPERS = `
33
+ function __comp(id) {
34
+ var item = app.project.itemByID(id);
35
+ if (!item || !(item instanceof CompItem)) { throw new Error("No composition with id " + id + ". Call ae_list_compositions."); }
36
+ return item;
37
+ }
38
+ function __layer(comp, index) {
39
+ if (index < 1 || index > comp.numLayers) { throw new Error("Layer index " + index + " is out of range (1.." + comp.numLayers + ")."); }
40
+ return comp.layer(index);
41
+ }
42
+ function __undo(name, fn) {
43
+ app.beginUndoGroup(name);
44
+ try { return fn(); } finally { app.endUndoGroup(); }
45
+ }
46
+ var __TRANSFORM = { position: "ADBE Position", scale: "ADBE Scale", rotation: "ADBE Rotate Z", opacity: "ADBE Opacity", anchorPoint: "ADBE Anchor Point" };
47
+ function __prop(layer, name, path) {
48
+ var p;
49
+ if (path !== null) {
50
+ p = layer;
51
+ for (var i = 0; i < path.length; i++) { p = p.property(path[i]); if (!p) { throw new Error("Property path not found at " + path[i]); } }
52
+ return p;
53
+ }
54
+ var mn = __TRANSFORM[name];
55
+ if (!mn) { throw new Error("Unknown property " + name); }
56
+ p = layer.property("ADBE Transform Group").property(mn);
57
+ if (!p) { throw new Error("Layer has no " + name); }
58
+ return p;
59
+ }
60
+ function __layerInfo(l) {
61
+ // instanceof against the layer classes is unreliable in AE ExtendScript; matchName is not.
62
+ var mn = l.matchName, kind;
63
+ if (mn === "ADBE Text Layer") { kind = "text"; }
64
+ else if (mn === "ADBE Camera Layer") { kind = "camera"; }
65
+ else if (mn === "ADBE Light Layer") { kind = "light"; }
66
+ else if (mn === "ADBE Vector Layer") { kind = "shape"; }
67
+ else if (l.nullLayer) { kind = "null"; }
68
+ else if (l.adjustmentLayer) { kind = "adjustment"; }
69
+ else if (l.source && l.source instanceof CompItem) { kind = "precomp"; }
70
+ else if (l.source && l.source.mainSource && l.source.mainSource instanceof SolidSource) { kind = "solid"; }
71
+ else { kind = "footage"; }
72
+ return { index: l.index, name: l.name, kind: kind, enabled: l.enabled, inPoint: l.inPoint, outPoint: l.outPoint, startTime: l.startTime, parentIndex: l.parent ? l.parent.index : null, sourceId: l.source ? l.source.id : null };
73
+ }
74
+ function __centerAnchor(l) {
75
+ var r = l.sourceRectAtTime(0, false);
76
+ if (r && r.width > 0) { l.property("ADBE Transform Group").property("ADBE Anchor Point").setValue([r.left + r.width / 2, r.top + r.height / 2]); }
77
+ }
78
+ function __compInfo(c) {
79
+ return { id: c.id, name: c.name, width: c.width, height: c.height, duration: c.duration, frameRate: c.frameRate, numLayers: c.numLayers, pixelAspect: c.pixelAspect };
80
+ }
81
+ `;
82
+ const wrap = (body) => `(function () {${HELPERS}${body}
83
+ })()`;
84
+ // ---- script builders (exported for tests) -----------------------------------
85
+ export const LIST_COMPOSITIONS = wrap(`
86
+ var comps = [];
87
+ for (var i = 1; i <= app.project.numItems; i++) {
88
+ var item = app.project.item(i);
89
+ if (item instanceof CompItem) { comps.push(__compInfo(item)); }
90
+ }
91
+ return comps;`);
92
+ export const PROJECT_INFO = wrap(`
93
+ var proj = app.project;
94
+ return { path: proj.file ? proj.file.fsName : null, numItems: proj.numItems, bitsPerChannel: proj.bitsPerChannel, dirty: proj.dirty === true, version: app.version };`);
95
+ export const LIST_FOOTAGE = wrap(`
96
+ var out = [];
97
+ for (var i = 1; i <= app.project.numItems; i++) {
98
+ var item = app.project.item(i);
99
+ if (item instanceof FootageItem) {
100
+ out.push({ id: item.id, name: item.name, path: item.file ? item.file.fsName : null, width: item.width, height: item.height, duration: item.duration, hasVideo: item.hasVideo, hasAudio: item.hasAudio, isSolid: item.mainSource instanceof SolidSource });
101
+ }
102
+ }
103
+ return out;`);
104
+ export function getCompScript(compId) {
105
+ return wrap(`
106
+ var c = __comp(${num(compId)});
107
+ var layers = [];
108
+ for (var i = 1; i <= c.numLayers; i++) { layers.push(__layerInfo(c.layer(i))); }
109
+ var info = __compInfo(c); info.layers = layers; return info;`);
110
+ }
111
+ export function getLayerScript(compId, layerIndex) {
112
+ return wrap(`
113
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
114
+ var info = __layerInfo(l);
115
+ var t = l.property("ADBE Transform Group");
116
+ var names = ["position", "scale", "rotation", "opacity", "anchorPoint"];
117
+ info.transform = {};
118
+ for (var i = 0; i < names.length; i++) {
119
+ var p = t.property(__TRANSFORM[names[i]]);
120
+ if (p) { info.transform[names[i]] = { value: p.value, numKeys: p.numKeys, expression: p.expressionEnabled ? p.expression : null }; }
121
+ }
122
+ var fx = l.property("ADBE Effect Parade"); info.effects = [];
123
+ if (fx) { for (var j = 1; j <= fx.numProperties; j++) { var e = fx.property(j); info.effects.push({ index: j, name: e.name, matchName: e.matchName, enabled: e.enabled }); } }
124
+ if (l.matchName === "ADBE Text Layer") { info.text = l.property("ADBE Text Properties").property("ADBE Text Document").value.text; }
125
+ return info;`);
126
+ }
127
+ export function openProjectScript(path) {
128
+ return wrap(`
129
+ var f = new File(${lit(path)});
130
+ if (!f.exists) { throw new Error("No file at " + f.fsName); }
131
+ var proj = app.open(f);
132
+ return { path: proj.file ? proj.file.fsName : null, numItems: proj.numItems };`);
133
+ }
134
+ export function saveProjectScript(path) {
135
+ return wrap(`
136
+ var path = ${opt(path)};
137
+ if (path === null) {
138
+ if (!app.project.file) { throw new Error("This project has never been saved — pass a path ending in .aep."); }
139
+ app.project.save();
140
+ return { path: app.project.file.fsName, saved: true };
141
+ }
142
+ var f = new File(path);
143
+ app.project.save(f);
144
+ return { path: f.fsName, saved: true };`);
145
+ }
146
+ export function importFootageScript(path) {
147
+ return wrap(`
148
+ var f = new File(${lit(path)});
149
+ if (!f.exists) { throw new Error("No file at " + f.fsName); }
150
+ return __undo("Import footage", function () {
151
+ var item = app.project.importFile(new ImportOptions(f));
152
+ return { id: item.id, name: item.name, width: item.width, height: item.height, duration: item.duration, hasVideo: item.hasVideo, hasAudio: item.hasAudio };
153
+ });`);
154
+ }
155
+ export function createCompScript(p) {
156
+ return wrap(`
157
+ return __undo("Create comp", function () {
158
+ var c = app.project.items.addComp(${lit(p.name)}, ${num(p.width)}, ${num(p.height)}, ${num(p.pixelAspect)}, ${num(p.duration)}, ${num(p.frameRate)});
159
+ return __compInfo(c);
160
+ });`);
161
+ }
162
+ export function addLayerScript(p) {
163
+ const color = p.color ? rgb(p.color) : "[1, 1, 1]";
164
+ return wrap(`
165
+ var c = __comp(${num(p.compId)});
166
+ return __undo("Add layer", function () {
167
+ var kind = ${lit(p.kind)}, l;
168
+ if (kind === "footage") {
169
+ var item = app.project.itemByID(${optNum(p.itemId)});
170
+ // AVItem is not a global in AE 26.3's ExtendScript; test the concrete classes.
171
+ if (!item || !(item instanceof FootageItem || item instanceof CompItem)) { throw new Error("footage needs itemId of a footage item or comp (ae_list_footage / ae_list_compositions)."); }
172
+ l = c.layers.add(item);
173
+ } else if (kind === "solid" || kind === "adjustment") {
174
+ l = c.layers.addSolid(${color}, ${opt(p.name) === "null" ? lit(p.kind === "adjustment" ? "Adjustment" : "Solid") : lit(p.name)}, ${optNum(p.width) === "null" ? "c.width" : num(p.width)}, ${optNum(p.height) === "null" ? "c.height" : num(p.height)}, 1.0);
175
+ if (kind === "adjustment") { l.adjustmentLayer = true; }
176
+ } else if (kind === "text") {
177
+ l = c.layers.addText(${opt(p.text) === "null" ? '""' : lit(p.text)});
178
+ } else {
179
+ l = c.layers.addNull();
180
+ }
181
+ var name = ${opt(p.name)};
182
+ if (name !== null) { l.name = name; }
183
+ if (kind === "text") { __centerAnchor(l); }
184
+ return __layerInfo(l);
185
+ });`);
186
+ }
187
+ export function setLayerPropsScript(compId, layerIndex, p) {
188
+ const arr = (v) => (v === undefined ? "null" : `[${v.map(num).join(", ")}]`);
189
+ return wrap(`
190
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
191
+ return __undo("Set layer properties", function () {
192
+ var v;
193
+ v = ${opt(p.name)}; if (v !== null) { l.name = v; }
194
+ v = ${p.enabled === undefined ? "null" : String(p.enabled)}; if (v !== null) { l.enabled = v; }
195
+ v = ${optNum(p.startTime)}; if (v !== null) { l.startTime = v; }
196
+ v = ${optNum(p.inPoint)}; if (v !== null) { l.inPoint = v; }
197
+ v = ${optNum(p.outPoint)}; if (v !== null) { l.outPoint = v; }
198
+ var t = l.property("ADBE Transform Group");
199
+ v = ${arr(p.position)}; if (v !== null) { t.property("ADBE Position").setValue(v); }
200
+ v = ${arr(p.scale)}; if (v !== null) { t.property("ADBE Scale").setValue(v); }
201
+ v = ${optNum(p.rotation)}; if (v !== null) { t.property("ADBE Rotate Z").setValue(v); }
202
+ v = ${optNum(p.opacity)}; if (v !== null) { t.property("ADBE Opacity").setValue(v); }
203
+ v = ${arr(p.anchorPoint)}; if (v !== null) { t.property("ADBE Anchor Point").setValue(v); }
204
+ ${p.parentIndex === undefined ? "" : p.parentIndex === null ? "l.parent = null;" : `l.parent = __layer(c, ${num(p.parentIndex)});`}
205
+ return __layerInfo(l);
206
+ });`);
207
+ }
208
+ export function duplicateLayerScript(compId, layerIndex, name) {
209
+ return wrap(`
210
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
211
+ return __undo("Duplicate layer", function () {
212
+ var d = l.duplicate();
213
+ var name = ${opt(name)}; if (name !== null) { d.name = name; }
214
+ return __layerInfo(d);
215
+ });`);
216
+ }
217
+ export function deleteLayerScript(compId, layerIndex) {
218
+ return wrap(`
219
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
220
+ return __undo("Delete layer", function () { var name = l.name; l.remove(); return { deleted: name }; });`);
221
+ }
222
+ export function setKeyframesScript(p) {
223
+ const path = p.propertyPath ? `[${p.propertyPath.map(lit).join(", ")}]` : "null";
224
+ const keys = p.keys
225
+ .map((k) => `{ t: ${num(k.time)}, v: ${Array.isArray(k.value) ? `[${k.value.map(num).join(", ")}]` : num(k.value)}, e: ${k.easy ? "true" : "false"} }`)
226
+ .join(", ");
227
+ return wrap(`
228
+ var c = __comp(${num(p.compId)}); var l = __layer(c, ${num(p.layerIndex)});
229
+ var prop = __prop(l, ${lit(p.property)}, ${path});
230
+ var keys = [${keys}];
231
+ return __undo("Set keyframes", function () {
232
+ for (var i = 0; i < keys.length; i++) {
233
+ prop.setValueAtTime(keys[i].t, keys[i].v);
234
+ if (keys[i].e) {
235
+ var k = prop.nearestKeyIndex(keys[i].t);
236
+ // Spatial properties (position, anchor point) take ONE ease; other
237
+ // multi-dimensional properties (scale) take one per dimension.
238
+ var dims = prop.isSpatial ? 1 : (prop.value instanceof Array ? prop.value.length : 1);
239
+ var ease = []; for (var d = 0; d < dims; d++) { ease.push(new KeyframeEase(0, 33.3333)); }
240
+ prop.setTemporalEaseAtKey(k, ease, ease);
241
+ }
242
+ }
243
+ return { property: prop.name, numKeys: prop.numKeys };
244
+ });`);
245
+ }
246
+ export function getKeyframesScript(compId, layerIndex, property, propertyPath) {
247
+ const path = propertyPath ? `[${propertyPath.map(lit).join(", ")}]` : "null";
248
+ return wrap(`
249
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
250
+ var prop = __prop(l, ${lit(property)}, ${path});
251
+ var keys = [];
252
+ for (var i = 1; i <= prop.numKeys; i++) { keys.push({ time: prop.keyTime(i), value: prop.keyValue(i) }); }
253
+ return { property: prop.name, value: prop.value, numKeys: prop.numKeys, keys: keys, expression: prop.expressionEnabled ? prop.expression : null };`);
254
+ }
255
+ export function removeKeyframesScript(compId, layerIndex, property, propertyPath) {
256
+ const path = propertyPath ? `[${propertyPath.map(lit).join(", ")}]` : "null";
257
+ return wrap(`
258
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
259
+ var prop = __prop(l, ${lit(property)}, ${path});
260
+ return __undo("Remove keyframes", function () {
261
+ var n = prop.numKeys;
262
+ while (prop.numKeys > 0) { prop.removeKey(1); }
263
+ return { property: prop.name, removed: n };
264
+ });`);
265
+ }
266
+ export function setExpressionScript(compId, layerIndex, property, propertyPath, expression) {
267
+ const path = propertyPath ? `[${propertyPath.map(lit).join(", ")}]` : "null";
268
+ return wrap(`
269
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
270
+ var prop = __prop(l, ${lit(property)}, ${path});
271
+ return __undo("Set expression", function () {
272
+ var e = ${opt(expression)};
273
+ if (e === null) { prop.expression = ""; return { property: prop.name, expression: null }; }
274
+ prop.expression = e;
275
+ if (prop.expressionError) { throw new Error("Expression error: " + prop.expressionError); }
276
+ return { property: prop.name, expression: prop.expression, enabled: prop.expressionEnabled };
277
+ });`);
278
+ }
279
+ export function applyEffectScript(compId, layerIndex, matchName) {
280
+ return wrap(`
281
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
282
+ return __undo("Apply effect", function () {
283
+ var fx = l.property("ADBE Effect Parade");
284
+ if (!fx.canAddProperty(${lit(matchName)})) { throw new Error("Effect not available: " + ${lit(matchName)} + " (use a match name like 'ADBE Gaussian Blur 2' or a display name)."); }
285
+ var e = fx.addProperty(${lit(matchName)});
286
+ var params = [];
287
+ for (var i = 1; i <= e.numProperties; i++) { var pp = e.property(i); params.push({ index: i, name: pp.name, matchName: pp.matchName }); }
288
+ return { index: e.propertyIndex, name: e.name, matchName: e.matchName, params: params };
289
+ });`);
290
+ }
291
+ export function setEffectParamScript(compId, layerIndex, effect, param, value) {
292
+ const v = typeof value === "string" ? lit(value) : Array.isArray(value) ? `[${value.map(num).join(", ")}]` : num(value);
293
+ return wrap(`
294
+ var c = __comp(${num(compId)}); var l = __layer(c, ${num(layerIndex)});
295
+ var fx = l.property("ADBE Effect Parade");
296
+ var e = fx.property(${typeof effect === "number" ? num(effect) : lit(effect)});
297
+ if (!e) { throw new Error("Effect not found on layer: " + ${typeof effect === "number" ? num(effect) : lit(effect)}); }
298
+ var pp = e.property(${lit(param)});
299
+ if (!pp) { throw new Error("Parameter not found: " + ${lit(param)}); }
300
+ return __undo("Set effect parameter", function () { pp.setValue(${v}); return { effect: e.name, param: pp.name, value: pp.value }; });`);
301
+ }
302
+ export function setTextScript(p) {
303
+ return wrap(`
304
+ var c = __comp(${num(p.compId)}); var l = __layer(c, ${num(p.layerIndex)});
305
+ if (l.matchName !== "ADBE Text Layer") { throw new Error("Layer " + l.index + " is not a text layer."); }
306
+ return __undo("Set text", function () {
307
+ var st = l.property("ADBE Text Properties").property("ADBE Text Document");
308
+ var td = st.value;
309
+ var v;
310
+ v = ${opt(p.text)}; if (v !== null) { td.text = v; }
311
+ v = ${optNum(p.fontSize)}; if (v !== null) { td.fontSize = v; }
312
+ v = ${opt(p.font)}; if (v !== null) { td.font = v; }
313
+ v = ${p.color ? rgb(p.color) : "null"}; if (v !== null) { td.applyFill = true; td.fillColor = v; }
314
+ v = ${opt(p.justification)};
315
+ if (v !== null) { td.justification = v === "center" ? ParagraphJustification.CENTER_JUSTIFY : v === "right" ? ParagraphJustification.RIGHT_JUSTIFY : ParagraphJustification.LEFT_JUSTIFY; }
316
+ st.setValue(td);
317
+ // Point text is positioned by its anchor; put the anchor at the visual
318
+ // center so position means "center of the text" regardless of
319
+ // justification (CENTER_JUSTIFY did not center on the anchor here).
320
+ __centerAnchor(l);
321
+ var info = __layerInfo(l); info.text = st.value.text; return info;
322
+ });`);
323
+ }
324
+ export function addMarkerScript(compId, time, comment, layerIndex, duration) {
325
+ return wrap(`
326
+ var c = __comp(${num(compId)});
327
+ return __undo("Add marker", function () {
328
+ var m = new MarkerValue(${lit(comment)});
329
+ var dur = ${optNum(duration)}; if (dur !== null) { m.duration = dur; }
330
+ var li = ${optNum(layerIndex)};
331
+ var target = li === null ? c.markerProperty : __layer(c, li).property("ADBE Marker");
332
+ target.setValueAtTime(${num(time)}, m);
333
+ return { time: ${num(time)}, comment: ${lit(comment)}, on: li === null ? "comp" : "layer " + li };
334
+ });`);
335
+ }
336
+ export function renderFrameScript(compId, time, path, maxDimension) {
337
+ return wrap(`
338
+ var c = __comp(${num(compId)});
339
+ var longest = Math.max(c.width, c.height);
340
+ var s = longest > ${num(maxDimension)} ? ${num(maxDimension)} / longest : 1;
341
+ var w = Math.max(4, Math.round(c.width * s)), h = Math.max(4, Math.round(c.height * s));
342
+ var tmp = app.project.items.addComp("__acm_preview", w, h, c.pixelAspect, Math.max(c.duration, ${num(time)} + c.frameDuration), c.frameRate);
343
+ try {
344
+ var l = tmp.layers.add(c);
345
+ l.property("ADBE Transform Group").property("ADBE Scale").setValue([s * 100, s * 100]);
346
+ l.property("ADBE Transform Group").property("ADBE Position").setValue([w / 2, h / 2]);
347
+ tmp.saveFrameToPng(${num(time)}, new File(${lit(path)}));
348
+ } finally {
349
+ tmp.remove();
350
+ }
351
+ return { path: ${lit(path)}, time: ${num(time)}, width: w, height: h, sourceWidth: c.width, sourceHeight: c.height };`);
352
+ }
353
+ export const AERENDER_INFO = wrap(`
354
+ var proj = app.project;
355
+ var appFolder = Folder.appPackage.fsName;
356
+ return { projectPath: proj.file ? proj.file.fsName : null, dirty: proj.dirty === true, appFolder: appFolder, isWindows: $.os.indexOf("Windows") === 0 };`);
357
+ // ---- aerender (process lane) -------------------------------------------------
358
+ function aerenderExecutable(appFolder, isWindows) {
359
+ // Windows: <...>\Support Files\aerender.exe (appPackage IS "Support Files").
360
+ // macOS: appPackage is the .app bundle's parent folder; aerender sits next to the app.
361
+ return isWindows ? join(appFolder, "aerender.exe") : join(appFolder, "aerender");
362
+ }
363
+ /** saveFrameToPng returns before the file is fully written: wait for it to appear and stop growing. */
364
+ async function waitForFile(path, timeoutMs) {
365
+ const deadline = Date.now() + timeoutMs;
366
+ let lastSize = -1;
367
+ while (Date.now() < deadline) {
368
+ try {
369
+ const { size } = await stat(path);
370
+ if (size > 0 && size === lastSize)
371
+ return;
372
+ lastSize = size;
373
+ }
374
+ catch {
375
+ /* not there yet */
376
+ }
377
+ await new Promise((r) => setTimeout(r, 150));
378
+ }
379
+ throw new Error(`After Effects did not finish writing ${path} within ${Math.round(timeoutMs / 1000)}s. Is "Allow Scripts to Write Files and Access Network" enabled in Preferences > Scripting & Expressions?`);
380
+ }
381
+ function runAerender(exe, args, timeoutMs, signal, onLine) {
382
+ return new Promise((resolve, reject) => {
383
+ const child = spawn(exe, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
384
+ let out = "";
385
+ let partial = "";
386
+ const keep = (d) => {
387
+ const text = d.toString();
388
+ out += text;
389
+ if (out.length > 8000)
390
+ out = out.slice(-8000);
391
+ if (onLine) {
392
+ partial += text;
393
+ const lines = partial.split(/\r?\n/);
394
+ partial = lines.pop() ?? "";
395
+ for (const l of lines)
396
+ if (l.includes("PROGRESS:") || l.includes("Finished") || /error/i.test(l))
397
+ onLine(l.trim());
398
+ }
399
+ };
400
+ child.stdout.on("data", keep);
401
+ child.stderr.on("data", keep);
402
+ const timer = setTimeout(() => {
403
+ child.kill();
404
+ reject(new Error(`aerender did not finish within ${Math.round(timeoutMs / 1000)}s`));
405
+ }, timeoutMs);
406
+ const onAbort = () => {
407
+ child.kill();
408
+ reject(new Error("aerender was cancelled"));
409
+ };
410
+ signal?.addEventListener("abort", onAbort, { once: true });
411
+ child.on("error", (e) => {
412
+ clearTimeout(timer);
413
+ signal?.removeEventListener("abort", onAbort);
414
+ reject(e);
415
+ });
416
+ child.on("close", (code) => {
417
+ clearTimeout(timer);
418
+ signal?.removeEventListener("abort", onAbort);
419
+ resolve({ code, tail: out });
420
+ });
421
+ });
422
+ }
423
+ /**
424
+ * Save the project, then render one comp with aerender. Shared by the
425
+ * ae_render_comp tool and the pipelines; throws with an actionable message.
426
+ */
427
+ export async function renderCompHeadless(bridge, o, ctx = {}) {
428
+ const info = (await bridge.evaluate(saveProjectScript(undefined), { timeoutClass: "slow" }));
429
+ const meta = (await bridge.evaluate(AERENDER_INFO, { timeoutClass: "fast" }));
430
+ if (!info.path || !meta.projectPath)
431
+ throw new Error("Project has no file. Save it first with ae_save_project.");
432
+ const exe = aerenderExecutable(meta.appFolder, meta.isWindows);
433
+ try {
434
+ await stat(exe);
435
+ }
436
+ catch {
437
+ throw new Error(`aerender not found at ${exe}`);
438
+ }
439
+ const args = ["-project", meta.projectPath, "-comp", o.compName, "-output", o.outputPath];
440
+ if (o.outputModule)
441
+ args.push("-OMtemplate", o.outputModule);
442
+ if (o.renderSettings)
443
+ args.push("-RStemplate", o.renderSettings);
444
+ log.info(`aerender ${args.join(" ")}`);
445
+ const { code, tail } = await runAerender(exe, args, 30 * 60_000, ctx.signal, ctx.log);
446
+ if (code !== 0)
447
+ throw new Error(`aerender exited with ${code}:\n${tail.slice(-1500)}`);
448
+ return { outputPath: o.outputPath, compName: o.compName, log: tail.slice(-600) };
449
+ }
450
+ // ---- registration --------------------------------------------------------------
451
+ const compId = z.number().int().describe("Composition id from ae_list_compositions.");
452
+ const layerIndex = z.number().int().min(1).describe("Layer index in the comp (1 = top of the timeline), from ae_get_comp.");
453
+ const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, "hex color like #ff8800").describe("Hex color like #ff8800.");
454
+ const propertyName = z
455
+ .enum(["position", "scale", "rotation", "opacity", "anchorPoint"])
456
+ .describe("Transform property. For anything else, give propertyPath instead.");
457
+ const propertyPath = z
458
+ .array(z.string().min(1))
459
+ .min(1)
460
+ .optional()
461
+ .describe("Optional explicit property path of match names or display names from the layer, e.g. [\"ADBE Effect Parade\", \"Gaussian Blur\", \"Blurriness\"]. Overrides `property`.");
462
+ const seconds = (what) => z.number().finite().nonnegative().describe(`${what}, in seconds.`);
463
+ export function registerAfterEffectsTools(server, bridge, options = {}) {
464
+ const run = (script, opts) => guard(async () => jsonResult(await bridge.evaluate(script, opts)));
465
+ // ---- read ---------------------------------------------------------------
466
+ server.registerTool("ae_project_info", {
467
+ title: "After Effects: project info",
468
+ description: "Read the open After Effects project's file path, item count, bit depth, and unsaved-changes flag.",
469
+ inputSchema: {},
470
+ annotations: { readOnlyHint: true },
471
+ }, async () => run(PROJECT_INFO, { timeoutClass: "fast" }));
472
+ server.registerTool("ae_list_compositions", {
473
+ title: "After Effects: list compositions",
474
+ description: "List every composition in the open project with its id, dimensions, duration, frame rate, and layer count.",
475
+ inputSchema: {},
476
+ annotations: { readOnlyHint: true },
477
+ }, async () => run(LIST_COMPOSITIONS, { timeoutClass: "fast" }));
478
+ server.registerTool("ae_list_footage", {
479
+ title: "After Effects: list footage items",
480
+ description: "List imported footage and solids with ids (for ae_add_layer kind=footage), paths, size, and duration.",
481
+ inputSchema: {},
482
+ annotations: { readOnlyHint: true },
483
+ }, async () => run(LIST_FOOTAGE, { timeoutClass: "fast" }));
484
+ server.registerTool("ae_get_comp", {
485
+ title: "After Effects: comp details with layers",
486
+ description: "A composition's settings and every layer (index, name, kind, in/out/start, parent). Layer indexes are what the layer tools take.",
487
+ inputSchema: { compId },
488
+ annotations: { readOnlyHint: true },
489
+ }, async ({ compId: id }) => run(getCompScript(id), { timeoutClass: "fast" }));
490
+ server.registerTool("ae_get_layer", {
491
+ title: "After Effects: layer details",
492
+ description: "One layer's transform values (with keyframe counts and expressions), effects, and text content.",
493
+ inputSchema: { compId, layerIndex },
494
+ annotations: { readOnlyHint: true },
495
+ }, async ({ compId: id, layerIndex: li }) => run(getLayerScript(id, li), { timeoutClass: "fast" }));
496
+ // ---- project ------------------------------------------------------------
497
+ server.registerTool("ae_open_project", {
498
+ title: "After Effects: open a project",
499
+ description: "Open an .aep project file (closes the current project; After Effects may prompt to save it).",
500
+ inputSchema: { path: z.string().min(1).describe("Absolute path to an .aep file.") },
501
+ annotations: { destructiveHint: true },
502
+ }, async ({ path }) => run(openProjectScript(path)));
503
+ server.registerTool("ae_save_project", {
504
+ title: "After Effects: save the project",
505
+ description: "Save the project. Pass a path to Save As (.aep; required the first time); omit it to save in place.",
506
+ inputSchema: { path: z.string().min(1).optional().describe("Absolute path ending in .aep.") },
507
+ annotations: { destructiveHint: true },
508
+ }, async ({ path }) => run(saveProjectScript(path)));
509
+ server.registerTool("ae_import_footage", {
510
+ title: "After Effects: import footage",
511
+ description: "Import an image, video, audio, or PSD file into the project. Returns the footage item id for ae_add_layer.",
512
+ inputSchema: { path: z.string().min(1).describe("Absolute path to the media file.") },
513
+ }, async ({ path }) => run(importFootageScript(path)));
514
+ // ---- comps & layers -----------------------------------------------------
515
+ server.registerTool("ae_create_comp", {
516
+ title: "After Effects: create a composition",
517
+ description: "Create a new composition. Returns its id.",
518
+ inputSchema: {
519
+ name: z.string().min(1),
520
+ width: z.number().int().positive().describe("Pixels."),
521
+ height: z.number().int().positive().describe("Pixels."),
522
+ frameRate: z.number().positive().optional().describe("Defaults to 30."),
523
+ duration: z.number().positive().optional().describe("Seconds. Defaults to 10."),
524
+ pixelAspect: z.number().positive().optional().describe("Defaults to 1."),
525
+ },
526
+ }, async (p) => run(createCompScript({ ...p, frameRate: p.frameRate ?? 30, duration: p.duration ?? 10, pixelAspect: p.pixelAspect ?? 1 })));
527
+ server.registerTool("ae_add_layer", {
528
+ title: "After Effects: add a layer",
529
+ description: "Add a layer to a comp: footage (needs itemId — a footage item or another comp), solid (color, optional size), " +
530
+ "text (initial text), null, or adjustment. New layers go to the top. Returns the layer's index.",
531
+ inputSchema: {
532
+ compId,
533
+ kind: z.enum(["footage", "solid", "text", "null", "adjustment"]),
534
+ itemId: z.number().int().optional().describe("footage: id from ae_list_footage or ae_list_compositions."),
535
+ color: hexColor.optional().describe("solid: fill color. Defaults to white."),
536
+ name: z.string().min(1).optional(),
537
+ text: z.string().optional().describe("text: initial content."),
538
+ width: z.number().int().positive().optional().describe("solid: defaults to comp width."),
539
+ height: z.number().int().positive().optional().describe("solid: defaults to comp height."),
540
+ },
541
+ }, async (p) => run(addLayerScript(p)));
542
+ server.registerTool("ae_set_layer_props", {
543
+ title: "After Effects: set layer properties",
544
+ description: "Set any of: name, enabled, startTime/inPoint/outPoint (seconds), position [x,y] or [x,y,z], scale [%,%], " +
545
+ "rotation (degrees), opacity (0–100), anchorPoint, parentIndex (null to unparent). Only given fields change.",
546
+ inputSchema: {
547
+ compId,
548
+ layerIndex,
549
+ name: z.string().min(1).optional(),
550
+ enabled: z.boolean().optional(),
551
+ startTime: z.number().finite().optional(),
552
+ inPoint: z.number().finite().optional(),
553
+ outPoint: z.number().finite().optional(),
554
+ position: z.array(z.number().finite()).min(2).max(3).optional(),
555
+ scale: z.array(z.number().finite()).min(2).max(3).optional(),
556
+ rotation: z.number().finite().optional(),
557
+ opacity: z.number().min(0).max(100).optional(),
558
+ anchorPoint: z.array(z.number().finite()).min(2).max(3).optional(),
559
+ parentIndex: z.number().int().min(1).nullable().optional(),
560
+ },
561
+ }, async ({ compId: id, layerIndex: li, ...props }) => run(setLayerPropsScript(id, li, props)));
562
+ server.registerTool("ae_duplicate_layer", {
563
+ title: "After Effects: duplicate a layer",
564
+ description: "Duplicate a layer (the copy goes directly above it). Returns the copy's index.",
565
+ inputSchema: { compId, layerIndex, name: z.string().min(1).optional() },
566
+ }, async ({ compId: id, layerIndex: li, name }) => run(duplicateLayerScript(id, li, name)));
567
+ server.registerTool("ae_delete_layer", {
568
+ title: "After Effects: delete a layer",
569
+ description: "Remove a layer from a comp.",
570
+ inputSchema: { compId, layerIndex },
571
+ annotations: { destructiveHint: true },
572
+ }, async ({ compId: id, layerIndex: li }) => run(deleteLayerScript(id, li)));
573
+ // ---- animation ----------------------------------------------------------
574
+ server.registerTool("ae_set_keyframes", {
575
+ title: "After Effects: set keyframes",
576
+ description: "Add or replace keyframes on a layer property. Each key is {time, value, easy?}; value is a number (opacity, rotation) " +
577
+ "or an array (position, scale, anchorPoint). easy applies Easy Ease. Use propertyPath for effect parameters.",
578
+ inputSchema: {
579
+ compId,
580
+ layerIndex,
581
+ property: propertyName,
582
+ propertyPath,
583
+ keys: z
584
+ .array(z.object({ time: seconds("Key time"), value: z.union([z.number(), z.array(z.number())]), easy: z.boolean().optional() }))
585
+ .min(1),
586
+ },
587
+ }, async (p) => run(setKeyframesScript(p)));
588
+ server.registerTool("ae_get_keyframes", {
589
+ title: "After Effects: read a property's keyframes",
590
+ description: "Current value, keyframes (time + value), and expression of a layer property.",
591
+ inputSchema: { compId, layerIndex, property: propertyName, propertyPath },
592
+ annotations: { readOnlyHint: true },
593
+ }, async ({ compId: id, layerIndex: li, property, propertyPath: pp }) => run(getKeyframesScript(id, li, property, pp), { timeoutClass: "fast" }));
594
+ server.registerTool("ae_remove_keyframes", {
595
+ title: "After Effects: remove all keyframes on a property",
596
+ description: "Delete every keyframe on the property (the value freezes at its current value).",
597
+ inputSchema: { compId, layerIndex, property: propertyName, propertyPath },
598
+ annotations: { destructiveHint: true },
599
+ }, async ({ compId: id, layerIndex: li, property, propertyPath: pp }) => run(removeKeyframesScript(id, li, property, pp)));
600
+ server.registerTool("ae_set_expression", {
601
+ title: "After Effects: set an expression",
602
+ description: "Set (or clear with null) the expression on a layer property. Fails with the error text if the expression does not compile.",
603
+ inputSchema: { compId, layerIndex, property: propertyName, propertyPath, expression: z.string().nullable().describe("Expression source, or null to remove.") },
604
+ }, async ({ compId: id, layerIndex: li, property, propertyPath: pp, expression }) => run(setExpressionScript(id, li, property, pp, expression)));
605
+ // ---- effects, text, markers ---------------------------------------------
606
+ server.registerTool("ae_apply_effect", {
607
+ title: "After Effects: apply an effect",
608
+ description: "Add an effect to a layer by match name (e.g. 'ADBE Gaussian Blur 2', 'ADBE Drop Shadow', 'ADBE Glo2', 'ADBE Tint') " +
609
+ "or display name. Returns the effect index and its parameter names for ae_set_effect_param.",
610
+ inputSchema: { compId, layerIndex, matchName: z.string().min(1) },
611
+ }, async ({ compId: id, layerIndex: li, matchName }) => run(applyEffectScript(id, li, matchName)));
612
+ server.registerTool("ae_set_effect_param", {
613
+ title: "After Effects: set an effect parameter",
614
+ description: "Set a parameter on an effect (by effect index or name) — number, array (color as [r,g,b] 0–1, point), or text.",
615
+ inputSchema: {
616
+ compId,
617
+ layerIndex,
618
+ effect: z.union([z.number().int().min(1), z.string().min(1)]).describe("Effect index (from ae_apply_effect / ae_get_layer) or name."),
619
+ param: z.string().min(1).describe("Parameter display name or match name, e.g. 'Blurriness'."),
620
+ value: z.union([z.number(), z.array(z.number()), z.string()]),
621
+ },
622
+ }, async ({ compId: id, layerIndex: li, effect, param, value }) => run(setEffectParamScript(id, li, effect, param, value)));
623
+ server.registerTool("ae_set_text", {
624
+ title: "After Effects: set text layer content and style",
625
+ description: "Change a text layer's text, font size, font (PostScript name), color, or justification.",
626
+ inputSchema: {
627
+ compId,
628
+ layerIndex,
629
+ text: z.string().optional(),
630
+ fontSize: z.number().positive().optional(),
631
+ font: z.string().min(1).optional().describe("PostScript font name, e.g. Arial-BoldMT."),
632
+ color: hexColor.optional(),
633
+ justification: z.enum(["left", "center", "right"]).optional(),
634
+ },
635
+ }, async (p) => run(setTextScript(p)));
636
+ server.registerTool("ae_add_marker", {
637
+ title: "After Effects: add a marker",
638
+ description: "Add a comp marker (or a layer marker if layerIndex is given) at a time with a comment.",
639
+ inputSchema: { compId, time: seconds("Marker time"), comment: z.string(), layerIndex: layerIndex.optional(), duration: seconds("Marker duration").optional() },
640
+ }, async ({ compId: id, time, comment, layerIndex: li, duration }) => run(addMarkerScript(id, time, comment, li, duration)));
641
+ // ---- render -------------------------------------------------------------
642
+ server.registerTool("ae_render_frame", {
643
+ title: "After Effects: render one frame as an image",
644
+ description: "Render a single frame of a comp at a time (seconds) to a small PNG and return it as an image so you can see it. " +
645
+ "Uses a temporary downscaled comp; the project is left as it was.",
646
+ inputSchema: { compId, time: seconds("Frame time").optional(), maxDimension: z.number().int().min(64).max(2048).optional().describe("Longest edge in px. Defaults to 1024.") },
647
+ annotations: { readOnlyHint: true },
648
+ }, async ({ compId: id, time, maxDimension }) => guard(async () => {
649
+ const dir = join(tmpdir(), "brainferno-mcp-bridge", "previews");
650
+ await mkdir(dir, { recursive: true });
651
+ const path = join(dir, `${randomUUID()}.png`).replace(/\\/g, "/");
652
+ await bridge.evaluate(renderFrameScript(id, time ?? 0, path, maxDimension ?? 1024), { timeoutClass: "slow" });
653
+ await waitForFile(path, 20_000);
654
+ return imageResult(path, "image/png", `Frame at ${time ?? 0}s`);
655
+ }));
656
+ server.registerTool("ae_queue_render", {
657
+ title: "After Effects: add a composition to the render queue",
658
+ description: "Add a comp to the render queue with an output path (and optional output-module template). Queues only; ae_render_comp renders headlessly.",
659
+ inputSchema: {
660
+ compName: z.string().min(1).describe("Exact name of the composition to queue."),
661
+ outputPath: z.string().min(1).describe("Absolute output file path."),
662
+ templateName: z.string().min(1).optional().describe("Output module template, e.g. 'H.264 - Match Render Settings'."),
663
+ },
664
+ }, async ({ compName, outputPath, templateName }) => run(wrap(`
665
+ var target = null;
666
+ for (var i = 1; i <= app.project.numItems; i++) { var item = app.project.item(i); if (item instanceof CompItem && item.name === ${lit(compName)}) { target = item; break; } }
667
+ if (target === null) { throw new Error("No composition named " + ${lit(compName)}); }
668
+ return __undo("Queue render", function () {
669
+ var rqItem = app.project.renderQueue.items.add(target);
670
+ var output = rqItem.outputModule(1);
671
+ var template = ${opt(templateName)};
672
+ if (template !== null) { output.applyTemplate(template); }
673
+ output.file = new File(${lit(outputPath)});
674
+ return { queueIndex: app.project.renderQueue.numItems, compName: target.name, outputPath: output.file.fsName, status: "queued" };
675
+ });`)));
676
+ server.registerTool("ae_render_comp", {
677
+ title: "After Effects: render a comp headlessly (aerender)",
678
+ description: "Save the project, then render a comp with aerender in the background (the After Effects UI stays free). " +
679
+ "Waits for it to finish (up to 30 minutes) and returns the output path. The project must have been saved once (ae_save_project).",
680
+ inputSchema: {
681
+ compName: z.string().min(1).describe("Exact composition name."),
682
+ outputPath: z.string().min(1).describe("Absolute output path, e.g. C:/renders/comp.mp4 or .mov; aerender picks the format from the output module."),
683
+ outputModule: z.string().min(1).optional().describe("Output module template name, e.g. 'H.264 - Match Render Settings' or 'Lossless'."),
684
+ renderSettings: z.string().min(1).optional().describe("Render settings template, e.g. 'Best Settings'."),
685
+ wait: z.boolean().optional().describe("Block until the render finishes (default). false returns a jobId at once; poll with cc_job_wait."),
686
+ },
687
+ annotations: { destructiveHint: true },
688
+ }, async ({ compName, outputPath, outputModule, renderSettings, wait }, extra) => guard(async () => {
689
+ const o = { compName, outputPath, ...(outputModule !== undefined ? { outputModule } : {}), ...(renderSettings !== undefined ? { renderSettings } : {}) };
690
+ if (!options.jobs)
691
+ return jsonResult(await renderCompHeadless(bridge, o));
692
+ return runOrQueue(options.jobs, "ae_render_comp", [{ name: "Render with aerender", recoveryTool: "ae_render_comp", run: (ctx) => renderCompHeadless(bridge, o, { signal: ctx.signal, log: ctx.log }).then((r) => (ctx.artifact(r.outputPath), r)) }], (results) => results[0], { wait: wait ?? true, timeoutMs: 30 * 60_000, extra: extra });
693
+ }));
694
+ }
695
+ //# sourceMappingURL=after-effects.js.map