@causyne/agent 1.10.18 → 1.10.20

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 (39) hide show
  1. package/docs/ARCHITECTURE.md +20 -0
  2. package/native-skills/media/creative-studio-pipeline/SKILL.md +52 -0
  3. package/native-skills/media/photo-studio-automation/SKILL.md +53 -0
  4. package/native-skills/media/video-timeline-assembly/SKILL.md +52 -0
  5. package/package.json +1 -1
  6. package/src/agentLoop.js +4 -0
  7. package/src/appChannel.js +7 -3
  8. package/src/cliMode.js +33 -67
  9. package/src/commands.js +56 -8
  10. package/src/compiledSkills.js +15 -0
  11. package/src/config.js +80 -11
  12. package/src/creative/apps.js +69 -0
  13. package/src/creative/apps.ts +58 -0
  14. package/src/creative/blenderBatch.js +233 -0
  15. package/src/creative/blenderBatch.ts +217 -0
  16. package/src/creative/pipeline.js +56 -0
  17. package/src/creative/pipeline.ts +82 -0
  18. package/src/creative/studioScripts.js +374 -0
  19. package/src/creative/studioScripts.ts +378 -0
  20. package/src/decisionPolicy.js +2 -0
  21. package/src/decisionPolicy.ts +1 -0
  22. package/src/dynamicCatalogue.js +270 -0
  23. package/src/dynamicCatalogue.ts +283 -0
  24. package/src/entrypoint.js +2 -1
  25. package/src/modelCatalogue.d.ts +5 -0
  26. package/src/modelCatalogue.js +49 -18
  27. package/src/providers/anthropic.js +4 -0
  28. package/src/sandbox.js +4 -0
  29. package/src/toolPacks.js +1 -0
  30. package/src/tools/creativeApps.js +106 -0
  31. package/src/tools/creativeApps.ts +99 -0
  32. package/src/tools/registry.js +2 -1
  33. package/src/tui/banner.js +391 -190
  34. package/src/tui/banner.ts +409 -0
  35. package/src/tui/chatRenderer.js +1 -1
  36. package/src/tui/dialogs.js +10 -0
  37. package/src/tui/home.js +29 -72
  38. package/src/tui/setup.js +104 -0
  39. package/src/tui/setup.ts +98 -0
@@ -0,0 +1,82 @@
1
+ import { runBlenderBatch, type Batch as BlenderBatch } from './blenderBatch.js';
2
+ import { runStudioEdit, type StudioJob } from './studioScripts.js';
3
+
4
+ export interface PipelineStage {
5
+ id: string;
6
+ type: '3d_batch' | 'studio_batch';
7
+ config: Record<string, unknown>;
8
+ }
9
+
10
+ export interface StudioPipeline {
11
+ stages: PipelineStage[];
12
+ }
13
+
14
+ export interface PipelineResult {
15
+ success: boolean;
16
+ stagesCompleted: number;
17
+ stageResults: Array<{ id: string; success: boolean; output?: string | undefined; error?: string | undefined }>;
18
+ error?: string | undefined;
19
+ }
20
+
21
+ export function validatePipeline(value: unknown): StudioPipeline {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ throw new Error('Pipeline must be an object');
24
+ }
25
+ const obj = value as Record<string, unknown>;
26
+ if (!Array.isArray(obj.stages) || obj.stages.length < 1 || obj.stages.length > 16) {
27
+ throw new Error('Provide 1..16 pipeline stages');
28
+ }
29
+
30
+ const stages: PipelineStage[] = obj.stages.map((raw, index) => {
31
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
32
+ throw new Error(`Stage ${index + 1} must be an object`);
33
+ }
34
+ const s = raw as Record<string, unknown>;
35
+ const id = typeof s.id === 'string' && s.id.trim() ? s.id.trim() : `stage_${index + 1}`;
36
+ if (s.type !== '3d_batch' && s.type !== 'studio_batch') {
37
+ throw new Error(`Stage "${id}" must have type "3d_batch" or "studio_batch"`);
38
+ }
39
+ const type: '3d_batch' | 'studio_batch' = s.type;
40
+ if (!s.config || typeof s.config !== 'object' || Array.isArray(s.config)) {
41
+ throw new Error(`Stage "${id}" requires a configuration object`);
42
+ }
43
+ return { id, type, config: s.config as Record<string, unknown> };
44
+ });
45
+
46
+ return { stages };
47
+ }
48
+
49
+ export async function runStudioPipeline(
50
+ value: unknown,
51
+ workspace = process.cwd(),
52
+ signal?: AbortSignal
53
+ ): Promise<PipelineResult> {
54
+ signal?.throwIfAborted();
55
+ const pipeline = validatePipeline(value);
56
+ const stageResults: PipelineResult['stageResults'] = [];
57
+
58
+ for (let i = 0; i < pipeline.stages.length; i++) {
59
+ signal?.throwIfAborted();
60
+ const stage = pipeline.stages[i]!;
61
+
62
+ try {
63
+ if (stage.type === '3d_batch') {
64
+ const res = await runBlenderBatch(stage.config, workspace, signal);
65
+ stageResults.push({ id: stage.id, success: true, output: res.output });
66
+ } else {
67
+ const res = await runStudioEdit(stage.config, workspace, signal);
68
+ if (!res.success) {
69
+ stageResults.push({ id: stage.id, success: false, error: res.error });
70
+ return { success: false, stagesCompleted: i, stageResults, error: `Stage "${stage.id}" failed: ${res.error}` };
71
+ }
72
+ stageResults.push({ id: stage.id, success: true, output: res.output });
73
+ }
74
+ } catch (err) {
75
+ const errMsg = err instanceof Error ? err.message : String(err);
76
+ stageResults.push({ id: stage.id, success: false, error: errMsg });
77
+ return { success: false, stagesCompleted: i, stageResults, error: `Stage "${stage.id}" failed: ${errMsg}` };
78
+ }
79
+ }
80
+
81
+ return { success: true, stagesCompleted: pipeline.stages.length, stageResults };
82
+ }
@@ -0,0 +1,374 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ const exec = promisify(execFile);
8
+ export function canonicalHost(host) {
9
+ if (host === 'photo-studio' || host === 'photoshop')
10
+ return 'photoshop';
11
+ if (host === 'video-studio' || host === 'premiere')
12
+ return 'premiere';
13
+ if (host === 'motion-studio' || host === 'after-effects')
14
+ return 'after-effects';
15
+ throw new Error('Supported studio hosts: photo-studio (or photoshop), video-studio (or premiere), motion-studio (or after-effects)');
16
+ }
17
+ function object(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ throw new Error('Expected an object');
20
+ return value;
21
+ }
22
+ function text(value, name, max = 256) {
23
+ if (typeof value !== 'string' || !value.trim() || value.length > max || /[\x00-\x1f]/.test(value)) {
24
+ throw new Error(`Invalid ${name}`);
25
+ }
26
+ return value;
27
+ }
28
+ function number(value, name, min, max) {
29
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
30
+ throw new Error(`Invalid ${name}: expected ${min}..${max}`);
31
+ }
32
+ return value;
33
+ }
34
+ export function validateStudioJob(value) {
35
+ const job = object(value);
36
+ const cHost = canonicalHost(job.host);
37
+ const output = text(job.output, 'output', 4096);
38
+ if (cHost === 'photoshop') {
39
+ if (!/\.(psd|png|jpg|jpeg|webp)$/i.test(output)) {
40
+ throw new Error('Output must be PSD, PNG, JPG, or WEBP for Photo Studio');
41
+ }
42
+ }
43
+ else if (cHost === 'premiere') {
44
+ if (!/\.(prproj|xml)$/i.test(output)) {
45
+ throw new Error('Output must be PRPROJ or XML for Video Studio');
46
+ }
47
+ }
48
+ else {
49
+ if (!/\.aep$/i.test(output)) {
50
+ throw new Error('Output must be AEP for Motion Studio');
51
+ }
52
+ }
53
+ const input = (cHost === 'photoshop' || cHost === 'premiere') && job.input !== undefined
54
+ ? text(job.input, 'input', 4096)
55
+ : undefined;
56
+ if (!Array.isArray(job.actions) || job.actions.length < 1 || job.actions.length > 64) {
57
+ throw new Error('Provide 1..64 editing actions');
58
+ }
59
+ const actions = job.actions.map(raw => {
60
+ const a = object(raw);
61
+ const type = text(a.type, 'action type');
62
+ if (cHost === 'photoshop') {
63
+ if (type === 'resize')
64
+ return { type, width: number(a.width, 'width', 1, 16384), height: number(a.height, 'height', 1, 16384) };
65
+ if (type === 'rotate')
66
+ return { type, angle: number(a.angle, 'angle', -360, 360) };
67
+ if (type === 'crop')
68
+ return { type, left: number(a.left ?? 0, 'left', 0, 16384), top: number(a.top ?? 0, 'top', 0, 16384), width: number(a.width, 'width', 1, 16384), height: number(a.height, 'height', 1, 16384) };
69
+ if (type === 'layer') {
70
+ const result = { type, index: number(a.index, 'layer index', 0, 9999) };
71
+ if (!Number.isInteger(result.index))
72
+ throw new Error('Layer index must be an integer');
73
+ if (a.name !== undefined)
74
+ result.name = text(a.name, 'layer name');
75
+ if (a.opacity !== undefined)
76
+ result.opacity = number(a.opacity, 'opacity', 0, 100);
77
+ if (a.visible !== undefined) {
78
+ if (typeof a.visible !== 'boolean')
79
+ throw new Error('visible must be boolean');
80
+ result.visible = a.visible;
81
+ }
82
+ if (a.blendMode !== undefined)
83
+ result.blendMode = text(a.blendMode, 'blend mode', 64);
84
+ if (Object.keys(result).length === 2)
85
+ throw new Error('Specify a layer change');
86
+ return result;
87
+ }
88
+ if (type === 'new_layer')
89
+ return { type, name: text(a.name, 'layer name'), opacity: typeof a.opacity === 'number' ? number(a.opacity, 'opacity', 0, 100) : 100 };
90
+ if (type === 'text')
91
+ return { type, text: text(a.text, 'text', 4000), x: number(a.x ?? 50, 'x', 0, 16384), y: number(a.y ?? 50, 'y', 0, 16384), size: typeof a.size === 'number' ? number(a.size, 'size', 1, 1000) : 32, font: typeof a.font === 'string' ? text(a.font, 'font') : 'Helvetica' };
92
+ if (type === 'adjust_color')
93
+ return { type, brightness: typeof a.brightness === 'number' ? number(a.brightness, 'brightness', -100, 100) : 0, contrast: typeof a.contrast === 'number' ? number(a.contrast, 'contrast', -100, 100) : 0 };
94
+ if (type === 'flatten')
95
+ return { type };
96
+ }
97
+ else if (cHost === 'premiere') {
98
+ if (type === 'sequence')
99
+ return { type, name: text(a.name, 'sequence name'), width: typeof a.width === 'number' ? number(a.width, 'width', 16, 8192) : 1920, height: typeof a.height === 'number' ? number(a.height, 'height', 16, 8192) : 1080, fps: typeof a.fps === 'number' ? number(a.fps, 'fps', 1, 120) : 30 };
100
+ if (type === 'import')
101
+ return { type, path: text(a.path, 'media path', 4096), start: number(a.start ?? 0, 'start', 0, 36000), ...(a.duration === undefined ? {} : { duration: number(a.duration, 'duration', 0.04, 36000) }), track: typeof a.track === 'number' ? number(a.track, 'track', 0, 99) : 0, mediaType: typeof a.mediaType === 'string' ? text(a.mediaType, 'mediaType') : 'video' };
102
+ if (type === 'marker')
103
+ return { type, start: number(a.start, 'marker time', 0, 36000), ...(a.name !== undefined ? { name: text(a.name, 'marker name') } : {}), ...(a.comment !== undefined ? { comment: text(a.comment, 'marker comment') } : {}) };
104
+ if (type === 'cut')
105
+ return { type, time: number(a.time, 'cut time', 0, 36000), track: number(a.track ?? 0, 'track index', 0, 99) };
106
+ }
107
+ else {
108
+ if (type === 'composition')
109
+ return { type, name: text(a.name, 'composition name'), width: number(a.width, 'width', 4, 16384), height: number(a.height, 'height', 4, 16384), duration: number(a.duration, 'duration', 0.04, 36000), fps: number(a.fps, 'fps', 1, 120) };
110
+ if (type === 'import')
111
+ return { type, path: text(a.path, 'media path', 4096), start: number(a.start ?? 0, 'start', 0, 36000), ...(a.duration === undefined ? {} : { duration: number(a.duration, 'duration', 0.04, 36000) }) };
112
+ if (type === 'text')
113
+ return { type, text: text(a.text, 'text', 2000), start: number(a.start ?? 0, 'start', 0, 36000) };
114
+ if (type === 'solid')
115
+ return { type, name: text(a.name, 'solid name'), width: number(a.width ?? 1920, 'width', 4, 16384), height: number(a.height ?? 1080, 'height', 4, 16384), duration: number(a.duration ?? 10, 'duration', 0.04, 36000) };
116
+ }
117
+ throw new Error('Unsupported editing action for this host');
118
+ });
119
+ if (cHost === 'after-effects' && (actions[0]?.type !== 'composition' || actions.filter(a => a.type === 'composition').length !== 1)) {
120
+ throw new Error('Motion Studio jobs must begin with exactly one composition');
121
+ }
122
+ return { host: job.host, canonicalHost: cHost, input, output, actions };
123
+ }
124
+ /** Fixed native operations; no model-provided script, eval, network, or service key. */
125
+ export function studioScript(value, resultFile) {
126
+ const job = validateStudioJob(value);
127
+ const cHost = job.canonicalHost;
128
+ const data = JSON.stringify({ ...job, resultFile }).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
129
+ const common = `var job = ${data};\nvar output = new File(job.output);\nif (output.exists) throw new Error('Output already exists');\n`;
130
+ const photoshop = `var source = null, edited = null;
131
+ var previousDialogs = app.displayDialogs;
132
+ try {
133
+ app.displayDialogs = DialogModes.NO;
134
+ if (job.input) {
135
+ source = app.open(new File(job.input));
136
+ edited = source.duplicate('Causyne Studio Edit', false);
137
+ } else {
138
+ edited = app.documents.add(1920, 1080, 72, 'Causyne Studio Project', NewDocumentMode.RGB);
139
+ }
140
+ for (var i=0; i<job.actions.length; i++) {
141
+ var a=job.actions[i];
142
+ if (a.type==='resize') edited.resizeImage(UnitValue(a.width,'px'),UnitValue(a.height,'px'),null,ResampleMethod.BICUBIC);
143
+ else if (a.type==='rotate') edited.rotateCanvas(a.angle);
144
+ else if (a.type==='crop') edited.crop([UnitValue(a.left,'px'),UnitValue(a.top,'px'),UnitValue(a.left+a.width,'px'),UnitValue(a.top+a.height,'px')]);
145
+ else if (a.type==='flatten') edited.flatten();
146
+ else if (a.type==='new_layer') {
147
+ var nl = edited.artLayers.add();
148
+ nl.name = a.name;
149
+ if (a.opacity !== undefined) nl.opacity = a.opacity;
150
+ } else if (a.type==='text') {
151
+ var tl = edited.artLayers.add();
152
+ tl.kind = LayerKind.TEXT;
153
+ tl.textItem.contents = a.text;
154
+ tl.textItem.position = [UnitValue(a.x,'px'), UnitValue(a.y,'px')];
155
+ if (a.size) tl.textItem.size = UnitValue(a.size,'pt');
156
+ } else if (a.type==='adjust_color') {
157
+ if (edited.activeLayer) {
158
+ if (a.brightness !== 0 || a.contrast !== 0) {
159
+ try { edited.activeLayer.adjustBrightnessContrast(a.brightness, a.contrast); } catch(e){}
160
+ }
161
+ }
162
+ } else {
163
+ if (a.index>=edited.layers.length) throw new Error('Layer index does not exist');
164
+ var layer=edited.layers[a.index];
165
+ if (a.name!==undefined) layer.name=a.name;
166
+ if (a.opacity!==undefined) layer.opacity=a.opacity;
167
+ if (a.visible!==undefined) layer.visible=a.visible;
168
+ }
169
+ }
170
+ if (output.exists) throw new Error('Output appeared while editing; refusing overwrite');
171
+ var options = /\\.png$/i.test(job.output) ? new PNGSaveOptions()
172
+ : /\\.(jpe?g)$/i.test(job.output) ? new JPEGSaveOptions()
173
+ : new PhotoshopSaveOptions();
174
+ edited.saveAs(output, options, true, Extension.LOWERCASE);
175
+ } finally {
176
+ if (edited) edited.close(SaveOptions.DONOTSAVECHANGES);
177
+ if (source) source.close(SaveOptions.DONOTSAVECHANGES);
178
+ app.displayDialogs=previousDialogs;
179
+ }
180
+ `;
181
+ const premiere = `// Video Studio batch automation
182
+ var proj = null;
183
+ if (job.input) {
184
+ app.openDocument(new File(job.input).fsName, false, false, false);
185
+ }
186
+ if (!app.project) {
187
+ app.newProject(new File(job.output).fsName);
188
+ }
189
+ proj = app.project;
190
+ if (!proj) throw new Error('Could not access Video Studio project');
191
+ for (var i = 0; i < job.actions.length; i++) {
192
+ var a = job.actions[i];
193
+ if (a.type === 'sequence') {
194
+ proj.createNewSequence(a.name, '');
195
+ } else if (a.type === 'import') {
196
+ var fileObj = new File(a.path);
197
+ if (!fileObj.exists) throw new Error('Import file does not exist: ' + a.path);
198
+ proj.importFiles([fileObj.fsName], true, proj.rootItem, false);
199
+ if (proj.activeSequence && proj.rootItem.children.numItems > 0) {
200
+ var item = proj.rootItem.children[proj.rootItem.children.numItems - 1];
201
+ if (item && proj.activeSequence.videoTracks && proj.activeSequence.videoTracks.numTracks > 0) {
202
+ proj.activeSequence.videoTracks[a.track || 0].insertClip(item, a.start || 0);
203
+ }
204
+ }
205
+ } else if (a.type === 'marker') {
206
+ if (proj.activeSequence && proj.activeSequence.markers) {
207
+ var m = proj.activeSequence.markers.createMarker(a.start);
208
+ if (a.name) m.name = a.name;
209
+ if (a.comment) m.comments = a.comment;
210
+ }
211
+ }
212
+ }
213
+ if (output.exists) throw new Error('Output appeared while editing; refusing overwrite');
214
+ proj.saveAs(output.fsName);
215
+ `;
216
+ const afterEffects = `// Motion Studio batch automation
217
+ if (app.project && (app.project.file || app.project.numItems>0)) throw new Error('Open an empty project before running this batch');
218
+ if (!app.project) app.newProject();
219
+ app.beginUndoGroup('Causyne creative batch');
220
+ try {
221
+ var c=job.actions[0];
222
+ var comp=app.project.items.addComp(c.name,c.width,c.height,1,c.duration,c.fps);
223
+ for (var i=1; i<job.actions.length; i++) {
224
+ var a=job.actions[i], layer;
225
+ if (a.type==='import') {
226
+ var media=app.project.importFile(new ImportOptions(new File(a.path)));
227
+ layer=comp.layers.add(media); layer.startTime=a.start;
228
+ if (a.duration!==undefined) layer.outPoint=Math.min(a.start+a.duration,comp.duration);
229
+ } else if (a.type==='solid') {
230
+ layer=comp.layers.addSolid([0.1,0.1,0.1],a.name,a.width||comp.width,a.height||comp.height,1,a.duration||comp.duration);
231
+ } else {
232
+ layer=comp.layers.addText(a.text); layer.startTime=a.start;
233
+ }
234
+ }
235
+ if (output.exists) throw new Error('Output appeared while editing; refusing overwrite');
236
+ app.project.save(output);
237
+ } finally { app.endUndoGroup(); }
238
+ `;
239
+ const body = cHost === 'photoshop' ? photoshop : cHost === 'premiere' ? premiere : afterEffects;
240
+ return `// Causyne Agent: reviewed local creative editing batch.\n(function(){\n${common}${body}
241
+ if (!output.exists || output.length===0) throw new Error('The host did not save the requested output');
242
+ var result = new File(job.resultFile);
243
+ result.encoding='UTF-8';
244
+ if (!result.open('w')) throw new Error('Cannot write completion receipt');
245
+ result.write('CAUSYNE_EDIT_COMPLETE'); result.close();
246
+ })();\n`;
247
+ }
248
+ function isInside(root, file) {
249
+ const relative = path.relative(root, file);
250
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
251
+ }
252
+ async function scoped(root, value, exists) {
253
+ const file = path.resolve(root, value);
254
+ const actual = exists
255
+ ? await fs.realpath(file)
256
+ : path.join(await fs.realpath(path.dirname(file)), path.basename(file));
257
+ if (!isInside(root, actual))
258
+ throw new Error('Editing files must remain in the selected workspace');
259
+ if (!exists) {
260
+ try {
261
+ await fs.lstat(file);
262
+ throw new Error('Output already exists');
263
+ }
264
+ catch (e) {
265
+ if (e.code !== 'ENOENT')
266
+ throw e;
267
+ }
268
+ }
269
+ return actual;
270
+ }
271
+ export async function findHostApplication(host) {
272
+ if (process.platform === 'darwin') {
273
+ const pattern = host === 'photoshop'
274
+ ? /(?:^|\s)Photoshop(?:\s|\.app)/i
275
+ : host === 'premiere'
276
+ ? /(?:^|\s)Premiere(?:\s+Pro)?(?:\s|\.app)/i
277
+ : /(?:^|\s)After\s+Effects(?:\s|\.app)/i;
278
+ for (const root of ['/Applications', path.join(os.homedir(), 'Applications')]) {
279
+ let entries;
280
+ try {
281
+ entries = await fs.readdir(root);
282
+ }
283
+ catch {
284
+ continue;
285
+ }
286
+ for (const name of entries.filter(n => pattern.test(n)).sort().reverse()) {
287
+ if (name.endsWith('.app'))
288
+ return path.join(root, name);
289
+ const parent = path.join(root, name);
290
+ try {
291
+ const bundle = (await fs.readdir(parent)).find(n => pattern.test(n) && n.endsWith('.app'));
292
+ if (bundle)
293
+ return path.join(parent, bundle);
294
+ }
295
+ catch { /* Not an application folder. */ }
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ if (process.platform === 'win32') {
301
+ const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
302
+ const folderPattern = host === 'photoshop' ? /Photoshop/i : host === 'premiere' ? /Premiere/i : /After Effects/i;
303
+ const exePattern = host === 'photoshop' ? /Photoshop\.exe$/i : host === 'premiere' ? /Premiere\.exe$/i : /AfterFX\.exe$/i;
304
+ try {
305
+ const topDirs = await fs.readdir(programFiles);
306
+ for (const dir of topDirs) {
307
+ const fullDir = path.join(programFiles, dir);
308
+ if (folderPattern.test(dir)) {
309
+ const files = await fs.readdir(fullDir);
310
+ const foundExe = files.find(f => exePattern.test(f));
311
+ if (foundExe)
312
+ return path.join(fullDir, foundExe);
313
+ }
314
+ }
315
+ }
316
+ catch { /* Ignore filesystem search failures */ }
317
+ }
318
+ return null;
319
+ }
320
+ export async function runStudioEdit(value, workspace = process.cwd(), signal) {
321
+ signal?.throwIfAborted();
322
+ const job = validateStudioJob(value);
323
+ const root = await fs.realpath(workspace);
324
+ job.output = await scoped(root, job.output, false);
325
+ if (job.input)
326
+ job.input = await scoped(root, job.input, true);
327
+ for (const action of job.actions) {
328
+ if (action.type === 'import')
329
+ action.path = await scoped(root, String(action.path), true);
330
+ }
331
+ const application = await findHostApplication(job.canonicalHost);
332
+ if (!application) {
333
+ return {
334
+ success: false,
335
+ error: `Creative host (${job.host}) is not installed at a supported location. No editing was attempted.`
336
+ };
337
+ }
338
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'causyne-studio-'));
339
+ try {
340
+ const scriptFile = path.join(dir, 'edit.jsx');
341
+ const receipt = path.join(dir, `${randomUUID()}.receipt`);
342
+ await fs.writeFile(scriptFile, studioScript(job, receipt), { mode: 0o600 });
343
+ if (process.platform === 'darwin') {
344
+ const quote = (s) => '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
345
+ const invocation = job.canonicalHost === 'photoshop'
346
+ ? `do javascript file (POSIX file ${quote(scriptFile)})`
347
+ : job.canonicalHost === 'premiere'
348
+ ? `try\n do javascript file (POSIX file ${quote(scriptFile)})\non error\n try\n DoScriptFile (POSIX file ${quote(scriptFile)})\n on error\n open (POSIX file ${quote(scriptFile)})\n end try\nend try`
349
+ : `DoScriptFile (POSIX file ${quote(scriptFile)})`;
350
+ await exec('/usr/bin/osascript', ['-e', `with timeout of 120 seconds\ntell application ${quote(application)}\n${invocation}\nend tell\nend timeout`], {
351
+ cwd: root, timeout: 125000, maxBuffer: 128 * 1024, ...(signal ? { signal } : {}), env: { PATH: '/usr/bin:/bin', HOME: os.homedir() }
352
+ });
353
+ }
354
+ else {
355
+ // Windows COM / Command invocation
356
+ await exec(application, ['-r', scriptFile], {
357
+ cwd: root, timeout: 125000, maxBuffer: 128 * 1024, ...(signal ? { signal } : {})
358
+ });
359
+ }
360
+ signal?.throwIfAborted();
361
+ if ((await fs.readFile(receipt, 'utf8')) !== 'CAUSYNE_EDIT_COMPLETE') {
362
+ throw new Error('No completion receipt from the creative host');
363
+ }
364
+ const output = await scoped(root, job.output, true);
365
+ const stat = await fs.stat(output);
366
+ if (!stat.isFile() || !stat.size) {
367
+ throw new Error('The creative host did not produce a nonempty output file');
368
+ }
369
+ return { success: true, output, bytes: stat.size, host: job.host };
370
+ }
371
+ finally {
372
+ await fs.rm(dir, { recursive: true, force: true });
373
+ }
374
+ }