#
tokens: 45465/50000 10/71 files (page 2/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 6. Use http://codebase.md/mikechambers/adb-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .gitignore
├── adb-proxy-socket
│   ├── .gitignore
│   ├── package-lock.json
│   ├── package.json
│   ├── proxy.js
│   └── README.md
├── cep
│   ├── com.mikechambers.ae
│   │   ├── .debug
│   │   ├── commands.js
│   │   ├── CSXS
│   │   │   └── manifest.xml
│   │   ├── index.html
│   │   ├── jsx
│   │   │   └── json-polyfill.jsx
│   │   ├── lib
│   │   │   └── CSInterface.js
│   │   ├── main.js
│   │   └── style.css
│   └── com.mikechambers.ai
│       ├── .debug
│       ├── commands.js
│       ├── CSXS
│       │   └── manifest.xml
│       ├── index.html
│       ├── jsx
│       │   ├── json-polyfill.jsx
│       │   └── utils.jsx
│       ├── lib
│       │   └── CSInterface.js
│       ├── main.js
│       └── style.css
├── dxt
│   ├── build
│   ├── pr
│   │   └── manifest.json
│   └── ps
│       └── manifest.json
├── images
│   └── claud-attach-mcp.png
├── LICENSE.md
├── mcp
│   ├── .gitignore
│   ├── ae-mcp.py
│   ├── ai-mcp.py
│   ├── core.py
│   ├── fonts.py
│   ├── id-mcp.py
│   ├── logger.py
│   ├── pr-mcp.py
│   ├── ps-batch-play.py
│   ├── ps-mcp.py
│   ├── pyproject.toml
│   ├── requirements.txt
│   ├── socket_client.py
│   └── uv.lock
├── package-lock.json
├── README.md
└── uxp
    ├── id
    │   ├── commands
    │   │   └── index.js
    │   ├── icons
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   └── [email protected]
    │   ├── index.html
    │   ├── LICENSE
    │   ├── main.js
    │   ├── manifest.json
    │   ├── package.json
    │   ├── socket.io.js
    │   └── style.css
    ├── pr
    │   ├── commands
    │   │   ├── consts.js
    │   │   ├── core.js
    │   │   ├── index.js
    │   │   └── utils.js
    │   ├── icons
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   └── [email protected]
    │   ├── index.html
    │   ├── LICENSE
    │   ├── main.js
    │   ├── manifest.json
    │   ├── package.json
    │   ├── socket.io.js
    │   └── style.css
    └── ps
        ├── commands
        │   ├── adjustment_layers.js
        │   ├── core.js
        │   ├── filters.js
        │   ├── index.js
        │   ├── layer_styles.js
        │   ├── layers.js
        │   ├── selection.js
        │   └── utils.js
        ├── icons
        │   ├── [email protected]
        │   ├── [email protected]
        │   ├── [email protected]
        │   └── [email protected]
        ├── index.html
        ├── LICENSE
        ├── main.js
        ├── manifest.json
        ├── package.json
        ├── socket.io.js
        └── style.css
```

# Files

--------------------------------------------------------------------------------
/mcp/ai-mcp.py:
--------------------------------------------------------------------------------

```python
  1 | # MIT License
  2 | #
  3 | # Copyright (c) 2025 Mike Chambers
  4 | #
  5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
  6 | # of this software and associated documentation files (the "Software"), to deal
  7 | # in the Software without restriction, including without limitation the rights
  8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 | # copies of the Software, and to permit persons to whom the Software is
 10 | # furnished to do so, subject to the following conditions:
 11 | #
 12 | # The above copyright notice and this permission notice shall be included in all
 13 | # copies or substantial portions of the Software.
 14 | #
 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 | # SOFTWARE.
 22 | 
 23 | from mcp.server.fastmcp import FastMCP
 24 | from core import init, sendCommand, createCommand
 25 | import socket_client
 26 | import sys
 27 | 
 28 | # Create an MCP server
 29 | mcp_name = "Adobe Illustrator MCP Server"
 30 | mcp = FastMCP(mcp_name, log_level="ERROR")
 31 | print(f"{mcp_name} running on stdio", file=sys.stderr)
 32 | 
 33 | APPLICATION = "illustrator"
 34 | PROXY_URL = 'http://localhost:3001'
 35 | PROXY_TIMEOUT = 20
 36 | 
 37 | socket_client.configure(
 38 |     app=APPLICATION, 
 39 |     url=PROXY_URL,
 40 |     timeout=PROXY_TIMEOUT
 41 | )
 42 | 
 43 | init(APPLICATION, socket_client)
 44 | 
 45 | @mcp.tool()
 46 | def get_documents():
 47 |     """
 48 |     Returns information about all currently open documents in Illustrator.
 49 | 
 50 |     """
 51 |     command = createCommand("getDocuments", {})
 52 |     return sendCommand(command)
 53 | 
 54 | @mcp.tool()
 55 | def get_active_document_info():
 56 |     """
 57 |     Returns information about the current active document.
 58 | 
 59 |     """
 60 |     command = createCommand("getActiveDocumentInfo", {})
 61 |     return sendCommand(command)
 62 | 
 63 | @mcp.tool()
 64 | def open_file(
 65 |     path: str
 66 | ):
 67 |     """
 68 |     Opens an Illustrator (.ai) file in Adobe Illustrator.
 69 |     
 70 |     Args:
 71 |         path (str): The absolute file path to the Illustrator file to open.
 72 |             Example: "/Users/username/Documents/my_artwork.ai"
 73 |     
 74 |     Returns:
 75 |         dict: Result containing:
 76 |             - success (bool): Whether the file was opened successfully
 77 |             - error (str): Error message if opening failed
 78 |     
 79 |     """
 80 |     
 81 |     command_params = {
 82 |         "path": path
 83 |     }
 84 |     
 85 |     command = createCommand("openFile", command_params)
 86 |     return sendCommand(command)
 87 | 
 88 | @mcp.tool()
 89 | def export_png(
 90 |     path: str,
 91 |     transparency: bool = True,
 92 |     anti_aliasing: bool = True,
 93 |     artboard_clipping: bool = True,
 94 |     horizontal_scale: int = 100,
 95 |     vertical_scale: int = 100,
 96 |     export_type: str = "PNG24",
 97 |     matte: bool = None,
 98 |     matte_color: dict = {"red": 255, "green": 255, "blue": 255}
 99 | ):
100 |     """
101 |     Exports the active Illustrator document as a PNG file.
102 |     
103 |     Args:
104 |         path (str): The absolute file path where the PNG will be saved.
105 |             Example: "/Users/username/Documents/my_export.png"
106 |         transparency (bool, optional): Enable/disable transparency. Defaults to True.
107 |         anti_aliasing (bool, optional): Enable/disable anti-aliasing for smooth edges. Defaults to True.
108 |         artboard_clipping (bool, optional): Clip export to artboard bounds. Defaults to True.
109 |         horizontal_scale (int, optional): Horizontal scale percentage (1-1000). Defaults to 100.
110 |         vertical_scale (int, optional): Vertical scale percentage (1-1000). Defaults to 100.
111 |         export_type (str, optional): PNG format type. "PNG24" (24-bit) or "PNG8" (8-bit). Defaults to "PNG24".
112 |         matte (bool, optional): Enable matte background color for transparency preview. 
113 |             If None, uses Illustrator's default behavior.
114 |         matte_color (dict, optional): RGB color for matte background. Defaults to {"red": 255, "green": 255, "blue": 255}.
115 |             Dict with keys "red", "green", "blue" with values 0-255.
116 |     
117 |     Returns:
118 |         dict: Export result containing:
119 |             - success (bool): Whether the export succeeded
120 |             - filePath (str): The actual file path where the PNG was saved
121 |             - fileExists (bool): Whether the exported file exists
122 |             - options (dict): The export options that were used
123 |             - documentName (str): Name of the exported document
124 |             - error (str): Error message if export failed
125 |     
126 |     Example:
127 |         # Basic PNG export
128 |         result = export_png("/Users/username/Desktop/my_artwork.png")
129 |         
130 |         # High-resolution export with transparency
131 |         result = export_png(
132 |             path="/Users/username/Desktop/high_res.png",
133 |             horizontal_scale=300,
134 |             vertical_scale=300,
135 |             transparency=True
136 |         )
137 |         
138 |         # PNG8 export with red matte background
139 |         result = export_png(
140 |             path="/Users/username/Desktop/small_file.png",
141 |             export_type="PNG8",
142 |             matte=True,
143 |             matte_color={"red": 255, "green": 0, "blue": 0}
144 |         )
145 |         
146 |         # Blue matte background
147 |         result = export_png(
148 |             path="/Users/username/Desktop/blue_bg.png",
149 |             matte=True,
150 |             matte_color={"red": 0, "green": 100, "blue": 255}
151 |         )
152 |     """
153 | 
154 | 
155 |     # Only include matte and matteColor if needed
156 |     command_params = {
157 |         "path": path,
158 |         "transparency": transparency,
159 |         "antiAliasing": anti_aliasing,
160 |         "artBoardClipping": artboard_clipping,
161 |         "horizontalScale": horizontal_scale,
162 |         "verticalScale": vertical_scale,
163 |         "exportType": export_type
164 |     }
165 | 
166 |     # Only include matte if explicitly set
167 |     if matte is not None:
168 |         command_params["matte"] = matte
169 |         
170 |     # Include matte color if matte is enabled or custom colors provided
171 |     if matte or matte_color != {"red": 255, "green": 255, "blue": 255}:
172 |         command_params["matteColor"] = matte_color
173 | 
174 |     command = createCommand("exportPNG", command_params)
175 |     return sendCommand(command)
176 | 
177 | 
178 | 
179 | @mcp.tool()
180 | def execute_extend_script(script_string: str):
181 |     """
182 |     Executes arbitrary ExtendScript code in Illustrator and returns the result.
183 |     
184 |     The script should use 'return' to send data back. The result will be automatically
185 |     JSON stringified. If the script throws an error, it will be caught and returned
186 |     as an error object.
187 |     
188 |     Args:
189 |         script_string (str): The ExtendScript code to execute. Must use 'return' to 
190 |                            send results back.
191 |     
192 |     Returns:
193 |         any: The result returned from the ExtendScript, or an error object containing:
194 |             - error (str): Error message
195 |             - line (str): Line number where error occurred
196 |     
197 |     Example:
198 |         script = '''
199 |             var comp = app.project.activeItem;
200 |             return {
201 |                 name: comp.name,
202 |                 layers: comp.numLayers
203 |             };
204 |         '''
205 |         result = execute_extend_script(script)
206 |     """
207 |     command = createCommand("executeExtendScript", {
208 |         "scriptString": script_string
209 |     })
210 |     return sendCommand(command)
211 | 
212 | @mcp.resource("config://get_instructions")
213 | def get_instructions() -> str:
214 |     """Read this first! Returns information and instructions on how to use Illustrator and this API"""
215 | 
216 |     return f"""
217 |     You are an Illustrator export who is creative and loves to help other people learn to use Illustrator.
218 | 
219 |     Rules to follow:
220 | 
221 |     1. Think deeply about how to solve the task
222 |     2. Always check your work before responding
223 |     3. Read the info for the API calls to make sure you understand the requirements and arguments
224 | 
225 |     """
226 | 
227 | 
228 | # Illustrator Blend Modes (for future use)
229 | BLEND_MODES = [
230 |     "ADD",
231 |     "ALPHA_ADD",
232 |     "CLASSIC_COLOR_BURN",
233 |     "CLASSIC_COLOR_DODGE",
234 |     "CLASSIC_DIFFERENCE",
235 |     "COLOR",
236 |     "COLOR_BURN",
237 |     "COLOR_DODGE",
238 |     "DANCING_DISSOLVE",
239 |     "DARKEN",
240 |     "DARKER_COLOR",
241 |     "DIFFERENCE",
242 |     "DISSOLVE",
243 |     "EXCLUSION",
244 |     "HARD_LIGHT",
245 |     "HARD_MIX",
246 |     "HUE",
247 |     "LIGHTEN",
248 |     "LIGHTER_COLOR",
249 |     "LINEAR_BURN",
250 |     "LINEAR_DODGE",
251 |     "LINEAR_LIGHT",
252 |     "LUMINESCENT_PREMUL",
253 |     "LUMINOSITY",
254 |     "MULTIPLY",
255 |     "NORMAL",
256 |     "OVERLAY",
257 |     "PIN_LIGHT",
258 |     "SATURATION",
259 |     "SCREEN",
260 |     "SILHOUETE_ALPHA",
261 |     "SILHOUETTE_LUMA",
262 |     "SOFT_LIGHT",
263 |     "STENCIL_ALPHA",
264 |     "STENCIL_LUMA",
265 |     "SUBTRACT",
266 |     "VIVID_LIGHT"
267 | ]
```

--------------------------------------------------------------------------------
/uxp/ps/commands/utils.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* MIT License
  2 |  *
  3 |  * Copyright (c) 2025 Mike Chambers
  4 |  *
  5 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
  6 |  * of this software and associated documentation files (the "Software"), to deal
  7 |  * in the Software without restriction, including without limitation the rights
  8 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 |  * copies of the Software, and to permit persons to whom the Software is
 10 |  * furnished to do so, subject to the following conditions:
 11 |  *
 12 |  * The above copyright notice and this permission notice shall be included in all
 13 |  * copies or substantial portions of the Software.
 14 |  *
 15 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 |  * SOFTWARE.
 22 |  */
 23 | 
 24 | const { app, constants, core } = require("photoshop");
 25 | const fs = require("uxp").storage.localFileSystem;
 26 | const openfs = require('fs')
 27 | 
 28 | 
 29 | const convertFontSize = (fontSize) => {
 30 |     return (app.activeDocument.resolution / 72) * fontSize
 31 | }
 32 | 
 33 | const convertFromPhotoshopFontSize = (photoshopFontSize) => {
 34 |     return photoshopFontSize / (app.activeDocument.resolution / 72);
 35 | }
 36 | 
 37 | const createFile = async (filePath) => {
 38 |     let url = `file:${filePath}`
 39 |     const fd = await openfs.open(url, "a+");
 40 |     await openfs.close(fd)
 41 | 
 42 |     return url
 43 | }
 44 | 
 45 | const parseColor = (color) => {
 46 |     try {
 47 |         const c = new app.SolidColor();
 48 |         c.rgb.red = color.red;
 49 |         c.rgb.green = color.green;
 50 |         c.rgb.blue = color.blue;
 51 | 
 52 |         return c;
 53 |     } catch (e) {
 54 |         throw new Error(`Invalid color values: ${JSON.stringify(color)}`);
 55 |     }
 56 | };
 57 | 
 58 | const getAlignmentMode = (mode) => {
 59 |     switch (mode) {
 60 |         case "LEFT":
 61 |             return "ADSLefts";
 62 |         case "CENTER_HORIZONTAL":
 63 |             return "ADSCentersH";
 64 |         case "RIGHT":
 65 |             return "ADSRights";
 66 |         case "TOP":
 67 |             return "ADSTops";
 68 |         case "CENTER_VERTICAL":
 69 |             return "ADSCentersV";
 70 |         case "BOTTOM":
 71 |             return "ADSBottoms";
 72 |         default:
 73 |             throw new Error(
 74 |                 `getAlignmentMode : Unknown alignment mode : ${mode}`
 75 |             );
 76 |     }
 77 | };
 78 | 
 79 | const getJustificationMode = (value) => {
 80 |     return getConstantValue(constants.Justification, value, "Justification");
 81 | };
 82 | 
 83 | const getBlendMode = (value) => {
 84 |     return getConstantValue(constants.BlendMode, value, "BlendMode");
 85 | };
 86 | 
 87 | const getInterpolationMethod = (value) => {
 88 |     return getConstantValue(
 89 |         constants.InterpolationMethod,
 90 |         value,
 91 |         "InterpolationMethod"
 92 |     );
 93 | };
 94 | 
 95 | const getAnchorPosition = (value) => {
 96 |     return getConstantValue(constants.AnchorPosition, value, "AnchorPosition");
 97 | };
 98 | 
 99 | const getNewDocumentMode = (value) => {
100 |     return getConstantValue(
101 |         constants.NewDocumentMode,
102 |         value,
103 |         "NewDocumentMode"
104 |     );
105 | };
106 | 
107 | const getConstantValue = (c, v, n) => {
108 |     let out = c[v.toUpperCase()];
109 | 
110 |     if (!out) {
111 |         throw new Error(`getConstantValue : Unknown constant value :${c} ${v}`);
112 |     }
113 | 
114 |     return out;
115 | };
116 | 
117 | const selectLayer = (layer, exclusive = false) => {
118 |     if (exclusive) {
119 |         clearLayerSelections();
120 |     }
121 | 
122 |     layer.selected = true;
123 | };
124 | 
125 | const clearLayerSelections = (layers) => {
126 |     if (!layers) {
127 |         layers = app.activeDocument.layers;
128 |     }
129 | 
130 |     for (const layer of layers) {
131 |         layer.selected = false;
132 | 
133 |         if (layer.layers && layer.layers.length > 0) {
134 |             clearLayerSelections(layer.layers);
135 |         }
136 |     }
137 | };
138 | 
139 | const setVisibleAllLayers = (visible, layers) => {
140 |     if (!layers) {
141 |         layers = app.activeDocument.layers;
142 |     }
143 | 
144 |     for (const layer of layers) {
145 |         layer.visible = visible
146 | 
147 |         if (layer.layers && layer.layers.length > 0) {
148 |             setVisibleAllLayers(visible, layer.layers)
149 |         }
150 |     }
151 | };
152 | 
153 | 
154 | const findLayer = (id, layers) => {
155 |     if (!layers) {
156 |         layers = app.activeDocument.layers;
157 |     }
158 | 
159 |     for (const layer of layers) {
160 |         if (layer.id === id) {
161 |             return layer;
162 |         }
163 | 
164 |         if (layer.layers && layer.layers.length > 0) {
165 |             const found = findLayer(id, layer.layers);
166 |             if (found) {
167 |                 return found; // Stop as soon as we’ve found the target layer
168 |             }
169 |         }
170 |     }
171 | 
172 |     return null;
173 | };
174 | 
175 | 
176 | const findLayerByName = (name, layers) => {
177 |     if (!layers) {
178 |         layers = app.activeDocument.layers;
179 |     }
180 | 
181 |     return app.activeDocument.layers.getByName(name);
182 | };
183 | 
184 | const _saveDocumentAs = async (filePath, fileType) => {
185 | 
186 |     let url = await createFile(filePath)
187 | 
188 |     let saveFile = await fs.getEntryWithUrl(url);
189 | 
190 |     return await execute(async () => {
191 | 
192 |         fileType = fileType.toUpperCase()
193 |         if (fileType == "JPG") {
194 |             await app.activeDocument.saveAs.jpg(saveFile, {
195 |                 quality:9
196 |             }, true)
197 |         } else if (fileType == "PNG") {
198 |             await app.activeDocument.saveAs.png(saveFile, {
199 |             }, true)
200 |         } else {
201 |             await app.activeDocument.saveAs.psd(saveFile, {
202 |                 alphaChannels:true,
203 |                 annotations:true,
204 |                 embedColorProfile:true,
205 |                 layers:true,
206 |                 maximizeCompatibility:true,
207 |                 spotColor:true,
208 |             }, true)
209 |         }
210 | 
211 |         return {savedFilePath:saveFile.nativePath}
212 |     });
213 | };
214 | 
215 | const execute = async (callback, commandName = "Executing command...") => {
216 |     try {
217 |         return await core.executeAsModal(callback, {
218 |             commandName: commandName,
219 |         });
220 |     } catch (e) {
221 |         throw new Error(`Error executing command [modal] : ${e}`);
222 |     }
223 | };
224 | 
225 | const tokenify = async (url) => {
226 |     let out = await fs.createSessionToken(
227 |         await fs.getEntryWithUrl("file:" + url)
228 |     );
229 |     return out;
230 | };
231 | 
232 | const getElementPlacement = (placement) => {
233 |     return constants.ElementPlacement[placement.toUpperCase()];
234 | };
235 | 
236 | const hasActiveSelection = () => {
237 |     return app.activeDocument.selection.bounds != null;
238 | };
239 | 
240 | const getMostRecentlyModifiedFile = async (directoryPath)  => {
241 |     try {
242 |       // Get directory contents
243 |       const dirEntries = await openfs.readdir(directoryPath);
244 |       
245 |       const fileDetails = [];
246 |       
247 |       // Process each file
248 |       let i = 0
249 |       for (const entry of dirEntries) {
250 |         console.log(i++)
251 |         const filePath = window.path.join(directoryPath, entry);
252 |         
253 |         // Get file stats using lstat
254 |         try {
255 |           const stats = await openfs.lstat(filePath);
256 | 
257 |           // Skip if it's a directory
258 |           if (stats.isDirectory()) {
259 |             continue;
260 |           }
261 |           
262 |           fileDetails.push({
263 |             name: entry,
264 |             path: filePath,
265 |             modifiedTime: stats.mtime,  // Date object
266 |             modifiedTimestamp: stats.mtimeMs  // Use mtimeMs directly instead of getTime()
267 |           });
268 |         } catch (err) {
269 |           console.log(`Error getting stats for ${filePath}:`, err);
270 |           // Continue to next file if there's an error with this one
271 |           continue;
272 |         }
273 |       }
274 |       
275 |       if (fileDetails.length === 0) {
276 |         return null;
277 |       }
278 |       
279 |       // Sort by modification timestamp (newest first)
280 |       fileDetails.sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp);
281 |       
282 |       // Return the most recently modified file
283 |       return fileDetails[0];
284 |     } catch (err) {
285 |       console.error('Error getting most recently modified file:', err);
286 |       return null;
287 |     }
288 |   }
289 | 
290 |   const fileExists = async (filePath) => {
291 |     try {
292 |       await openfs.lstat(`file:${filePath}`);
293 |       return true;
294 |     } catch (error) {
295 |         return false;
296 |     }
297 |   }
298 | 
299 |   const generateDocumentInfo = (document, activeDocument) => {
300 |     return {
301 |             name:document.name,
302 |             id:document.id,
303 |             isActive: document === activeDocument,
304 |             path:document.path,
305 |             saved:document.saved,
306 |             title:document.title
307 |         };
308 | }
309 | 
310 | const listOpenDocuments = () => {
311 |     const docs = app.documents;
312 |     const activeDocument = app.activeDocument
313 | 
314 |     let out = []
315 | 
316 |     for (let doc of docs) {
317 |         let d = generateDocumentInfo(doc, activeDocument)
318 |         out.push(d)
319 |     }
320 | 
321 |     return out
322 | }
323 | 
324 | module.exports = {
325 |     findLayerByName,
326 |     generateDocumentInfo,
327 |     listOpenDocuments,
328 |     convertFromPhotoshopFontSize,
329 |     convertFontSize,
330 |     setVisibleAllLayers,
331 |     _saveDocumentAs,
332 |     getMostRecentlyModifiedFile,
333 |     fileExists,
334 |     createFile,
335 |     parseColor,
336 |     getAlignmentMode,
337 |     getJustificationMode,
338 |     getBlendMode,
339 |     getInterpolationMethod,
340 |     getAnchorPosition,
341 |     getNewDocumentMode,
342 |     getConstantValue,
343 |     selectLayer,
344 |     clearLayerSelections,
345 |     findLayer,
346 |     execute,
347 |     tokenify,
348 |     getElementPlacement,
349 |     hasActiveSelection
350 | }
```

--------------------------------------------------------------------------------
/uxp/ps/commands/selection.js:
--------------------------------------------------------------------------------

```javascript
  1 | const { app, constants, action } = require("photoshop");
  2 | const { 
  3 |     findLayer, 
  4 |     execute, 
  5 |     parseColor, 
  6 |     selectLayer 
  7 | } = require("./utils");
  8 | 
  9 | const {hasActiveSelection} = require("./utils")
 10 | 
 11 | const clearSelection = async () => {
 12 |     await app.activeDocument.selection.selectRectangle(
 13 |         { top: 0, left: 0, bottom: 0, right: 0 },
 14 |         constants.SelectionType.REPLACE,
 15 |         0,
 16 |         true
 17 |     );
 18 | };
 19 | 
 20 | const createMaskFromSelection = async (command) => {
 21 | 
 22 |     let options = command.options;
 23 |     let layerId = options.layerId;
 24 | 
 25 |     let layer = findLayer(layerId);
 26 | 
 27 |     if (!layer) {
 28 |         throw new Error(
 29 |             `createMaskFromSelection : Could not find layerId : ${layerId}`
 30 |         );
 31 |     }
 32 | 
 33 |     await execute(async () => {
 34 |         selectLayer(layer, true);
 35 | 
 36 |         let commands = [
 37 |             {
 38 |                 _obj: "make",
 39 |                 at: {
 40 |                     _enum: "channel",
 41 |                     _ref: "channel",
 42 |                     _value: "mask",
 43 |                 },
 44 |                 new: {
 45 |                     _class: "channel",
 46 |                 },
 47 |                 using: {
 48 |                     _enum: "userMaskEnabled",
 49 |                     _value: "revealSelection",
 50 |                 },
 51 |             },
 52 |         ];
 53 | 
 54 |         await action.batchPlay(commands, {});
 55 |     });
 56 | };
 57 | 
 58 | const selectSubject = async (command) => {
 59 | 
 60 |     let options = command.options;
 61 |     let layerId = options.layerId;
 62 | 
 63 |     let layer = findLayer(layerId);
 64 | 
 65 |     if (!layer) {
 66 |         throw new Error(
 67 |             `selectSubject : Could not find layerId : ${layerId}`
 68 |         );
 69 |     }
 70 | 
 71 |     return await execute(async () => {
 72 |         selectLayer(layer, true);
 73 | 
 74 |         let commands = [
 75 |             // Select Subject
 76 |             {
 77 |                 _obj: "autoCutout",
 78 |                 sampleAllLayers: false,
 79 |             },
 80 |         ];
 81 | 
 82 |         await action.batchPlay(commands, {});
 83 |     });
 84 | };
 85 | 
 86 | const selectSky = async (command) => {
 87 | 
 88 |     let options = command.options;
 89 |     let layerId = options.layerId;
 90 | 
 91 |     let layer = findLayer(layerId);
 92 | 
 93 |     if (!layer) {
 94 |         throw new Error(`selectSky : Could not find layerId : ${layerId}`);
 95 |     }
 96 | 
 97 |     return await execute(async () => {
 98 |         selectLayer(layer, true);
 99 | 
100 |         let commands = [
101 |             // Select Sky
102 |             {
103 |                 _obj: "selectSky",
104 |                 sampleAllLayers: false,
105 |             },
106 |         ];
107 | 
108 |         await action.batchPlay(commands, {});
109 | 
110 |     });
111 | };
112 | 
113 | const cutSelectionToClipboard = async (command) => {
114 | 
115 |     let options = command.options;
116 |     let layerId = options.layerId;
117 | 
118 |     let layer = findLayer(layerId);
119 | 
120 |     if (!layer) {
121 |         throw new Error(
122 |             `cutSelectionToClipboard : Could not find layerId : ${layerId}`
123 |         );
124 |     }
125 | 
126 |     if (!hasActiveSelection()) {
127 |         throw new Error(
128 |             "cutSelectionToClipboard : Requires an active selection"
129 |         );
130 |     }
131 | 
132 |     return await execute(async () => {
133 |         selectLayer(layer, true);
134 | 
135 |         let commands = [
136 |             {
137 |                 _obj: "cut",
138 |             },
139 |         ];
140 | 
141 |         await action.batchPlay(commands, {});
142 |     });
143 | };
144 | 
145 | const copyMergedSelectionToClipboard = async (command) => {
146 | 
147 |     let options = command.options;
148 | 
149 |     if (!hasActiveSelection()) {
150 |         throw new Error(
151 |             "copySelectionToClipboard : Requires an active selection"
152 |         );
153 |     }
154 | 
155 |     return await execute(async () => {
156 |         let commands = [{
157 |             _obj: "copyMerged",
158 |         }];
159 | 
160 |         await action.batchPlay(commands, {});
161 |     });
162 | };
163 | 
164 | const copySelectionToClipboard = async (command) => {
165 | 
166 |     let options = command.options;
167 |     let layerId = options.layerId;
168 | 
169 |     let layer = findLayer(layerId);
170 | 
171 |     if (!layer) {
172 |         throw new Error(
173 |             `copySelectionToClipboard : Could not find layerId : ${layerId}`
174 |         );
175 |     }
176 | 
177 |     if (!hasActiveSelection()) {
178 |         throw new Error(
179 |             "copySelectionToClipboard : Requires an active selection"
180 |         );
181 |     }
182 | 
183 |     return await execute(async () => {
184 |         selectLayer(layer, true);
185 | 
186 |         let commands = [{
187 |             _obj: "copyEvent",
188 |             copyHint: "pixels",
189 |         }];
190 | 
191 |         await action.batchPlay(commands, {});
192 |     });
193 | };
194 | 
195 | const pasteFromClipboard = async (command) => {
196 | 
197 |     let options = command.options;
198 |     let layerId = options.layerId;
199 | 
200 |     let layer = findLayer(layerId);
201 | 
202 |     if (!layer) {
203 |         throw new Error(
204 |             `pasteFromClipboard : Could not find layerId : ${layerId}`
205 |         );
206 |     }
207 | 
208 |     return await execute(async () => {
209 |         selectLayer(layer, true);
210 | 
211 |         let pasteInPlace = options.pasteInPlace;
212 | 
213 |         let commands = [
214 |             {
215 |                 _obj: "paste",
216 |                 antiAlias: {
217 |                     _enum: "antiAliasType",
218 |                     _value: "antiAliasNone",
219 |                 },
220 |                 as: {
221 |                     _class: "pixel",
222 |                 },
223 |                 inPlace: pasteInPlace,
224 |             },
225 |         ];
226 | 
227 |         await action.batchPlay(commands, {});
228 |     });
229 | };
230 | 
231 | const deleteSelection = async (command) => {
232 | 
233 |     let options = command.options;
234 |     let layerId = options.layerId;
235 |     let layer = findLayer(layerId);
236 | 
237 |     if (!layer) {
238 |         throw new Error(
239 |             `deleteSelection : Could not find layerId : ${layerId}`
240 |         );
241 |     }
242 | 
243 |     if (!app.activeDocument.selection.bounds) {
244 |         throw new Error(`invertSelection : Requires an active selection`);
245 |     }
246 | 
247 |     await execute(async () => {
248 |         selectLayer(layer, true);
249 |         let commands = [
250 |             {
251 |                 _obj: "delete",
252 |             },
253 |         ];
254 |         await action.batchPlay(commands, {});
255 |     });
256 | };
257 | 
258 | const fillSelection = async (command) => {
259 | 
260 |     let options = command.options;
261 |     let layerId = options.layerId;
262 |     let layer = findLayer(layerId);
263 | 
264 |     if (!layer) {
265 |         throw new Error(
266 |             `fillSelection : Could not find layerId : ${layerId}`
267 |         );
268 |     }
269 | 
270 |     if (!app.activeDocument.selection.bounds) {
271 |         throw new Error(`invertSelection : Requires an active selection`);
272 |     }
273 | 
274 |     await execute(async () => {
275 |         selectLayer(layer, true);
276 | 
277 |         let c = parseColor(options.color).rgb;
278 |         let commands = [
279 |             // Fill
280 |             {
281 |                 _obj: "fill",
282 |                 color: {
283 |                     _obj: "RGBColor",
284 |                     blue: c.blue,
285 |                     grain: c.green,
286 |                     red: c.red,
287 |                 },
288 |                 mode: {
289 |                     _enum: "blendMode",
290 |                     _value: options.blendMode.toLowerCase(),
291 |                 },
292 |                 opacity: {
293 |                     _unit: "percentUnit",
294 |                     _value: options.opacity,
295 |                 },
296 |                 using: {
297 |                     _enum: "fillContents",
298 |                     _value: "color",
299 |                 },
300 |             },
301 |         ];
302 |         await action.batchPlay(commands, {});
303 |     });
304 | };
305 | 
306 | const selectPolygon = async (command) => {
307 | 
308 |     let options = command.options;
309 |     let layerId = options.layerId;
310 |     let layer = findLayer(layerId);
311 | 
312 |     if (!layer) {
313 |         throw new Error(
314 |             `selectPolygon : Could not find layerId : ${layerId}`
315 |         );
316 |     }
317 | 
318 |     await execute(async () => {
319 | 
320 |         selectLayer(layer, true);
321 | 
322 |         await app.activeDocument.selection.selectPolygon(
323 |             options.points,
324 |             constants.SelectionType.REPLACE,
325 |             options.feather,
326 |             options.antiAlias
327 |         );
328 |     });
329 | };
330 | 
331 | let selectEllipse = async (command) => {
332 | 
333 |     let options = command.options;
334 |     let layerId = options.layerId;
335 |     let layer = findLayer(layerId);
336 | 
337 |     if (!layer) {
338 |         throw new Error(
339 |             `selectEllipse : Could not find layerId : ${layerId}`
340 |         );
341 |     }
342 | 
343 |     await execute(async () => {
344 | 
345 |         selectLayer(layer, true);
346 | 
347 |         await app.activeDocument.selection.selectEllipse(
348 |             options.bounds,
349 |             constants.SelectionType.REPLACE,
350 |             options.feather,
351 |             options.antiAlias
352 |         );
353 |     });
354 | };
355 | 
356 | const selectRectangle = async (command) => {
357 |     let options = command.options;
358 |     let layerId = options.layerId;
359 |     let layer = findLayer(layerId);
360 | 
361 |     if (!layer) {
362 |         throw new Error(
363 |             `selectRectangle : Could not find layerId : ${layerId}`
364 |         );
365 |     }
366 | 
367 |     await execute(async () => {
368 |         selectLayer(layer, true);
369 | 
370 |         await app.activeDocument.selection.selectRectangle(
371 |             options.bounds,
372 |             constants.SelectionType.REPLACE,
373 |             options.feather,
374 |             options.antiAlias
375 |         );
376 |     });
377 | };
378 | 
379 | const invertSelection = async (command) => {
380 | 
381 |     if (!app.activeDocument.selection.bounds) {
382 |         throw new Error(`invertSelection : Requires an active selection`);
383 |     }
384 | 
385 |     await execute(async () => {
386 |         let commands = [
387 |             {
388 |                 _obj: "inverse",
389 |             },
390 |         ];
391 |         await action.batchPlay(commands, {});
392 |     });
393 | };
394 | 
395 | const commandHandlers = {
396 |     clearSelection,
397 |     createMaskFromSelection,
398 |     selectSubject,
399 |     selectSky,
400 |     cutSelectionToClipboard,
401 |     copyMergedSelectionToClipboard,
402 |     copySelectionToClipboard,
403 |     pasteFromClipboard,
404 |     deleteSelection,
405 |     fillSelection,
406 |     selectPolygon,
407 |     selectEllipse,
408 |     selectRectangle,
409 |     invertSelection
410 | };
411 | 
412 | module.exports = {
413 |     commandHandlers
414 | };
```

--------------------------------------------------------------------------------
/uxp/ps/commands/layer_styles.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* MIT License
  2 |  *
  3 |  * Copyright (c) 2025 Mike Chambers
  4 |  *
  5 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
  6 |  * of this software and associated documentation files (the "Software"), to deal
  7 |  * in the Software without restriction, including without limitation the rights
  8 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 |  * copies of the Software, and to permit persons to whom the Software is
 10 |  * furnished to do so, subject to the following conditions:
 11 |  *
 12 |  * The above copyright notice and this permission notice shall be included in all
 13 |  * copies or substantial portions of the Software.
 14 |  *
 15 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 |  * SOFTWARE.
 22 |  */
 23 | 
 24 | const { action } = require("photoshop");
 25 | 
 26 | const {
 27 |     selectLayer,
 28 |     findLayer,
 29 |     execute
 30 | } = require("./utils")
 31 | 
 32 | const addDropShadowLayerStyle = async (command) => {
 33 | 
 34 |     let options = command.options;
 35 |     let layerId = options.layerId;
 36 | 
 37 |     let layer = findLayer(layerId);
 38 | 
 39 |     if (!layer) {
 40 |         throw new Error(
 41 |             `addDropShadowLayerStyle : Could not find layerId : ${layerId}`
 42 |         );
 43 |     }
 44 | 
 45 |     await execute(async () => {
 46 |         selectLayer(layer, true);
 47 | 
 48 |         let commands = [
 49 |             // Set Layer Styles of current layer
 50 |             {
 51 |                 _obj: "set",
 52 |                 _target: [
 53 |                     {
 54 |                         _property: "layerEffects",
 55 |                         _ref: "property",
 56 |                     },
 57 |                     {
 58 |                         _enum: "ordinal",
 59 |                         _ref: "layer",
 60 |                         _value: "targetEnum",
 61 |                     },
 62 |                 ],
 63 |                 to: {
 64 |                     _obj: "layerEffects",
 65 |                     dropShadow: {
 66 |                         _obj: "dropShadow",
 67 |                         antiAlias: false,
 68 |                         blur: {
 69 |                             _unit: "pixelsUnit",
 70 |                             _value: options.size,
 71 |                         },
 72 |                         chokeMatte: {
 73 |                             _unit: "pixelsUnit",
 74 |                             _value: options.spread,
 75 |                         },
 76 |                         color: {
 77 |                             _obj: "RGBColor",
 78 |                             blue: options.color.blue,
 79 |                             grain: options.color.green,
 80 |                             red: options.color.red,
 81 |                         },
 82 |                         distance: {
 83 |                             _unit: "pixelsUnit",
 84 |                             _value: options.distance,
 85 |                         },
 86 |                         enabled: true,
 87 |                         layerConceals: true,
 88 |                         localLightingAngle: {
 89 |                             _unit: "angleUnit",
 90 |                             _value: options.angle,
 91 |                         },
 92 |                         mode: {
 93 |                             _enum: "blendMode",
 94 |                             _value: options.blendMode.toLowerCase(),
 95 |                         },
 96 |                         noise: {
 97 |                             _unit: "percentUnit",
 98 |                             _value: 0.0,
 99 |                         },
100 |                         opacity: {
101 |                             _unit: "percentUnit",
102 |                             _value: options.opacity,
103 |                         },
104 |                         present: true,
105 |                         showInDialog: true,
106 |                         transferSpec: {
107 |                             _obj: "shapeCurveType",
108 |                             name: "Linear",
109 |                         },
110 |                         useGlobalAngle: true,
111 |                     },
112 |                     globalLightingAngle: {
113 |                         _unit: "angleUnit",
114 |                         _value: options.angle,
115 |                     },
116 |                     scale: {
117 |                         _unit: "percentUnit",
118 |                         _value: 100.0,
119 |                     },
120 |                 },
121 |             },
122 |         ];
123 | 
124 |         await action.batchPlay(commands, {});
125 |     });
126 | };
127 | 
128 | const addStrokeLayerStyle = async (command) => {
129 |     const options = command.options
130 | 
131 |     const layerId = options.layerId
132 | 
133 |     let layer = findLayer(layerId)
134 | 
135 |     if (!layer) {
136 |         throw new Error(
137 |             `addStrokeLayerStyle : Could not find layerId : ${layerId}`
138 |         );
139 |     }
140 | 
141 |     let position = "centeredFrame"
142 | 
143 |     if (options.position == "INSIDE") {
144 |         position = "insetFrame"
145 |     } else if (options.position == "OUTSIDE") {
146 |         position = "outsetFrame"
147 |     }
148 | 
149 | 
150 |     await execute(async () => {
151 |         selectLayer(layer, true);
152 | 
153 |         let strokeColor = options.color
154 |         let commands = [
155 |             // Set Layer Styles of current layer
156 |             {
157 |                 "_obj": "set",
158 |                 "_target": [
159 |                     {
160 |                         "_property": "layerEffects",
161 |                         "_ref": "property"
162 |                     },
163 |                     {
164 |                         "_enum": "ordinal",
165 |                         "_ref": "layer",
166 |                         "_value": "targetEnum"
167 |                     }
168 |                 ],
169 |                 "to": {
170 |                     "_obj": "layerEffects",
171 |                     "frameFX": {
172 |                         "_obj": "frameFX",
173 |                         "color": {
174 |                             "_obj": "RGBColor",
175 |                             "blue": strokeColor.blue,
176 |                             "grain": strokeColor.green,
177 |                             "red": strokeColor.red
178 |                         },
179 |                         "enabled": true,
180 |                         "mode": {
181 |                             "_enum": "blendMode",
182 |                             "_value": options.blendMode.toLowerCase()
183 |                         },
184 |                         "opacity": {
185 |                             "_unit": "percentUnit",
186 |                             "_value": options.opacity
187 |                         },
188 |                         "overprint": false,
189 |                         "paintType": {
190 |                             "_enum": "frameFill",
191 |                             "_value": "solidColor"
192 |                         },
193 |                         "present": true,
194 |                         "showInDialog": true,
195 |                         "size": {
196 |                             "_unit": "pixelsUnit",
197 |                             "_value": options.size
198 |                         },
199 |                         "style": {
200 |                             "_enum": "frameStyle",
201 |                             "_value": position
202 |                         }
203 |                     },
204 |                     "scale": {
205 |                         "_unit": "percentUnit",
206 |                         "_value": 100.0
207 |                     }
208 |                 }
209 |             }
210 |         ];
211 | 
212 |         await action.batchPlay(commands, {});
213 |     });
214 | }
215 | 
216 | const createGradientLayerStyle = async (command) => {
217 | 
218 |     let options = command.options;
219 |     let layerId = options.layerId;
220 | 
221 |     let layer = findLayer(layerId);
222 | 
223 |     if (!layer) {
224 |         throw new Error(
225 |             `createGradientAdjustmentLayer : Could not find layerId : ${layerId}`
226 |         );
227 |     }
228 | 
229 |     await execute(async () => {
230 |         selectLayer(layer, true);
231 | 
232 |         let angle = options.angle;
233 |         let colorStops = options.colorStops;
234 |         let opacityStops = options.opacityStops;
235 | 
236 |         let colors = [];
237 |         for (let c of colorStops) {
238 |             colors.push({
239 |                 _obj: "colorStop",
240 |                 color: {
241 |                     _obj: "RGBColor",
242 |                     blue: c.color.blue,
243 |                     grain: c.color.green,
244 |                     red: c.color.red,
245 |                 },
246 |                 location: Math.round((c.location / 100) * 4096),
247 |                 midpoint: c.midpoint,
248 |                 type: {
249 |                     _enum: "colorStopType",
250 |                     _value: "userStop",
251 |                 },
252 |             });
253 |         }
254 | 
255 |         let opacities = [];
256 |         for (let o of opacityStops) {
257 |             opacities.push({
258 |                 _obj: "transferSpec",
259 |                 location: Math.round((o.location / 100) * 4096),
260 |                 midpoint: o.midpoint,
261 |                 opacity: {
262 |                     _unit: "percentUnit",
263 |                     _value: o.opacity,
264 |                 },
265 |             });
266 |         }
267 | 
268 |         let commands = [
269 |             // Make fill layer
270 |             {
271 |                 _obj: "make",
272 |                 _target: [
273 |                     {
274 |                         _ref: "contentLayer",
275 |                     },
276 |                 ],
277 |                 using: {
278 |                     _obj: "contentLayer",
279 |                     type: {
280 |                         _obj: "gradientLayer",
281 |                         angle: {
282 |                             _unit: "angleUnit",
283 |                             _value: angle,
284 |                         },
285 |                         gradient: {
286 |                             _obj: "gradientClassEvent",
287 |                             colors: colors,
288 |                             gradientForm: {
289 |                                 _enum: "gradientForm",
290 |                                 _value: "customStops",
291 |                             },
292 |                             interfaceIconFrameDimmed: 4096.0,
293 |                             name: "Custom",
294 |                             transparency: opacities,
295 |                         },
296 |                         gradientsInterpolationMethod: {
297 |                             _enum: "gradientInterpolationMethodType",
298 |                             _value: "smooth",
299 |                         },
300 |                         type: {
301 |                             _enum: "gradientType",
302 |                             _value: options.type.toLowerCase(),
303 |                         },
304 |                     },
305 |                 },
306 |             },
307 |         ];
308 | 
309 |         await action.batchPlay(commands, {});
310 |     });
311 | };
312 | 
313 | 
314 | 
315 | const commandHandlers = {
316 |     createGradientLayerStyle,
317 |     addStrokeLayerStyle,
318 |     addDropShadowLayerStyle
319 | };
320 | 
321 | module.exports = {
322 |     commandHandlers
323 | };
```

--------------------------------------------------------------------------------
/uxp/pr/commands/utils.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* MIT License
  2 |  *
  3 |  * Copyright (c) 2025 Mike Chambers
  4 |  *
  5 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
  6 |  * of this software and associated documentation files (the "Software"), to deal
  7 |  * in the Software without restriction, including without limitation the rights
  8 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 |  * copies of the Software, and to permit persons to whom the Software is
 10 |  * furnished to do so, subject to the following conditions:
 11 |  *
 12 |  * The above copyright notice and this permission notice shall be included in all
 13 |  * copies or substantial portions of the Software.
 14 |  *
 15 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 |  * SOFTWARE.
 22 |  */
 23 | 
 24 | const app = require("premierepro");
 25 | const { TRACK_TYPE, TICKS_PER_SECOND } = require("./consts.js");
 26 | 
 27 | const _getSequenceFromId = async (id) => {
 28 |     let project = await app.Project.getActiveProject();
 29 | 
 30 |     let guid = app.Guid.fromString(id);
 31 |     let sequence = await project.getSequence(guid);
 32 | 
 33 |     if (!sequence) {
 34 |         throw new Error(
 35 |             `_getSequenceFromId : Could not find sequence with id : ${id}`
 36 |         );
 37 |     }
 38 | 
 39 |     return sequence;
 40 | };
 41 | 
 42 | const _setActiveSequence = async (sequence) => {
 43 |     let project = await app.Project.getActiveProject();
 44 |     await project.setActiveSequence(sequence);
 45 | 
 46 |     let item = await findProjectItem(sequence.name, project);
 47 |     await app.SourceMonitor.openProjectItem(item);
 48 | };
 49 | 
 50 | const setParam = async (trackItem, componentName, paramName, value) => {
 51 |     const project = await app.Project.getActiveProject();
 52 | 
 53 |     let param = await getParam(trackItem, componentName, paramName);
 54 | 
 55 |     let keyframe = await param.createKeyframe(value);
 56 | 
 57 |     execute(() => {
 58 |         let action = param.createSetValueAction(keyframe);
 59 |         return [action];
 60 |     }, project);
 61 | };
 62 | 
 63 | const getParam = async (trackItem, componentName, paramName) => {
 64 |     let components = await trackItem.getComponentChain();
 65 | 
 66 |     const count = components.getComponentCount();
 67 |     for (let i = 0; i < count; i++) {
 68 |         const component = components.getComponentAtIndex(i);
 69 | 
 70 |         //search for match name
 71 |         //component name AE.ADBE Opacity
 72 |         const matchName = await component.getMatchName();
 73 | 
 74 |         if (matchName == componentName) {
 75 |             console.log(matchName);
 76 |             let pCount = component.getParamCount();
 77 | 
 78 |             for (let j = 0; j < pCount; j++) {
 79 |                 const param = component.getParam(j);
 80 | 
 81 |                 console.log(param.type);
 82 |                 console.log(param);
 83 |                 if (param.displayName == paramName) {
 84 |                     return param;
 85 |                 }
 86 |             }
 87 |         }
 88 |     }
 89 | };
 90 | 
 91 | const addEffect = async (trackItem, effectName) => {
 92 |     let project = await app.Project.getActiveProject();
 93 |     const effect = await app.VideoFilterFactory.createComponent(effectName);
 94 | 
 95 |     let componentChain = await trackItem.getComponentChain();
 96 | 
 97 |     execute(() => {
 98 |         let action = componentChain.createAppendComponentAction(effect, 0); //todo, second isnt needed
 99 |         return [action];
100 |     }, project);
101 | };
102 | 
103 | /*
104 | const findProjectItem2 = async (itemName, project) => {
105 |     let root = await project.getRootItem();
106 |     let rootItems = await root.getItems();
107 | 
108 |     let insertItem;
109 |     for (const item of rootItems) {
110 |         if (item.name == itemName) {
111 |             insertItem = item;
112 |             break;
113 |         }
114 |     }
115 | 
116 |     if (!insertItem) {
117 |         throw new Error(
118 |             `addItemToSequence : Could not find item named ${itemName}`
119 |         );
120 |     }
121 | 
122 |     return insertItem;
123 | };
124 | */
125 | 
126 | const findProjectItem = async (itemName, project) => {
127 |     let root = await project.getRootItem();
128 |     
129 |     const searchItems = async (parentItem) => {
130 |         let items = await parentItem.getItems();
131 |         
132 |         // First, check items at this level
133 |         for (const item of items) {
134 |             if (item.name === itemName) {
135 |                 return item;
136 |             }
137 |         }
138 |         
139 |         // If not found, search recursively in bins/folders
140 |         for (const item of items) {
141 |             const folderItem = app.FolderItem.cast(item);
142 |             if (folderItem) {
143 |                 // This is a bin/folder, search inside it
144 |                 const foundItem = await searchItems(folderItem);
145 |                 if (foundItem) {
146 |                     return foundItem;
147 |                 }
148 |             }
149 |         }
150 |         
151 |         return null; // Not found at this level or in any sub-folders
152 |     };
153 |     
154 |     const insertItem = await searchItems(root);
155 |     
156 |     if (!insertItem) {
157 |         throw new Error(
158 |             `addItemToSequence : Could not find item named ${itemName}`
159 |         );
160 |     }
161 | 
162 |     return insertItem;
163 | };
164 | 
165 | 
166 | const execute = (getActions, project) => {
167 |     try {
168 |         project.lockedAccess(() => {
169 |             project.executeTransaction((compoundAction) => {
170 |                 let actions = getActions();
171 | 
172 |                 for (const a of actions) {
173 |                     compoundAction.addAction(a);
174 |                 }
175 |             });
176 |         });
177 |     } catch (e) {
178 |         throw new Error(`Error executing locked transaction : ${e}`);
179 |     }
180 | };
181 | 
182 | const getTracks = async (sequence, trackType) => {
183 |     let count;
184 | 
185 |     if (trackType === TRACK_TYPE.VIDEO) {
186 |         count = await sequence.getVideoTrackCount();
187 |     } else if (trackType === TRACK_TYPE.AUDIO) {
188 |         count = await sequence.getAudioTrackCount();
189 |     }
190 | 
191 |     let tracks = [];
192 |     for (let i = 0; i < count; i++) {
193 |         let track;
194 | 
195 |         if (trackType === TRACK_TYPE.VIDEO) {
196 |             track = await sequence.getVideoTrack(i);
197 |         } else if (trackType === TRACK_TYPE.AUDIO) {
198 |             track = await sequence.getAudioTrack(i);
199 |         }
200 | 
201 |         let out = {
202 |             index: i,
203 |             tracks: [],
204 |         };
205 | 
206 |         let clips = await track.getTrackItems(1, false);
207 | 
208 |         if (clips.length === 0) {
209 |             continue;
210 |         }
211 | 
212 |         let k = 0;
213 |         for (const c of clips) {
214 |             let startTimeTicks = (await c.getStartTime()).ticks;
215 |             let endTimeTicks = (await c.getEndTime()).ticks;
216 |             let durationTicks = (await c.getDuration()).ticks;
217 |             let durationSeconds = (await c.getDuration()).seconds;
218 |             let name = (await c.getProjectItem()).name;
219 |             let type = await c.getType();
220 |             let index = k++;
221 | 
222 |             out.tracks.push({
223 |                 startTimeTicks,
224 |                 endTimeTicks,
225 |                 durationTicks,
226 |                 durationSeconds,
227 |                 name,
228 |                 type,
229 |                 index,
230 |             });
231 |         }
232 | 
233 |         tracks.push(out);
234 |     }
235 |     return tracks;
236 | };
237 | 
238 | const getSequences = async () => {
239 |     let project = await app.Project.getActiveProject();
240 |     let active = await project.getActiveSequence();
241 | 
242 |     let sequences = await project.getSequences();
243 | 
244 |     let out = [];
245 |     for (const sequence of sequences) {
246 |         let size = await sequence.getFrameSize();
247 |         //let settings = await sequence.getSettings()
248 | 
249 |         //let projectItem = await sequence.getProjectItem()
250 |         //let name = projectItem.name
251 |         let name = sequence.name;
252 |         let id = sequence.guid.toString();
253 | 
254 |         let videoTracks = await getTracks(sequence,TRACK_TYPE.VIDEO);
255 |         let audioTracks = await getTracks(sequence, TRACK_TYPE.AUDIO);
256 | 
257 |         let isActive = active == sequence;
258 | 
259 | 
260 |         let timebase = await sequence.getTimebase()
261 |         let fps = TICKS_PER_SECOND / timebase
262 | 
263 |         let endTime = await sequence.getEndTime()
264 |         let durationSeconds = await endTime.seconds
265 |         let durationTicks = await endTime.ticksNumber
266 |         let ticksPerSecond = TICKS_PER_SECOND
267 | 
268 |         out.push({
269 |             isActive,
270 |             name,
271 |             id,
272 |             frameSize: { width: size.width, height: size.height },
273 |             videoTracks,
274 |             audioTracks,
275 |             timebase,
276 |             fps,
277 |             durationSeconds,
278 |             durationTicks,
279 |             ticksPerSecond
280 |         });
281 |     }
282 | 
283 |     return out;
284 | };
285 | 
286 | const getTrack = async (sequence, trackIndex, clipIndex, trackType) => {
287 |     let trackItems = await getTrackItems(sequence, trackIndex, trackType);
288 | 
289 |     let trackItem;
290 |     let i = 0;
291 |     for (const t of trackItems) {
292 |         let index = i++;
293 |         if (index === clipIndex) {
294 |             trackItem = t;
295 |             break;
296 |         }
297 |     }
298 |     if (!trackItem) {
299 |         throw new Error(
300 |             `getTrack : trackItemIndex [${clipIndex}] does not exist for track type [${trackType}]`
301 |         );
302 |     }
303 | 
304 |     return trackItem;
305 | };
306 | 
307 | /*
308 | const getAudioTrack = async (sequence, trackIndex, clipIndex) => {
309 | 
310 |     let trackItems = await getAudioTrackItems(sequence, trackIndex)
311 | 
312 |     let trackItem;
313 |     let i = 0
314 |     for(const t of trackItems) {
315 |         let index = i++
316 |         if(index === clipIndex) {
317 |             trackItem = t
318 |             break
319 |         }
320 |     }
321 |     if(!trackItem) {
322 |         throw new Error(`getAudioTrack : trackItemIndex [${clipIndex}] does not exist`)
323 |     }
324 | 
325 |     return trackItem
326 | }
327 |     */
328 | 
329 | const getTrackItems = async (sequence, trackIndex, trackType) => {
330 |     let track;
331 | 
332 |     if (trackType === TRACK_TYPE.AUDIO) {
333 |         track = await sequence.getAudioTrack(trackIndex);
334 |     } else if (trackType === TRACK_TYPE.VIDEO) {
335 |         track = await sequence.getVideoTrack(trackIndex);
336 |     }
337 | 
338 |     if (!track) {
339 |         throw new Error(
340 |             `getTrackItems : getTrackItems [${trackIndex}] does not exist. Type : [${trackType}]`
341 |         );
342 |     }
343 | 
344 |     let trackItems = await track.getTrackItems(1, false);
345 | 
346 |     return trackItems;
347 | };
348 | 
349 | /*
350 | const getAudioTrackItems = async (sequence, trackIndex) => {
351 |     let audioTrack = await sequence.getAudioTrack(trackIndex)
352 |  
353 |     if(!audioTrack) {
354 |         throw new Error(`getAudioTrackItems : getAudioTrackItems [${trackIndex}] does not exist`)
355 |     }
356 | 
357 |     let trackItems = await audioTrack.getTrackItems(1, false)
358 | 
359 |     return trackItems
360 | }
361 | 
362 | const getVideoTrackItems = async (sequence, trackIndex) => {
363 |     let videoTrack = await sequence.getVideoTrack(trackIndex)
364 |  
365 |     if(!videoTrack) {
366 |         throw new Error(`getVideoTrackItems : videoTrackIndex [${trackIndex}] does not exist`)
367 |     }
368 | 
369 |     let trackItems = await videoTrack.getTrackItems(1, false)
370 | 
371 |     return trackItems
372 | }
373 | */
374 | /*
375 | const getVideoTrack = async (sequence, trackIndex, clipIndex) => {
376 | 
377 |     let trackItems = await getVideoTrackItems(sequence, trackIndex)
378 | 
379 |     let trackItem;
380 |     let i = 0
381 |     for(const t of trackItems) {
382 |         let index = i++
383 |         if(index === clipIndex) {
384 |             trackItem = t
385 |             break
386 |         }
387 |     }
388 |     if(!trackItem) {
389 |         throw new Error(`getVideoTrack : clipIndex [${clipIndex}] does not exist`)
390 |     }
391 | 
392 |     return trackItem
393 | }
394 |     */
395 | 
396 | module.exports = {
397 |     getTrackItems,
398 |     _getSequenceFromId,
399 |     _setActiveSequence,
400 |     setParam,
401 |     getParam,
402 |     addEffect,
403 |     findProjectItem,
404 |     execute,
405 |     getTracks,
406 |     getSequences,
407 |     getTrack,
408 | };
409 | 
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/commands.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* commands.js
  2 |  * Illustrator command handlers
  3 |  */
  4 | 
  5 | 
  6 | const getDocuments = async (command) => {
  7 |     const script = `
  8 |         (function() {
  9 |             try {
 10 |                 var result = (function() {
 11 |                     if (app.documents.length > 0) {
 12 |                         var activeDoc = app.activeDocument;
 13 |                         var docs = [];
 14 |                         
 15 |                         for (var i = 0; i < app.documents.length; i++) {
 16 |                             var doc = app.documents[i];
 17 |                             docs.push($.global.createDocumentInfo(doc, activeDoc));
 18 |                         }
 19 |                         
 20 |                         return docs;
 21 |                     } else {
 22 |                         return [];
 23 |                     }
 24 |                 })();
 25 |                 
 26 |                 if (result === undefined) {
 27 |                     return 'null';
 28 |                 }
 29 |                 
 30 |                 return JSON.stringify(result);
 31 |             } catch(e) {
 32 |                 return JSON.stringify({
 33 |                     error: e.toString(),
 34 |                     line: e.line || 'unknown'
 35 |                 });
 36 |             }
 37 |         })();
 38 |     `;
 39 |     
 40 |     let result = await executeCommand(script);
 41 |     return createPacket(result);
 42 | }
 43 | 
 44 | const exportPNG = async (command) => {
 45 |     const options = command.options || {};
 46 |     
 47 |     // Extract all options into variables
 48 |     const path = options.path;
 49 |     const transparency = options.transparency !== undefined ? options.transparency : true;
 50 |     const antiAliasing = options.antiAliasing !== undefined ? options.antiAliasing : true;
 51 |     const artBoardClipping = options.artBoardClipping !== undefined ? options.artBoardClipping : true;
 52 |     const horizontalScale = options.horizontalScale || 100;
 53 |     const verticalScale = options.verticalScale || 100;
 54 |     const exportType = options.exportType || 'PNG24';
 55 |     const matte = options.matte;
 56 |     const matteColor = options.matteColor;
 57 |     
 58 |     // Validate required path parameter
 59 |     if (!path) {
 60 |         return createPacket(JSON.stringify({
 61 |             error: "Path is required for PNG export"
 62 |         }));
 63 |     }
 64 |     
 65 |     const script = `
 66 |         (function() {
 67 |             try {
 68 |                 var result = (function() {
 69 |                     if (app.documents.length === 0) {
 70 |                         return { error: "No document is currently open" };
 71 |                     }
 72 |                     
 73 |                     var doc = app.activeDocument;
 74 |                     var exportPath = "${path}";
 75 |                     
 76 |                     // Export options from variables
 77 |                     var exportOptions = {
 78 |                         transparency: ${transparency},
 79 |                         antiAliasing: ${antiAliasing},
 80 |                         artBoardClipping: ${artBoardClipping},
 81 |                         horizontalScale: ${horizontalScale},
 82 |                         verticalScale: ${verticalScale},
 83 |                         exportType: "${exportType}"
 84 |                     };
 85 |                     
 86 |                     ${matte !== undefined ? `exportOptions.matte = ${matte};` : ''}
 87 |                     ${matteColor ? `exportOptions.matteColor = ${JSON.stringify(matteColor)};` : ''}
 88 |                     
 89 |                     // Use the global helper function if available, otherwise inline export
 90 |                     if (typeof $.global.exportToPNG === 'function') {
 91 |                         return $.global.exportToPNG(doc, exportPath, exportOptions);
 92 |                     } else {
 93 |                         // Inline export logic
 94 |                         try {
 95 |                             // Create PNG export options
 96 |                             var pngOptions = exportOptions.exportType === 'PNG8' ? 
 97 |                                 new ExportOptionsPNG8() : new ExportOptionsPNG24();
 98 |                                 
 99 |                             pngOptions.transparency = exportOptions.transparency;
100 |                             pngOptions.antiAliasing = exportOptions.antiAliasing;
101 |                             pngOptions.artBoardClipping = exportOptions.artBoardClipping;
102 |                             pngOptions.horizontalScale = exportOptions.horizontalScale;
103 |                             pngOptions.verticalScale = exportOptions.verticalScale;
104 |                             
105 |                             ${matte !== undefined ? `pngOptions.matte = ${matte};` : ''}
106 |                             
107 |                             ${matteColor ? `
108 |                             // Set matte color
109 |                             pngOptions.matteColor.red = ${matteColor.red};
110 |                             pngOptions.matteColor.green = ${matteColor.green};
111 |                             pngOptions.matteColor.blue = ${matteColor.blue};
112 |                             ` : ''}
113 |                             
114 |                             // Create file object
115 |                             var exportFile = new File(exportPath);
116 |                             
117 |                             // Determine export type
118 |                             var exportType = exportOptions.exportType === 'PNG8' ? 
119 |                                 ExportType.PNG8 : ExportType.PNG24;
120 |                             
121 |                             // Export the file
122 |                             doc.exportFile(exportFile, exportType, pngOptions);
123 |                             
124 |                             return {
125 |                                 success: true,
126 |                                 filePath: exportFile.fsName,
127 |                                 fileExists: exportFile.exists,
128 |                                 options: exportOptions,
129 |                                 documentName: doc.name
130 |                             };
131 |                             
132 |                         } catch(exportError) {
133 |                             return {
134 |                                 success: false,
135 |                                 error: exportError.toString(),
136 |                                 filePath: exportPath,
137 |                                 options: exportOptions,
138 |                                 documentName: doc.name
139 |                             };
140 |                         }
141 |                     }
142 |                 })();
143 |                 
144 |                 if (result === undefined) {
145 |                     return 'null';
146 |                 }
147 |                 
148 |                 return JSON.stringify(result);
149 |             } catch(e) {
150 |                 return JSON.stringify({
151 |                     error: e.toString(),
152 |                     line: e.line || 'unknown'
153 |                 });
154 |             }
155 |         })();
156 |     `;
157 |     
158 |     let result = await executeCommand(script);
159 |     return createPacket(result);
160 | }
161 | 
162 | const openFile = async (command) => {
163 |     const options = command.options || {};
164 |     
165 |     // Extract path parameter
166 |     const path = options.path;
167 |     
168 |     // Validate required path parameter
169 |     if (!path) {
170 |         return createPacket(JSON.stringify({
171 |             error: "Path is required to open an Illustrator file"
172 |         }));
173 |     }
174 |     
175 |     const script = `
176 |         (function() {
177 |             try {
178 |                 var result = (function() {
179 |                     var filePath = "${path}";
180 |                     
181 |                     try {
182 |                         // Create file object
183 |                         var fileToOpen = new File(filePath);
184 |                         
185 |                         // Check if file exists
186 |                         if (!fileToOpen.exists) {
187 |                             return {
188 |                                 success: false,
189 |                                 error: "File does not exist at the specified path",
190 |                                 filePath: filePath
191 |                             };
192 |                         }
193 |                         
194 |                         // Open the document
195 |                         var doc = app.open(fileToOpen);
196 |                         
197 |                         return {
198 |                             success: true,
199 |                         };
200 |                         
201 |                     } catch(openError) {
202 |                         return {
203 |                             success: false,
204 |                             error: openError.toString(),
205 |                             filePath: filePath
206 |                         };
207 |                     }
208 |                 })();
209 |                 
210 |                 if (result === undefined) {
211 |                     return 'null';
212 |                 }
213 |                 
214 |                 return JSON.stringify(result);
215 |             } catch(e) {
216 |                 return JSON.stringify({
217 |                     error: e.toString(),
218 |                     line: e.line || 'unknown'
219 |                 });
220 |             }
221 |         })();
222 |     `;
223 |     
224 |     let result = await executeCommand(script);
225 |     return createPacket(result);
226 | };
227 | 
228 | const getActiveDocumentInfo = async (command) => {
229 |     const script = `
230 |         (function() {
231 |             try {
232 |                 var result = (function() {
233 |                     if (app.documents.length > 0) {
234 |                         var doc = app.activeDocument;
235 |                         return $.global.createDocumentInfo(doc, doc);
236 |                     } else {
237 |                         return { error: "No document is currently open" };
238 |                     }
239 |                 })();
240 |                 
241 |                 if (result === undefined) {
242 |                     return 'null';
243 |                 }
244 |                 
245 |                 return JSON.stringify(result);
246 |             } catch(e) {
247 |                 return JSON.stringify({
248 |                     error: e.toString(),
249 |                     line: e.line || 'unknown'
250 |                 });
251 |             }
252 |         })();
253 |     `;
254 |     
255 |     let result = await executeCommand(script);
256 |     return createPacket(result);
257 | }
258 | 
259 | // Execute Illustrator command via ExtendScript
260 | function executeCommand(script) {
261 |     return new Promise((resolve, reject) => {
262 |         const csInterface = new CSInterface();
263 |         csInterface.evalScript(script, (result) => {
264 |             if (result === 'EvalScript error.') {
265 |                 reject(new Error('ExtendScript execution failed'));
266 |             } else {
267 |                 try {
268 |                     resolve(JSON.parse(result));
269 |                 } catch (e) {
270 |                     resolve(result);
271 |                 }
272 |             }
273 |         });
274 |     });
275 | }
276 | 
277 | 
278 | async function executeExtendScript(command) {
279 |     const options = command.options
280 |     const scriptString = options.scriptString;
281 | 
282 |     const script = `
283 |         (function() {
284 |             try {
285 |                 ${scriptString}
286 |             } catch(e) {
287 |                 return JSON.stringify({
288 |                     error: e.toString(),
289 |                     line: e.line || 'unknown'
290 |                 });
291 |             }
292 |         })();
293 |     `;
294 |     
295 |     const result = await executeCommand(script);
296 |     
297 |     return createPacket(result);
298 | }
299 | 
300 | const createPacket = (result) => {
301 |     return {
302 |         content: [{
303 |             type: "text",
304 |             text: JSON.stringify(result, null, 2)
305 |         }]
306 |     };
307 | }
308 | 
309 | const parseAndRouteCommand = async (command) => {
310 |     let action = command.action;
311 | 
312 |     let f = commandHandlers[action];
313 | 
314 |     if (typeof f !== "function") {
315 |         throw new Error(`Unknown Command: ${action}`);
316 |     }
317 | 
318 |     console.log(f.name)
319 |     return await f(command);
320 | };
321 | 
322 | 
323 | // Execute commands
324 | /*
325 | async function executeCommand(command) {
326 |     switch(command.action) {
327 | 
328 |         case "getLayers":
329 |             return await getLayers();
330 |         
331 |         case "executeExtendScript":
332 |             return await executeExtendScript(command);
333 |         
334 |         default:
335 |             throw new Error(`Unknown command: ${command.action}`);
336 |     }
337 | }*/
338 | 
339 | const commandHandlers = {
340 |     executeExtendScript,
341 |     getDocuments,
342 |     getActiveDocumentInfo,
343 |     exportPNG,
344 |     openFile
345 | };
```

--------------------------------------------------------------------------------
/uxp/ps/commands/core.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* MIT License
  2 |  *
  3 |  * Copyright (c) 2025 Mike Chambers
  4 |  *
  5 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
  6 |  * of this software and associated documentation files (the "Software"), to deal
  7 |  * in the Software without restriction, including without limitation the rights
  8 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 |  * copies of the Software, and to permit persons to whom the Software is
 10 |  * furnished to do so, subject to the following conditions:
 11 |  *
 12 |  * The above copyright notice and this permission notice shall be included in all
 13 |  * copies or substantial portions of the Software.
 14 |  *
 15 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 |  * SOFTWARE.
 22 |  */
 23 | 
 24 | const { app, constants, action, imaging } = require("photoshop");
 25 | const fs = require("uxp").storage.localFileSystem;
 26 | 
 27 | const {
 28 |     _saveDocumentAs,
 29 |     parseColor,
 30 |     getAlignmentMode,
 31 |     getNewDocumentMode,
 32 |     selectLayer,
 33 |     findLayer,
 34 |     findLayerByName,
 35 |     execute,
 36 |     tokenify,
 37 |     hasActiveSelection,
 38 |     listOpenDocuments
 39 | } = require("./utils");
 40 | 
 41 | const { rasterizeLayer } = require("./layers").commandHandlers;
 42 | 
 43 | const openFile = async (command) => {
 44 |     let options = command.options;
 45 | 
 46 |     await execute(async () => {
 47 |         let entry = null;
 48 |         try {
 49 |             entry = await fs.getEntryWithUrl("file:" + options.filePath);
 50 |         } catch (e) {
 51 |             throw new Error(
 52 |                 "openFile: Could not create file entry. File probably does not exist."
 53 |             );
 54 |         }
 55 | 
 56 |         await app.open(entry);
 57 |     });
 58 | };
 59 | 
 60 | const placeImage = async (command) => {
 61 |     let options = command.options;
 62 |     let layerId = options.layerId;
 63 |     let layer = findLayer(layerId);
 64 | 
 65 |     if (!layer) {
 66 |         throw new Error(`placeImage : Could not find layerId : ${layerId}`);
 67 |     }
 68 | 
 69 |     await execute(async () => {
 70 |         selectLayer(layer, true);
 71 |         let layerId = layer.id;
 72 | 
 73 |         let imagePath = await tokenify(options.imagePath);
 74 | 
 75 |         let commands = [
 76 |             // Place
 77 |             {
 78 |                 ID: layerId,
 79 |                 _obj: "placeEvent",
 80 |                 freeTransformCenterState: {
 81 |                     _enum: "quadCenterState",
 82 |                     _value: "QCSAverage",
 83 |                 },
 84 |                 null: {
 85 |                     _kind: "local",
 86 |                     _path: imagePath,
 87 |                 },
 88 |                 offset: {
 89 |                     _obj: "offset",
 90 |                     horizontal: {
 91 |                         _unit: "pixelsUnit",
 92 |                         _value: 0.0,
 93 |                     },
 94 |                     vertical: {
 95 |                         _unit: "pixelsUnit",
 96 |                         _value: 0.0,
 97 |                     },
 98 |                 },
 99 |                 replaceLayer: {
100 |                     _obj: "placeEvent",
101 |                     to: {
102 |                         _id: layerId,
103 |                         _ref: "layer",
104 |                     },
105 |                 },
106 |             },
107 |             {
108 |                 _obj: "set",
109 |                 _target: [
110 |                     {
111 |                         _enum: "ordinal",
112 |                         _ref: "layer",
113 |                         _value: "targetEnum",
114 |                     },
115 |                 ],
116 |                 to: {
117 |                     _obj: "layer",
118 |                     name: layerId,
119 |                 },
120 |             },
121 |         ];
122 | 
123 |         await action.batchPlay(commands, {});
124 |         await rasterizeLayer(command);
125 |     });
126 | };
127 | 
128 | const getDocumentImage = async (command) => {
129 |     let out = await execute(async () => {
130 | 
131 |         const pixelsOpt = {
132 |             applyAlpha: true
133 |         };
134 | 
135 |         const imgObj = await imaging.getPixels(pixelsOpt);
136 | 
137 |         const base64Data = await imaging.encodeImageData({
138 |             imageData: imgObj.imageData,
139 |             base64: true,
140 |         });
141 | 
142 |         const result = {
143 |             base64Image: base64Data,
144 |             dataUrl: `data:image/jpeg;base64,${base64Data}`,
145 |             width: imgObj.imageData.width,
146 |             height: imgObj.imageData.height,
147 |             colorSpace: imgObj.imageData.colorSpace,
148 |             components: imgObj.imageData.components,
149 |             format: "jpeg",
150 |         };
151 | 
152 |         imgObj.imageData.dispose();
153 |         return result;
154 |     });
155 | 
156 |     return out;
157 | };
158 | 
159 | const getDocumentInfo = async (command) => {
160 |     let doc = app.activeDocument;
161 |     let path = doc.path;
162 | 
163 |     let out = {
164 |         height: doc.height,
165 |         width: doc.width,
166 |         colorMode: doc.mode.toString(),
167 |         pixelAspectRatio: doc.pixelAspectRatio,
168 |         resolution: doc.resolution,
169 |         path: path,
170 |         saved: path.length > 0,
171 |         hasUnsavedChanges: !doc.saved,
172 |     };
173 | 
174 |     return out;
175 | };
176 | 
177 | const cropDocument = async (command) => {
178 |     let options = command.options;
179 | 
180 |     if (!hasActiveSelection()) {
181 |         throw new Error("cropDocument : Requires an active selection");
182 |     }
183 | 
184 |     return await execute(async () => {
185 |         let commands = [
186 |             // Crop
187 |             {
188 |                 _obj: "crop",
189 |                 delete: true,
190 |             },
191 |         ];
192 | 
193 |         await action.batchPlay(commands, {});
194 |     });
195 | };
196 | 
197 | const removeBackground = async (command) => {
198 |     let options = command.options;
199 |     let layerId = options.layerId;
200 | 
201 |     let layer = findLayer(layerId);
202 | 
203 |     if (!layer) {
204 |         throw new Error(
205 |             `removeBackground : Could not find layerId : ${layerId}`
206 |         );
207 |     }
208 | 
209 |     await execute(async () => {
210 |         selectLayer(layer, true);
211 | 
212 |         let commands = [
213 |             // Remove Background
214 |             {
215 |                 _obj: "removeBackground",
216 |             },
217 |         ];
218 | 
219 |         await action.batchPlay(commands, {});
220 |     });
221 | };
222 | 
223 | const alignContent = async (command) => {
224 |     let options = command.options;
225 |     let layerId = options.layerId;
226 | 
227 |     let layer = findLayer(layerId);
228 | 
229 |     if (!layer) {
230 |         throw new Error(
231 |             `alignContent : Could not find layerId : ${layerId}`
232 |         );
233 |     }
234 | 
235 |     if (!app.activeDocument.selection.bounds) {
236 |         throw new Error(`alignContent : Requires an active selection`);
237 |     }
238 | 
239 |     await execute(async () => {
240 |         let m = getAlignmentMode(options.alignmentMode);
241 | 
242 |         selectLayer(layer, true);
243 | 
244 |         let commands = [
245 |             {
246 |                 _obj: "align",
247 |                 _target: [
248 |                     {
249 |                         _enum: "ordinal",
250 |                         _ref: "layer",
251 |                         _value: "targetEnum",
252 |                     },
253 |                 ],
254 |                 alignToCanvas: false,
255 |                 using: {
256 |                     _enum: "alignDistributeSelector",
257 |                     _value: m,
258 |                 },
259 |             },
260 |         ];
261 |         await action.batchPlay(commands, {});
262 |     });
263 | };
264 | 
265 | const generateImage = async (command) => {
266 |     let options = command.options;
267 | 
268 |     await execute(async () => {
269 |         let doc = app.activeDocument;
270 | 
271 |         await doc.selection.selectAll();
272 | 
273 |         let contentType = "none";
274 |         const c = options.contentType.toLowerCase()
275 |         if (c === "photo" || c === "art") {
276 |             contentType = c;
277 |         }
278 | 
279 |         let commands = [
280 |             // Generate Image current document
281 |             {
282 |                 _obj: "syntheticTextToImage",
283 |                 _target: [
284 |                     {
285 |                         _enum: "ordinal",
286 |                         _ref: "document",
287 |                         _value: "targetEnum",
288 |                     },
289 |                 ],
290 |                 documentID: doc.id,
291 |                 layerID: 0,
292 |                 prompt: options.prompt,
293 |                 serviceID: "clio",
294 |                 serviceOptionsList: {
295 |                     clio: {
296 |                         _obj: "clio",
297 |                         clio_advanced_options: {
298 |                             text_to_image_styles_options: {
299 |                                 text_to_image_content_type: contentType,
300 |                                 text_to_image_effects_count: 0,
301 |                                 text_to_image_effects_list: [
302 |                                     "none",
303 |                                     "none",
304 |                                     "none",
305 |                                 ],
306 |                             },
307 |                         },
308 |                         dualCrop: true,
309 |                         gentech_workflow_name: "text_to_image",
310 |                         gi_ADVANCED: '{"enable_mts":true}',
311 |                         gi_CONTENT_PRESERVE: 0,
312 |                         gi_CROP: false,
313 |                         gi_DILATE: false,
314 |                         gi_ENABLE_PROMPT_FILTER: true,
315 |                         gi_GUIDANCE: 6,
316 |                         gi_MODE: "ginp",
317 |                         gi_NUM_STEPS: -1,
318 |                         gi_PROMPT: options.prompt,
319 |                         gi_SEED: -1,
320 |                         gi_SIMILARITY: 0,
321 |                     },
322 |                 },
323 |                 workflow: "text_to_image",
324 |                 workflowType: {
325 |                     _enum: "genWorkflow",
326 |                     _value: "text_to_image",
327 |                 },
328 |             },
329 |             // Rasterize current layer
330 |             {
331 |                 _obj: "rasterizeLayer",
332 |                 _target: [
333 |                     {
334 |                         _enum: "ordinal",
335 |                         _ref: "layer",
336 |                         _value: "targetEnum",
337 |                     },
338 |                 ],
339 |             },
340 |         ];
341 |         let o = await action.batchPlay(commands, {});
342 |         let layerId = o[0].layerID;
343 | 
344 |         //let l = findLayerByName(options.prompt);
345 |         let l = findLayer(layerId);
346 |         l.name = options.layerName;
347 |     });
348 | };
349 | 
350 | const generativeFill = async (command) => {
351 |     const options = command.options;
352 |     const layerId = options.layerId;
353 |     const prompt = options.prompt;
354 | 
355 |     const layer = findLayer(layerId);
356 | 
357 |     if (!layer) {
358 |         throw new Error(
359 |             `generativeFill : Could not find layerId : ${layerId}`
360 |         );
361 |     }
362 | 
363 |     if(!hasActiveSelection()) {
364 |         throw new Error(
365 |             `generativeFill : Requires an active selection.`
366 |         ); 
367 |     }
368 | 
369 |     await execute(async () => {
370 |         let doc = app.activeDocument;
371 | 
372 |         let contentType = "none";
373 |         const c = options.contentType.toLowerCase()
374 |         if (c === "photo" || c === "art") {
375 |             contentType = c;
376 |         }
377 | 
378 |         let commands = [
379 |             // Generative Fill current document
380 |             {
381 |                 "_obj": "syntheticFill",
382 |                 "_target": [
383 |                     {
384 |                         "_enum": "ordinal",
385 |                         "_ref": "document",
386 |                         "_value": "targetEnum"
387 |                     }
388 |                 ],
389 |                 "documentID": doc.id,
390 |                 "layerID": layerId,
391 |                 "prompt": prompt,
392 |                 "serviceID": "clio",
393 |                 "serviceOptionsList": {
394 |                     "clio": {
395 |                         "_obj": "clio",
396 |                         "dualCrop": true,
397 |                         "gi_ADVANCED": "{\"enable_mts\":true}",
398 |                         "gi_CONTENT_PRESERVE": 0,
399 |                         "gi_CROP": false,
400 |                         "gi_DILATE": false,
401 |                         "gi_ENABLE_PROMPT_FILTER": true,
402 |                         "gi_GUIDANCE": 6,
403 |                         "gi_MODE": "tinp",
404 |                         "gi_NUM_STEPS": -1,
405 |                         "gi_PROMPT": prompt,
406 |                         "gi_SEED": -1,
407 |                         "gi_SIMILARITY": 0,
408 | 
409 | 
410 |                         clio_advanced_options: {
411 |                             text_to_image_styles_options: {
412 |                                 text_to_image_content_type: contentType,
413 |                                 text_to_image_effects_count: 0,
414 |                                 text_to_image_effects_list: [
415 |                                     "none",
416 |                                     "none",
417 |                                     "none",
418 |                                 ],
419 |                             },
420 |                         },
421 | 
422 |                     }
423 |                 },
424 |                 "serviceVersion": "clio3",
425 |                 "workflowType": {
426 |                     "_enum": "genWorkflow",
427 |                     "_value": "in_painting"
428 |                 },
429 |                 "workflow_to_active_service_identifier_map": {
430 |                     "gen_harmonize": "clio3",
431 |                     "generate_background": "clio3",
432 |                     "generate_similar": "clio3",
433 |                     "generativeUpscale": "fal_aura_sr",
434 |                     "in_painting": "clio3",
435 |                     "instruct_edit": "clio3",
436 |                     "out_painting": "clio3",
437 |                     "text_to_image": "clio3"
438 |                 }
439 |             }
440 |         ];
441 | 
442 | 
443 |         let o = await action.batchPlay(commands, {});
444 |         let id = o[0].layerID;
445 | 
446 |         //let l = findLayerByName(options.prompt);
447 |         let l = findLayer(id);
448 |         l.name = options.layerName;
449 |     });
450 | };
451 | 
452 | const saveDocument = async (command) => {
453 |     await execute(async () => {
454 |         await app.activeDocument.save();
455 |     });
456 | };
457 | 
458 | const saveDocumentAs = async (command) => {
459 |     let options = command.options;
460 | 
461 |     return await _saveDocumentAs(options.filePath, options.fileType);
462 | };
463 | 
464 | const setActiveDocument = async (command) => {
465 | 
466 |     let options = command.options;
467 |     let documentId = options.documentId;
468 |     let docs = listOpenDocuments();
469 | 
470 |     for (let doc of docs) {
471 |         if (doc.id === documentId) {
472 |             await execute(async () => {
473 |                 app.activeDocument = doc;
474 |             });
475 | 
476 |             return
477 |         }
478 |     }
479 | }
480 | 
481 | const getDocuments = async (command) => {
482 |     return listOpenDocuments()
483 | }
484 | 
485 | const duplicateDocument = async (command) => {
486 |     let options = command.options;
487 |     let name = options.name
488 | 
489 |     await execute(async () => {
490 |         const doc = app.activeDocument;
491 |         await doc.duplicate(name)
492 |     });
493 | };
494 | 
495 | const createDocument = async (command) => {
496 |     let options = command.options;
497 |     let colorMode = getNewDocumentMode(command.options.colorMode);
498 |     let fillColor = parseColor(options.fillColor);
499 | 
500 |     await execute(async () => {
501 |         await app.createDocument({
502 |             typename: "DocumentCreateOptions",
503 |             width: options.width,
504 |             height: options.height,
505 |             resolution: options.resolution,
506 |             mode: colorMode,
507 |             fill: constants.DocumentFill.COLOR,
508 |             fillColor: fillColor,
509 |             profile: "sRGB IEC61966-2.1",
510 |         });
511 | 
512 |         let background = findLayerByName("Background");
513 |         background.allLocked = false;
514 |         background.name = "Background";
515 |     });
516 | };
517 | 
518 | const executeBatchPlayCommand = async (commands) => {
519 |     let options = commands.options;
520 |     let c = options.commands;
521 | 
522 | 
523 | 
524 |     let out = await execute(async () => {
525 |         let o = await action.batchPlay(c, {});
526 |         return o[0]
527 |     });
528 | 
529 |     console.log(out)
530 |     return out;
531 | }
532 | 
533 | const commandHandlers = {
534 |     generativeFill,
535 |     executeBatchPlayCommand,
536 |     setActiveDocument,
537 |     getDocuments,
538 |     duplicateDocument,
539 |     getDocumentImage,
540 |     openFile,
541 |     placeImage,
542 |     getDocumentInfo,
543 |     cropDocument,
544 |     removeBackground,
545 |     alignContent,
546 |     generateImage,
547 |     saveDocument,
548 |     saveDocumentAs,
549 |     createDocument,
550 | };
551 | 
552 | module.exports = {
553 |     commandHandlers,
554 | };
555 | 
```

--------------------------------------------------------------------------------
/uxp/pr/commands/core.js:
--------------------------------------------------------------------------------

```javascript
  1 | 
  2 | const fs = require("uxp").storage.localFileSystem;
  3 | const app = require("premierepro");
  4 | const constants = require("premierepro").Constants;
  5 | 
  6 | const {BLEND_MODES, TRACK_TYPE } = require("./consts.js")
  7 | 
  8 | const {
  9 |     _getSequenceFromId,
 10 |     _setActiveSequence,
 11 |     setParam,
 12 |     getParam,
 13 |     addEffect,
 14 |     findProjectItem,
 15 |     execute,
 16 |     getTrack,
 17 |     getTrackItems
 18 | } = require("./utils.js")
 19 | 
 20 | const saveProject = async (command) => {
 21 |     let project = await app.Project.getActiveProject()
 22 | 
 23 |     project.save()
 24 | }
 25 | 
 26 | const saveProjectAs = async (command) => {
 27 |     let project = await app.Project.getActiveProject()
 28 | 
 29 |     const options = command.options;
 30 |     const filePath = options.filePath;
 31 | 
 32 |     project.saveAs(filePath)
 33 | }
 34 | 
 35 | const openProject = async (command) => {
 36 | 
 37 |     const options = command.options;
 38 |     const filePath = options.filePath;
 39 | 
 40 |     await app.Project.open(filePath);    
 41 | }
 42 | 
 43 | 
 44 | const importMedia = async (command) => {
 45 | 
 46 |     let options = command.options
 47 |     let paths = command.options.filePaths
 48 | 
 49 |     let project = await app.Project.getActiveProject()
 50 | 
 51 |     let root = await project.getRootItem()
 52 |     let originalItems = await root.getItems()
 53 | 
 54 |     //import everything into root
 55 |     let rootFolderItems = await project.getRootItem()
 56 | 
 57 | 
 58 |     let success = await project.importFiles(paths, true, rootFolderItems)
 59 |     //TODO: what is not success?
 60 | 
 61 |     let updatedItems = await root.getItems()
 62 |     
 63 |     const addedItems = updatedItems.filter(
 64 |         updatedItem => !originalItems.some(originalItem => originalItem.name === updatedItem.name)
 65 |       );
 66 |       
 67 |     let addedProjectItems = [];
 68 |     for (const p of addedItems) { 
 69 |         addedProjectItems.push({ name: p.name });
 70 |     }
 71 |     
 72 |     return { addedProjectItems };
 73 | }
 74 | 
 75 | 
 76 | //note: right now, we just always add to the active sequence. Need to add support
 77 | //for specifying sequence
 78 | const addMediaToSequence = async (command) => {
 79 | 
 80 |     let options = command.options
 81 |     let itemName = options.itemName
 82 |     let id = options.sequenceId
 83 | 
 84 |     let project = await app.Project.getActiveProject()
 85 |     let sequence = await _getSequenceFromId(id)
 86 | 
 87 |     let insertItem = await findProjectItem(itemName, project)
 88 | 
 89 |     let editor = await app.SequenceEditor.getEditor(sequence)
 90 |   
 91 |     const insertionTime = await app.TickTime.createWithTicks(options.insertionTimeTicks.toString());
 92 |     const videoTrackIndex = options.videoTrackIndex
 93 |     const audioTrackIndex = options.audioTrackIndex
 94 |   
 95 |     //not sure what this does
 96 |     const limitShift = false
 97 | 
 98 |     //let f = ((options.overwrite) ? editor.createOverwriteItemAction : editor.createInsertProjectItemAction).bind(editor)
 99 |     //let action = f(insertItem, insertionTime, videoTrackIndex, audioTrackIndex, limitShift)
100 |     execute(() => {
101 |         let action = editor.createOverwriteItemAction(insertItem, insertionTime, videoTrackIndex, audioTrackIndex)
102 |         return [action]
103 |     }, project)  
104 | }
105 | 
106 | 
107 | const setAudioTrackMute = async (command) => {
108 | 
109 |     let options = command.options
110 |     let id = options.sequenceId
111 | 
112 |     let sequence = await _getSequenceFromId(id)
113 | 
114 |     let track = await sequence.getTrack(options.audioTrackIndex, TRACK_TYPE.AUDIO)
115 |     track.setMute(options.mute)
116 | }
117 | 
118 | 
119 | 
120 | const setVideoClipProperties = async (command) => {
121 | 
122 |     const options = command.options
123 |     let id = options.sequenceId
124 | 
125 |     let project = await app.Project.getActiveProject()
126 |     let sequence = await _getSequenceFromId(id)
127 | 
128 |     if(!sequence) {
129 |         throw new Error(`setVideoClipProperties : Requires an active sequence.`)
130 |     }
131 | 
132 |     let trackItem = await getTrack(sequence, options.videoTrackIndex, options.trackItemIndex, TRACK_TYPE.VIDEO)
133 | 
134 |     let opacityParam = await getParam(trackItem, "AE.ADBE Opacity", "Opacity")
135 |     let opacityKeyframe = await opacityParam.createKeyframe(options.opacity)
136 | 
137 |     let blendModeParam = await getParam(trackItem, "AE.ADBE Opacity", "Blend Mode")
138 | 
139 |     let mode = BLEND_MODES[options.blendMode.toUpperCase()]
140 |     let blendModeKeyframe = await blendModeParam.createKeyframe(mode)
141 | 
142 |     execute(() => {
143 |         let opacityAction = opacityParam.createSetValueAction(opacityKeyframe);
144 |         let blendModeAction = blendModeParam.createSetValueAction(blendModeKeyframe);
145 |         return [opacityAction, blendModeAction]
146 |     }, project)
147 | 
148 |     // /AE.ADBE Opacity
149 |     //Opacity
150 |     //Blend Mode
151 | 
152 | }
153 | 
154 | const appendVideoFilter = async (command) => {
155 | 
156 |     let options = command.options
157 |     let id = options.sequenceId
158 | 
159 |     let sequence = await _getSequenceFromId(id)
160 | 
161 |     if(!sequence) {
162 |         throw new Error(`appendVideoFilter : Requires an active sequence.`)
163 |     }
164 | 
165 |     let trackItem = await getTrackTrack(sequence, options.videoTrackIndex, options.trackItemIndex, TRACK_TYPE.VIDEO)
166 | 
167 |     let effectName = options.effectName
168 |     let properties = options.properties
169 | 
170 |     let d = await addEffect(trackItem, effectName)
171 | 
172 |     for(const p of properties) {
173 |         console.log(p.value)
174 |         await setParam(trackItem, effectName, p.name, p.value)
175 |     }
176 | }
177 | 
178 | 
179 | const setActiveSequence = async (command) => {
180 |     let options = command.options
181 |     let id = options.sequenceId
182 | 
183 |     let sequence = await _getSequenceFromId(id)
184 | 
185 |     await _setActiveSequence(sequence)
186 | }
187 | 
188 | const createProject = async (command) => {
189 | 
190 |     let options = command.options
191 |     let path = options.path
192 |     let name = options.name
193 | 
194 |     if (!path.endsWith('/')) {
195 |         path = path + '/';
196 |     }
197 | 
198 |     //todo: this will open a dialog if directory doesnt exist
199 |     let project = await app.Project.createProject(`${path}${name}.prproj`) 
200 | 
201 | 
202 |     if(!project) {
203 |         throw new Error("createProject : Could not create project. Check that the directory path exists and try again.")
204 |     }
205 | 
206 |     //create a default sequence and set it as active
207 |     //let sequence = await project.createSequence("default")
208 |     //await project.setActiveSequence(sequence)
209 | }
210 | 
211 | 
212 | const _exportFrame = async (sequence, filePath, seconds) => {
213 | 
214 |     const fileType = filePath.split('.').pop()
215 | 
216 |     let size = await sequence.getFrameSize()
217 | 
218 |     let p = window.path.parse(filePath)
219 |     let t = app.TickTime.createWithSeconds(seconds)
220 | 
221 |     let out = await app.Exporter.exportSequenceFrame(sequence, t, p.base, p.dir, size.width, size.height)
222 | 
223 |     let ps = `${p.dir}${window.path.sep}${p.base}`
224 |     let outPath = `${ps}.${fileType}`
225 | 
226 |     if(!out) {
227 |         throw new Error(`exportFrame : Could not save frame to [${outPath}]`);
228 |     }
229 | 
230 |     return outPath
231 | }
232 | 
233 | const exportFrame = async (command) => {
234 |     const options = command.options;
235 |     let id = options.sequenceId;
236 |     let filePath = options.filePath;
237 |     let seconds = options.seconds;
238 | 
239 |     let sequence = await _getSequenceFromId(id);
240 | 
241 |     const outPath = await _exportFrame(sequence, filePath, seconds);
242 | 
243 |     return {"filePath": outPath}
244 | }
245 | 
246 | const setClipDisabled = async (command) => {
247 | 
248 |     const options = command.options;
249 |     const id = options.sequenceId;
250 |     const trackIndex = options.trackIndex;
251 |     const trackItemIndex = options.trackItemIndex;
252 |     const trackType = options.trackType;
253 | 
254 |     let project = await app.Project.getActiveProject()
255 |     let sequence = await _getSequenceFromId(id)
256 | 
257 |     if(!sequence) {
258 |         throw new Error(`setClipDisabled : Requires an active sequence.`)
259 |     }
260 | 
261 |     let trackItem = await getTrack(sequence, trackIndex, trackItemIndex, trackType)
262 | 
263 |     execute(() => {
264 |         let action = trackItem.createSetDisabledAction(options.disabled)
265 |         return [action]
266 |     }, project)
267 | 
268 | }
269 | 
270 | 
271 | const appendVideoTransition = async (command) => {
272 | 
273 |     let options = command.options
274 |     let id = options.sequenceId
275 | 
276 |     let project = await app.Project.getActiveProject()
277 |     let sequence = await _getSequenceFromId(id)
278 | 
279 |     if(!sequence) {
280 |         throw new Error(`appendVideoTransition : Requires an active sequence.`)
281 |     }
282 | 
283 |     let trackItem = await getTrack(sequence, options.videoTrackIndex, options.trackItemIndex,TRACK_TYPE.VIDEO)
284 | 
285 |     let transition = await app.TransitionFactory.createVideoTransition(options.transitionName);
286 | 
287 |     let transitionOptions = new app.AddTransitionOptions()
288 |     transitionOptions.setApplyToStart(false)
289 | 
290 |     const time = await app.TickTime.createWithSeconds(options.duration)
291 |     transitionOptions.setDuration(time)
292 |     transitionOptions.setTransitionAlignment(options.clipAlignment)
293 | 
294 |     execute(() => {
295 |         let action = trackItem.createAddVideoTransitionAction(transition, transitionOptions)
296 |         return [action]
297 |     }, project)
298 | }
299 | 
300 | 
301 | const getProjectInfo = async (command) => {
302 |     return {}
303 | }
304 | 
305 | 
306 | 
307 | const createSequenceFromMedia = async (command) => {
308 | 
309 |     let options = command.options
310 | 
311 |     let itemNames = options.itemNames
312 |     let sequenceName = options.sequenceName
313 | 
314 |     let project = await app.Project.getActiveProject()
315 | 
316 |     let found = false
317 |     try {
318 |         await findProjectItem(sequenceName, project)
319 |         found  = true
320 |     } catch {
321 |         //do nothing
322 |     }
323 | 
324 |     if(found) {
325 |         throw Error(`createSequenceFromMedia : sequence name [${sequenceName}] is already in use`)
326 |     }
327 | 
328 |     let items = []
329 |     for (const name of itemNames) {
330 | 
331 |         //this is a little inefficient
332 |         let insertItem = await findProjectItem(name, project)
333 |         items.push(insertItem)
334 |     }
335 | 
336 | 
337 |     let root = await project.getRootItem()
338 |     
339 |     let sequence = await project.createSequenceFromMedia(sequenceName, items, root)
340 | 
341 |     await _setActiveSequence(sequence)
342 | }
343 | 
344 | const setClipStartEndTimes = async (command) => {
345 |     const options = command.options;
346 | 
347 |     const sequenceId = options.sequenceId;
348 |     const trackIndex = options.trackIndex;
349 |     const trackItemIndex = options.trackItemIndex;
350 |     const startTimeTicks = options.startTimeTicks;
351 |     const endTimeTicks = options.endTimeTicks;
352 |     const trackType = options.trackType
353 | 
354 |     const sequence = await _getSequenceFromId(sequenceId)
355 |     let trackItem = await getTrack(sequence, trackIndex, trackItemIndex, trackType)
356 | 
357 |     const startTick = await app.TickTime.createWithTicks(startTimeTicks.toString());
358 |     const endTick = await app.TickTime.createWithTicks(endTimeTicks.toString());;
359 | 
360 |     let project = await app.Project.getActiveProject();
361 | 
362 |     execute(() => {
363 | 
364 |         let out = []
365 | 
366 |         out.push(trackItem.createSetStartAction(startTick));
367 |         out.push(trackItem.createSetEndAction(endTick))
368 | 
369 |         return out
370 |     }, project)
371 | }
372 | 
373 | const closeGapsOnSequence = async(command) => {
374 |     const options = command.options
375 |     const sequenceId = options.sequenceId;
376 |     const trackIndex = options.trackIndex;
377 |     const trackType = options.trackType;
378 | 
379 |     let sequence = await _getSequenceFromId(sequenceId)
380 | 
381 |     let out = await _closeGapsOnSequence(sequence, trackIndex, trackType)
382 |     
383 |     return out
384 | }
385 | 
386 | const _closeGapsOnSequence = async (sequence, trackIndex, trackType) => {
387 |   
388 |     let project = await app.Project.getActiveProject()
389 | 
390 |     let items = await getTrackItems(sequence, trackIndex, trackType)
391 | 
392 |     if(!items || items.length === 0) {
393 |         return;
394 |     }
395 |     
396 |     const f = async (item, targetPosition) => {
397 |         let currentStart = await item.getStartTime()
398 | 
399 |         let a = await currentStart.ticksNumber
400 |         let b = await targetPosition.ticksNumber
401 |         let shiftAmount = (a - b)// How much to shift 
402 |         
403 |         shiftAmount *= -1;
404 | 
405 |         let shiftTick = app.TickTime.createWithTicks(shiftAmount.toString())
406 | 
407 |         return shiftTick
408 |     }
409 | 
410 |     let targetPosition = app.TickTime.createWithTicks("0")
411 | 
412 | 
413 |     for(let i = 0; i < items.length; i++) {
414 |         let item = items[i];
415 |         let shiftTick = await f(item, targetPosition)
416 |         
417 |         execute(() => {
418 |             let out = []
419 | 
420 |                 out.push(item.createMoveAction(shiftTick))
421 | 
422 |             return out
423 |         }, project)
424 |         
425 |         targetPosition = await item.getEndTime()
426 |     }
427 | }
428 | 
429 | //TODO: change API to take trackType?
430 | 
431 | //TODO: pass in scope here
432 | const removeItemFromSequence = async (command) => {
433 |     const options = command.options;
434 | 
435 |     const sequenceId = options.sequenceId;
436 |     const trackIndex = options.trackIndex;
437 |     const trackItemIndex = options.trackItemIndex;
438 |     const rippleDelete = options.rippleDelete;
439 |     const trackType = options.trackType
440 | 
441 |     let project = await app.Project.getActiveProject()
442 |     let sequence = await _getSequenceFromId(sequenceId)
443 | 
444 |     if(!sequence) {
445 |         throw Error(`addMarkerToSequence : sequence with id [${sequenceId}] not found.`)
446 |     }
447 | 
448 |     let item = await getTrack(sequence, trackIndex, trackItemIndex, trackType);
449 | 
450 |     let editor = await app.SequenceEditor.getEditor(sequence)
451 | 
452 |     let trackItemSelection = await sequence.getSelection();
453 |     let items = await trackItemSelection.getTrackItems()
454 | 
455 |     for (let t of items) {
456 |         await trackItemSelection.removeItem(t)
457 |     }
458 | 
459 |     trackItemSelection.addItem(item, true)
460 | 
461 |     execute(() => {
462 |         const shiftOverlapping = false
463 |         let action = editor.createRemoveItemsAction(trackItemSelection, rippleDelete, constants.MediaType.ANY, shiftOverlapping )
464 |         return [action]
465 |     }, project)
466 | }
467 | 
468 | const addMarkerToSequence = async (command) => {
469 |     const options = command.options;
470 |     const sequenceId = options.sequenceId;
471 |     const markerName = options.markerName;
472 |     const startTimeTicks = options.startTimeTicks;
473 |     const durationTicks = options.durationTicks;
474 |     const comments = options.comments;
475 | 
476 |     const sequence = await _getSequenceFromId(sequenceId)
477 | 
478 |     if(!sequence) {
479 |         throw Error(`addMarkerToSequence : sequence with id [${sequenceId}] not found.`)
480 |     }
481 | 
482 |     let markers = await app.Markers.getMarkers(sequence);
483 | 
484 |     let project = await app.Project.getActiveProject()
485 | 
486 |     execute(() => {
487 | 
488 |         let start = app.TickTime.createWithTicks(startTimeTicks.toString())
489 |         let duration = app.TickTime.createWithTicks(durationTicks.toString())
490 | 
491 |         let action = markers.createAddMarkerAction(markerName, "WebLink",  start, duration, comments)
492 |         return [action]
493 |     }, project)
494 | 
495 | }
496 | 
497 | const moveProjectItemsToBin = async (command) => {
498 |     const options = command.options;
499 |     const binName = options.binName;
500 |     const projectItemNames = options.itemNames;
501 | 
502 |     const project = await app.Project.getActiveProject()
503 |     
504 |     const binFolderItem = await findProjectItem(binName, project)
505 | 
506 |     if(!binFolderItem) {
507 |         throw Error(`moveProjectItemsToBin : Bin with name [${binName}] not found.`)
508 |     }
509 | 
510 |     let folderItems = [];
511 | 
512 |     for(let name of projectItemNames) {
513 |         let item = await findProjectItem(name, project)
514 | 
515 |         if(!item) {
516 |             throw Error(`moveProjectItemsToBin : FolderItem with name [${name}] not found.`)
517 |         }
518 | 
519 |         folderItems.push(item)
520 |     }
521 | 
522 |     const rootFolderItem = await project.getRootItem()
523 | 
524 |     execute(() => {
525 | 
526 |         let actions = []
527 | 
528 |         for(let folderItem of folderItems) {
529 |             let b = app.FolderItem.cast(binFolderItem)
530 |             let action = rootFolderItem.createMoveItemAction(folderItem, b)
531 |             actions.push(action)
532 |         }
533 | 
534 |         return actions
535 |     }, project)
536 | 
537 | }
538 | 
539 | const createBinInActiveProject = async (command) => {
540 |     const options = command.options;
541 |     const binName = options.binName;
542 | 
543 |     const project = await app.Project.getActiveProject()
544 |     const folderItem = await project.getRootItem()
545 | 
546 |     execute(() => {
547 |         let action = folderItem.createBinAction(binName, true)
548 |         return [action]
549 |     }, project)
550 | }
551 | 
552 | const exportSequence = async (command) => {
553 |     const options = command.options;
554 |     const sequenceId = options.sequenceId;
555 |     const outputPath = options.outputPath;
556 |     const presetPath = options.presetPath;
557 | 
558 |     const manager = await app.EncoderManager.getManager();
559 | 
560 |     const sequence = await _getSequenceFromId(sequenceId);
561 | 
562 |     await manager.exportSequence(sequence, constants.ExportType.IMMEDIATELY, outputPath, presetPath);
563 | }
564 | 
565 | const commandHandlers = {
566 |     exportSequence,
567 |     moveProjectItemsToBin,
568 |     createBinInActiveProject,
569 |     addMarkerToSequence,
570 |     closeGapsOnSequence,
571 |     removeItemFromSequence,
572 |     setClipStartEndTimes,
573 |     openProject,
574 |     saveProjectAs,
575 |     saveProject,
576 |     getProjectInfo,
577 |     setActiveSequence,
578 |     exportFrame,
579 |     setVideoClipProperties,
580 |     createSequenceFromMedia,
581 |     setAudioTrackMute,
582 |     setClipDisabled,
583 |     appendVideoTransition,
584 |     appendVideoFilter,
585 |     addMediaToSequence,
586 |     importMedia,
587 |     createProject,
588 | };
589 | 
590 | module.exports = {
591 |     commandHandlers
592 | }
```

--------------------------------------------------------------------------------
/uxp/ps/commands/layers.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* MIT License
  2 |  *
  3 |  * Copyright (c) 2025 Mike Chambers
  4 |  *
  5 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
  6 |  * of this software and associated documentation files (the "Software"), to deal
  7 |  * in the Software without restriction, including without limitation the rights
  8 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 |  * copies of the Software, and to permit persons to whom the Software is
 10 |  * furnished to do so, subject to the following conditions:
 11 |  *
 12 |  * The above copyright notice and this permission notice shall be included in all
 13 |  * copies or substantial portions of the Software.
 14 |  *
 15 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 |  * SOFTWARE.
 22 |  */
 23 | 
 24 | const { app, constants, action, imaging } = require("photoshop");
 25 | const fs = require("uxp").storage.localFileSystem;
 26 | 
 27 | const {
 28 |     setVisibleAllLayers,
 29 |     findLayer,
 30 |     execute,
 31 |     parseColor,
 32 |     getAnchorPosition,
 33 |     getInterpolationMethod,
 34 |     getBlendMode,
 35 |     getJustificationMode,
 36 |     selectLayer,
 37 |     hasActiveSelection,
 38 |     _saveDocumentAs,
 39 |     convertFontSize,
 40 |     convertFromPhotoshopFontSize
 41 | } = require("./utils");
 42 | 
 43 | 
 44 | // Function to capture visibility state
 45 | const _captureVisibilityState = (layers) => {
 46 |     const state = new Map();
 47 | 
 48 |     const capture = (layerSet) => {
 49 |         for (const layer of layerSet) {
 50 |             state.set(layer.id, layer.visible);
 51 |             if (layer.layers && layer.layers.length > 0) {
 52 |                 capture(layer.layers);
 53 |             }
 54 |         }
 55 |     };
 56 | 
 57 |     capture(layers);
 58 |     return state;
 59 | };
 60 | 
 61 | // Function to restore visibility state
 62 | const _restoreVisibilityState = async (state) => {
 63 |     const restore = (layerSet) => {
 64 |         for (const layer of layerSet) {
 65 |             if (state.has(layer.id)) {
 66 |                 layer.visible = state.get(layer.id);
 67 |             }
 68 | 
 69 |             if (layer.layers && layer.layers.length > 0) {
 70 |                 restore(layer.layers);
 71 |             }
 72 |         }
 73 |     };
 74 | 
 75 |     await execute(async () => {
 76 |         restore(app.activeDocument.layers);
 77 |     });
 78 | };
 79 | 
 80 | const exportLayersAsPng = async (command) => {
 81 |     let options = command.options;
 82 |     let layersInfo = options.layersInfo;
 83 | 
 84 |     const results = [];
 85 | 
 86 | 
 87 |     let originalState;
 88 |     await execute(async () => {
 89 |         originalState = _captureVisibilityState(app.activeDocument.layers);
 90 |         setVisibleAllLayers(false);
 91 |     });
 92 | 
 93 |     for (const info of layersInfo) {
 94 |         let result = {};
 95 | 
 96 |         let layer = findLayer(info.layerId);
 97 | 
 98 |         try {
 99 |             if (!layer) {
100 |                 throw new Error(
101 |                     `exportLayersAsPng: Could not find layer with ID: [${info.layerId}]` // Fixed error message
102 |                 );
103 |             }
104 |             await execute(async () => {
105 |                 layer.visible = true;
106 |             });
107 | 
108 |             let tmp = await _saveDocumentAs(info.filePath, "PNG");
109 | 
110 |             result = {
111 |                 ...tmp,
112 |                 layerId: info.layerId,
113 |                 success: true
114 |             };
115 | 
116 |         } catch (e) {
117 |             result = {
118 |                 ...info,
119 |                 success: false,
120 |                 message: e.message
121 |             };
122 |         } finally {
123 |             if (layer) {
124 |                 await execute(async () => {
125 |                     layer.visible = false;
126 |                 });
127 |             }
128 |         }
129 | 
130 |         results.push(result);
131 |     }
132 | 
133 |     await execute(async () => {
134 |         await _restoreVisibilityState(originalState);
135 |     })
136 | 
137 |     return results;
138 | };
139 | 
140 | const scaleLayer = async (command) => {
141 |     let options = command.options;
142 | 
143 |     let layerId = options.layerId;
144 |     let layer = findLayer(layerId);
145 | 
146 |     if (!layer) {
147 |         throw new Error(
148 |             `scaleLayer : Could not find layer with ID : [${layerId}]`
149 |         );
150 |     }
151 | 
152 |     await execute(async () => {
153 |         let anchor = getAnchorPosition(options.anchorPosition);
154 |         let interpolation = getInterpolationMethod(options.interpolationMethod);
155 | 
156 |         await layer.scale(options.width, options.height, anchor, {
157 |             interpolation: interpolation,
158 |         });
159 |     });
160 | };
161 | 
162 | const rotateLayer = async (command) => {
163 |     let options = command.options;
164 | 
165 |     let layerId = options.layerId;
166 |     let layer = findLayer(layerId);
167 | 
168 |     if (!layer) {
169 |         throw new Error(
170 |             `rotateLayer : Could not find layer with ID : [${layerId}]`
171 |         );
172 |     }
173 | 
174 |     await execute(async () => {
175 |         selectLayer(layer, true);
176 | 
177 |         let anchor = getAnchorPosition(options.anchorPosition);
178 |         let interpolation = getInterpolationMethod(options.interpolationMethod);
179 | 
180 |         await layer.rotate(options.angle, anchor, {
181 |             interpolation: interpolation,
182 |         });
183 |     });
184 | };
185 | 
186 | const flipLayer = async (command) => {
187 |     let options = command.options;
188 | 
189 |     let layerId = options.layerId;
190 |     let layer = findLayer(layerId);
191 | 
192 |     if (!layer) {
193 |         throw new Error(
194 |             `flipLayer : Could not find layer with ID : [${layerId}]`
195 |         );
196 |     }
197 | 
198 |     await execute(async () => {
199 |         await layer.flip(options.axis);
200 |     });
201 | };
202 | 
203 | const deleteLayer = async (command) => {
204 |     let options = command.options;
205 | 
206 |     let layerId = options.layerId;
207 |     let layer = findLayer(layerId);
208 | 
209 |     if (!layer) {
210 |         throw new Error(
211 |             `setLayerVisibility : Could not find layer with ID : [${layerId}]`
212 |         );
213 |     }
214 | 
215 |     await execute(async () => {
216 |         layer.delete();
217 |     });
218 | };
219 | 
220 | const renameLayer = async (command) => {
221 |     let options = command.options;
222 | 
223 |     let layerId = options.layerId;
224 |     let newLayerName = options.newLayerName;
225 | 
226 |     await _renameLayer(layerId, newLayerName)
227 | };
228 | 
229 | const _renameLayer = async (layerId, layerName) => {
230 | 
231 |     let layer = findLayer(layerId);
232 | 
233 |     if (!layer) {
234 |         throw new Error(
235 |             `_renameLayer : Could not find layer with ID : [${layerId}]`
236 |         );
237 |     }
238 | 
239 |     await execute(async () => {
240 |         layer.name = layerName;
241 |     });
242 | }
243 | 
244 | const renameLayers = async (command) => {
245 |     let options = command.options;
246 | 
247 |     let data = options.layerData;
248 | 
249 |     for(const d of data) {
250 |         await _renameLayer(d.layer_id, d.new_layer_name)
251 |     }
252 | };
253 | 
254 | const groupLayers = async (command) => {
255 |     let options = command.options;
256 |     const layerIds = options.layerIds;
257 | 
258 |     let layers = [];
259 | 
260 |     for (const layerId of layerIds) {
261 | 
262 |         let layer = findLayer(layerId);
263 | 
264 |         if (!layer) {
265 |             throw new Error(
266 |                 `groupLayers : Could not find layerId : ${layerId}`
267 |             );
268 |         }
269 | 
270 |         layers.push(layer);
271 |     }
272 | 
273 |     await execute(async () => {
274 |         await app.activeDocument.createLayerGroup({
275 |             name: options.groupName,
276 |             fromLayers: layers,
277 |         });
278 |     });
279 | };
280 | 
281 | const setLayerVisibility = async (command) => {
282 |     let options = command.options;
283 | 
284 |     let layerId = options.layerId;
285 |     let layer = findLayer(layerId);
286 | 
287 |     if (!layer) {
288 |         throw new Error(
289 |             `setLayerVisibility : Could not find layer with ID : [${layerId}]`
290 |         );
291 |     }
292 | 
293 |     await execute(async () => {
294 |         layer.visible = options.visible;
295 |     });
296 | };
297 | 
298 | const translateLayer = async (command) => {
299 |     let options = command.options;
300 | 
301 |     let layerId = options.layerId;
302 |     let layer = findLayer(layerId);
303 | 
304 |     if (!layer) {
305 |         throw new Error(
306 |             `translateLayer : Could not find layer with ID : [${layerId}]`
307 |         );
308 |     }
309 | 
310 |     await execute(async () => {
311 |         await layer.translate(options.xOffset, options.yOffset);
312 |     });
313 | };
314 | 
315 | const setLayerProperties = async (command) => {
316 |     let options = command.options;
317 | 
318 |     let layerId = options.layerId;
319 |     let layer = findLayer(layerId);
320 | 
321 |     if (!layer) {
322 |         throw new Error(
323 |             `setLayerProperties : Could not find layer with ID : [${layerId}]`
324 |         );
325 |     }
326 | 
327 |     await execute(async () => {
328 |         layer.blendMode = getBlendMode(options.blendMode);
329 |         layer.opacity = options.layerOpacity;
330 |         layer.fillOpacity = options.fillOpacity;
331 | 
332 |         if (layer.isClippingMask != options.isClippingMask) {
333 |             selectLayer(layer, true);
334 |             let command = options.isClippingMask
335 |                 ? {
336 |                     _obj: "groupEvent",
337 |                     _target: [
338 |                         {
339 |                             _enum: "ordinal",
340 |                             _ref: "layer",
341 |                             _value: "targetEnum",
342 |                         },
343 |                     ],
344 |                 }
345 |                 : {
346 |                     _obj: "ungroup",
347 |                     _target: [
348 |                         {
349 |                             _enum: "ordinal",
350 |                             _ref: "layer",
351 |                             _value: "targetEnum",
352 |                         },
353 |                     ],
354 |                 };
355 | 
356 |             await action.batchPlay([command], {});
357 |         }
358 |     });
359 | };
360 | 
361 | const duplicateLayer = async (command) => {
362 |     let options = command.options;
363 | 
364 |     await execute(async () => {
365 |         let layer = findLayer(options.sourceLayerId);
366 | 
367 |         if (!layer) {
368 |             throw new Error(
369 |                 `duplicateLayer : Could not find sourceLayerId : ${options.sourceLayerId}`
370 |             );
371 |         }
372 | 
373 |         let d = await layer.duplicate();
374 |         d.name = options.duplicateLayerName;
375 |     });
376 | };
377 | 
378 | const flattenAllLayers = async (command) => {
379 |     const options = command.options;
380 |     const layerName = options.layerName
381 | 
382 |     await execute(async () => {
383 |         await app.activeDocument.flatten();
384 | 
385 |         let layers = app.activeDocument.layers;
386 | 
387 |         if (layers.length != 1) {
388 |             throw new Error(`flattenAllLayers : Unknown error`);
389 |         }
390 | 
391 |         let l = layers[0];
392 |         l.allLocked = false;
393 |         l.name = layerName;
394 |     });
395 | };
396 | 
397 | const getLayerBounds = async (command) => {
398 |     let options = command.options;
399 |     let layerId = options.layerId;
400 | 
401 |     let layer = findLayer(layerId);
402 | 
403 |     if (!layer) {
404 |         throw new Error(
405 |             `getLayerBounds : Could not find layerId : ${layerId}`
406 |         );
407 |     }
408 | 
409 |     let b = layer.bounds;
410 |     return { left: b.left, top: b.top, bottom: b.bottom, right: b.right };
411 | };
412 | 
413 | const rasterizeLayer = async (command) => {
414 |     let options = command.options;
415 |     let layerId = options.layerId;
416 | 
417 |     let layer = findLayer(layerId);
418 | 
419 |     if (!layer) {
420 |         throw new Error(
421 |             `rasterizeLayer : Could not find layerId : ${layerId}`
422 |         );
423 |     }
424 | 
425 |     await execute(async () => {
426 |         layer.rasterize(constants.RasterizeType.ENTIRELAYER);
427 |     });
428 | };
429 | 
430 | const editTextLayer = async (command) => {
431 |     let options = command.options;
432 | 
433 |     let layerId = options.layerId;
434 |     let layer = findLayer(layerId);
435 | 
436 |     if (!layer) {
437 |         throw new Error(`editTextLayer : Could not find layerId : ${layerId}`);
438 |     }
439 | 
440 |     if (layer.kind.toUpperCase() != constants.LayerKind.TEXT.toUpperCase()) {
441 |         throw new Error(`editTextLayer : Layer type must be TEXT : ${layer.kind}`);
442 |     }
443 | 
444 |     await execute(async () => {
445 |         const contents = options.contents;
446 |         const fontSize = options.fontSize;
447 |         const textColor = options.textColor;
448 |         const fontName = options.fontName;
449 | 
450 | 
451 |         console.log("contents", options.contents)
452 |         console.log("fontSize", options.fontSize)
453 |         console.log("textColor", options.textColor)
454 |         console.log("fontName", options.fontName)
455 | 
456 |         if (contents != undefined) {
457 |             layer.textItem.contents = contents;
458 |         }
459 | 
460 |         if (fontSize != undefined) {
461 |             let s = convertFontSize(fontSize);
462 |             layer.textItem.characterStyle.size = s;
463 |         }
464 | 
465 |         if (textColor != undefined) {
466 |             let c = parseColor(textColor);
467 |             layer.textItem.characterStyle.color = c;
468 |         }
469 | 
470 |         if (fontName != undefined) {
471 |             layer.textItem.characterStyle.font = fontName;
472 |         }
473 |     });
474 | }
475 | 
476 | const moveLayer = async (command) => {
477 |     let options = command.options;
478 | 
479 |     let layerId = options.layerId;
480 |     let layer = findLayer(layerId);
481 | 
482 |     if (!layer) {
483 |         throw new Error(`moveLayer : Could not find layerId : ${layerId}`);
484 |     }
485 | 
486 |     let position;
487 |     switch (options.position) {
488 |         case "TOP":
489 |             position = "front";
490 |             break;
491 |         case "BOTTOM":
492 |             position = "back";
493 |             break;
494 |         case "UP":
495 |             position = "next";
496 |             break;
497 |         case "DOWN":
498 |             position = "previous";
499 |             break;
500 |         default:
501 |             throw new Error(
502 |                 `moveLayer: Unknown placement : ${options.position}`
503 |             );
504 |     }
505 | 
506 |     await execute(async () => {
507 |         selectLayer(layer, true);
508 | 
509 |         let commands = [
510 |             {
511 |                 _obj: "move",
512 |                 _target: [
513 |                     {
514 |                         _enum: "ordinal",
515 |                         _ref: "layer",
516 |                         _value: "targetEnum",
517 |                     },
518 |                 ],
519 |                 to: {
520 |                     _enum: "ordinal",
521 |                     _ref: "layer",
522 |                     _value: position,
523 |                 },
524 |             },
525 |         ];
526 | 
527 |         await action.batchPlay(commands, {});
528 |     });
529 | };
530 | 
531 | const createMultiLineTextLayer = async (command) => {
532 |     let options = command.options;
533 | 
534 |     await execute(async () => {
535 |         let c = parseColor(options.textColor);
536 | 
537 |         let fontSize = convertFontSize(options.fontSize);
538 | 
539 |         let contents = options.contents.replace(/\\n/g, "\n");
540 | 
541 |         let a = await app.activeDocument.createTextLayer({
542 |             //blendMode: constants.BlendMode.DISSOLVE,//ignored
543 |             textColor: c,
544 |             //color:constants.LabelColors.BLUE,//ignored
545 |             //opacity:50, //ignored
546 |             //name: "layer name",//ignored
547 |             contents: contents,
548 |             fontSize: fontSize,
549 |             fontName: options.fontName, //"ArialMT",
550 |             position: options.position, //y is the baseline of the text. Not top left
551 |         });
552 | 
553 |         //https://developer.adobe.com/photoshop/uxp/2022/ps_reference/classes/layer/
554 | 
555 |         a.blendMode = getBlendMode(options.blendMode);
556 |         a.name = options.layerName;
557 |         a.opacity = options.opacity;
558 | 
559 |         await a.textItem.convertToParagraphText();
560 |         a.textItem.paragraphStyle.justification = getJustificationMode(
561 |             options.justification
562 |         );
563 | 
564 |         selectLayer(a, true);
565 |         let commands = [
566 |             // Set current text layer
567 |             {
568 |                 _obj: "set",
569 |                 _target: [
570 |                     {
571 |                         _enum: "ordinal",
572 |                         _ref: "textLayer",
573 |                         _value: "targetEnum",
574 |                     },
575 |                 ],
576 |                 to: {
577 |                     _obj: "textLayer",
578 | 
579 |                     textShape: [
580 |                         {
581 |                             _obj: "textShape",
582 |                             bounds: {
583 |                                 _obj: "rectangle",
584 |                                 bottom: options.bounds.bottom,
585 |                                 left: options.bounds.left,
586 |                                 right: options.bounds.right,
587 |                                 top: options.bounds.top,
588 |                             },
589 |                             char: {
590 |                                 _enum: "char",
591 |                                 _value: "box",
592 |                             },
593 |                             columnCount: 1,
594 |                             columnGutter: {
595 |                                 _unit: "pointsUnit",
596 |                                 _value: 0.0,
597 |                             },
598 |                             firstBaselineMinimum: {
599 |                                 _unit: "pointsUnit",
600 |                                 _value: 0.0,
601 |                             },
602 |                             frameBaselineAlignment: {
603 |                                 _enum: "frameBaselineAlignment",
604 |                                 _value: "alignByAscent",
605 |                             },
606 |                             orientation: {
607 |                                 _enum: "orientation",
608 |                                 _value: "horizontal",
609 |                             },
610 |                             rowCount: 1,
611 |                             rowGutter: {
612 |                                 _unit: "pointsUnit",
613 |                                 _value: 0.0,
614 |                             },
615 |                             rowMajorOrder: true,
616 |                             spacing: {
617 |                                 _unit: "pointsUnit",
618 |                                 _value: 0.0,
619 |                             },
620 |                             transform: {
621 |                                 _obj: "transform",
622 |                                 tx: 0.0,
623 |                                 ty: 0.0,
624 |                                 xx: 1.0,
625 |                                 xy: 0.0,
626 |                                 yx: 0.0,
627 |                                 yy: 1.0,
628 |                             },
629 |                         },
630 |                     ],
631 |                 },
632 |             },
633 |         ];
634 | 
635 |         a.textItem.contents = contents;
636 |         await action.batchPlay(commands, {});
637 |     });
638 | };
639 | 
640 | const createSingleLineTextLayer = async (command) => {
641 |     let options = command.options;
642 | 
643 |     await execute(async () => {
644 |         let c = parseColor(options.textColor);
645 | 
646 |         let fontSize = convertFontSize(options.fontSize);
647 | 
648 |         let a = await app.activeDocument.createTextLayer({
649 |             //blendMode: constants.BlendMode.DISSOLVE,//ignored
650 |             textColor: c,
651 |             //color:constants.LabelColors.BLUE,//ignored
652 |             //opacity:50, //ignored
653 |             //name: "layer name",//ignored
654 |             contents: options.contents,
655 |             fontSize: fontSize,
656 |             fontName: options.fontName, //"ArialMT",
657 |             position: options.position, //y is the baseline of the text. Not top left
658 |         });
659 | 
660 |         //https://developer.adobe.com/photoshop/uxp/2022/ps_reference/classes/layer/
661 | 
662 |         a.blendMode = getBlendMode(options.blendMode);
663 |         a.name = options.layerName;
664 |         a.opacity = options.opacity;
665 |     });
666 | };
667 | 
668 | const createPixelLayer = async (command) => {
669 |     let options = command.options;
670 | 
671 |     await execute(async () => {
672 |         //let c = parseColor(options.textColor)
673 | 
674 |         let b = getBlendMode(options.blendMode);
675 | 
676 |         let a = await app.activeDocument.createPixelLayer({
677 |             name: options.layerName,
678 |             opacity: options.opacity,
679 |             fillNeutral: options.fillNeutral,
680 |             blendMode: b,
681 |         });
682 |     });
683 | };
684 | 
685 | 
686 | const getLayers = async (command) => {
687 |     let out = await execute(async () => {
688 |         let result = [];
689 | 
690 |         // Function to recursively process layers
691 |         const processLayers = (layersList) => {
692 |             let layersArray = [];
693 | 
694 |             for (let i = 0; i < layersList.length; i++) {
695 |                 let layer = layersList[i];
696 | 
697 |                 let kind = layer.kind.toUpperCase()
698 | 
699 |                 let layerInfo = {
700 |                     name: layer.name,
701 |                     type: kind,
702 |                     id: layer.id,
703 |                     isClippingMask: layer.isClippingMask,
704 |                     opacity: Math.round(layer.opacity),
705 |                     blendMode: layer.blendMode.toUpperCase(),
706 |                 };
707 | 
708 |                 if (kind == constants.LayerKind.TEXT.toUpperCase()) {
709 | 
710 |                     let _c = layer.textItem.characterStyle.color;
711 |                     let color = {
712 |                         red: Math.round(_c.rgb.red),
713 |                         green: Math.round(_c.rgb.green),
714 |                         blue: Math.round(_c.rgb.blue)
715 |                     }
716 | 
717 |                     layerInfo.textInfo = {
718 |                         fontSize: convertFromPhotoshopFontSize(layer.textItem.characterStyle.size),
719 |                         fontName: layer.textItem.characterStyle.font,
720 |                         fontColor: color,
721 |                         text: layer.textItem.contents,
722 |                         isMultiLineText: layer.textItem.isParagraphText
723 |                     }
724 |                 }
725 | 
726 | 
727 |                 // Check if this layer has sublayers (is a group)
728 |                 if (layer.layers && layer.layers.length > 0) {
729 |                     layerInfo.layers = processLayers(layer.layers);
730 |                 }
731 | 
732 |                 layersArray.push(layerInfo);
733 |             }
734 | 
735 |             return layersArray;
736 |         };
737 | 
738 |         // Start with the top-level layers
739 |         result = processLayers(app.activeDocument.layers);
740 | 
741 |         return result;
742 |     });
743 | 
744 |     return out;
745 | };
746 | 
747 | const removeLayerMask = async (command) => {
748 |     const options = command.options;
749 | 
750 |     const layerId = options.layerId;
751 |     const layer = findLayer(layerId);
752 | 
753 |     if (!layer) {
754 |         throw new Error(`removeLayerMask : Could not find layerId : ${layerId}`);
755 |     }
756 | 
757 |     await execute(async () => {
758 |         selectLayer(layer, true);
759 | 
760 |         let commands = [
761 |             // Delete mask channel
762 |             {
763 |                 _obj: "delete",
764 |                 _target: [
765 |                     {
766 |                         _enum: "channel",
767 |                         _ref: "channel",
768 |                         _value: "mask",
769 |                     },
770 |                 ],
771 |             },
772 |         ];
773 |         await action.batchPlay(commands, {});
774 |     });
775 | };
776 | 
777 | const addLayerMask = async (command) => {
778 |     if (!hasActiveSelection()) {
779 |         throw new Error("addLayerMask : Requires an active selection.");
780 |     }
781 | 
782 |     const options = command.options;
783 | 
784 |     const layerId = options.layerId;
785 |     const layer = findLayer(layerId);
786 | 
787 |     if (!layer) {
788 |         throw new Error(`addLayerMask : Could not find layerId : ${layerId}`);
789 |     }
790 | 
791 |     await execute(async () => {
792 |         selectLayer(layer, true);
793 | 
794 |         let commands = [
795 |             // Make
796 |             {
797 |                 _obj: "make",
798 |                 at: {
799 |                     _enum: "channel",
800 |                     _ref: "channel",
801 |                     _value: "mask",
802 |                 },
803 |                 new: {
804 |                     _class: "channel",
805 |                 },
806 |                 using: {
807 |                     _enum: "userMaskEnabled",
808 |                     _value: "revealSelection",
809 |                 },
810 |             },
811 |         ];
812 | 
813 |         await action.batchPlay(commands, {});
814 |     });
815 | };
816 | 
817 | const harmonizeLayer = async (command) => {
818 |     const options = command.options;
819 | 
820 |     const layerId = options.layerId;
821 |     const newLayerName = options.newLayerName;
822 |     const rasterizeLayer = options.rasterizeLayer;
823 | 
824 |     const layer = findLayer(layerId);
825 | 
826 |     if (!layer) {
827 |         throw new Error(`harmonizeLayer : Could not find layerId : ${layerId}`);
828 |     }
829 | 
830 |     await execute(async () => {
831 |         selectLayer(layer, true);
832 | 
833 |         let commands = [
834 |             {
835 |                 "_obj": "syntheticGenHarmonize",
836 |                 "_target": [
837 |                     {
838 |                         "_enum": "ordinal",
839 |                         "_ref": "document",
840 |                         "_value": "targetEnum"
841 |                     }
842 |                 ],
843 |                 "documentID": 60,
844 |                 "layerID": 7,
845 |                 "prompt": "",
846 |                 "serviceID": "gen_harmonize",
847 |                 "serviceOptionsList": {
848 |                     "clio": {
849 |                         "_obj": "clio",
850 |                         "dualCrop": true,
851 |                         "gi_ADVANCED": "{\"enable_mts\":true}",
852 |                         "gi_CONTENT_PRESERVE": 0,
853 |                         "gi_CROP": false,
854 |                         "gi_DILATE": false,
855 |                         "gi_ENABLE_PROMPT_FILTER": true,
856 |                         "gi_GUIDANCE": 6,
857 |                         "gi_MODE": "ginp",
858 |                         "gi_NUM_STEPS": -1,
859 |                         "gi_PROMPT": "",
860 |                         "gi_SEED": -1,
861 |                         "gi_SIMILARITY": 0
862 |                     },
863 |                     "gen_harmonize": {
864 |                         "_obj": "gen_harmonize",
865 |                         "dualCrop": true,
866 |                         "gi_SEED": -1
867 |                     }
868 |                 },
869 |                 "workflow": "gen_harmonize",
870 |                 "workflowType": {
871 |                     "_enum": "genWorkflow",
872 |                     "_value": "gen_harmonize"
873 |                 },
874 |                 "workflow_to_active_service_identifier_map": {
875 |                     "gen_harmonize": "gen_harmonize",
876 |                     "generate_background": "clio3",
877 |                     "generate_similar": "clio3",
878 |                     "generativeUpscale": "fal_aura_sr",
879 |                     "in_painting": "gen_harmonize",
880 |                     "instruct_edit": "clio3",
881 |                     "out_painting": "clio3",
882 |                     "text_to_image": "clio3"
883 |                 }
884 |             },
885 | 
886 |         ];
887 | 
888 | 
889 |         console.log(rasterizeLayer)
890 |         if(rasterizeLayer) {
891 |             commands.push({
892 |                 _obj: "rasterizeLayer",
893 |                 _target: [
894 |                     {
895 |                         _enum: "ordinal",
896 |                         _ref: "layer",
897 |                         _value: "targetEnum",
898 |                     },
899 |                 ],
900 |             })
901 |         }
902 | 
903 |         let o = await action.batchPlay(commands, {});
904 |         let layerId = o[0].layerID;
905 | 
906 |         let l = findLayer(layerId);
907 |         l.name = newLayerName;
908 |     });
909 | };
910 | 
911 | const getLayerImage = async (command) => {
912 | 
913 |     const options = command.options;
914 |     const layerId = options.layerId;
915 | 
916 |     const layer = findLayer(layerId);
917 | 
918 |     if (!layer) {
919 |         throw new Error(`harmonizeLayer : Could not find layerId : ${layerId}`);
920 |     }
921 | 
922 |     let out = await execute(async () => {
923 | 
924 |         const pixelsOpt = {
925 |             applyAlpha: true,
926 |             layerID:layerId
927 |         };
928 |         
929 |         const imgObj = await imaging.getPixels(pixelsOpt);
930 | 
931 |         const base64Data = await imaging.encodeImageData({
932 |             imageData: imgObj.imageData,
933 |             base64: true,
934 |         });
935 | 
936 |         const result = {
937 |             base64Image: base64Data,
938 |             dataUrl: `data:image/jpeg;base64,${base64Data}`,
939 |             width: imgObj.imageData.width,
940 |             height: imgObj.imageData.height,
941 |             colorSpace: imgObj.imageData.colorSpace,
942 |             components: imgObj.imageData.components,
943 |             format: "jpeg",
944 |         };
945 | 
946 |         imgObj.imageData.dispose();
947 |         return result;
948 |     });
949 | 
950 |     return out;
951 | };
952 | 
953 | const commandHandlers = {
954 |     renameLayers,
955 |     getLayerImage,
956 |     harmonizeLayer,
957 |     editTextLayer,
958 |     exportLayersAsPng,
959 |     removeLayerMask,
960 |     addLayerMask,
961 |     getLayers,
962 |     scaleLayer,
963 |     rotateLayer,
964 |     flipLayer,
965 |     deleteLayer,
966 |     renameLayer,
967 |     groupLayers,
968 |     setLayerVisibility,
969 |     translateLayer,
970 |     setLayerProperties,
971 |     duplicateLayer,
972 |     flattenAllLayers,
973 |     getLayerBounds,
974 |     rasterizeLayer,
975 |     moveLayer,
976 |     createMultiLineTextLayer,
977 |     createSingleLineTextLayer,
978 |     createPixelLayer,
979 | };
980 | 
981 | module.exports = {
982 |     commandHandlers,
983 | };
984 | 
```

--------------------------------------------------------------------------------
/mcp/pr-mcp.py:
--------------------------------------------------------------------------------

```python
  1 | # MIT License
  2 | #
  3 | # Copyright (c) 2025 Mike Chambers
  4 | #
  5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
  6 | # of this software and associated documentation files (the "Software"), to deal
  7 | # in the Software without restriction, including without limitation the rights
  8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 | # copies of the Software, and to permit persons to whom the Software is
 10 | # furnished to do so, subject to the following conditions:
 11 | #
 12 | # The above copyright notice and this permission notice shall be included in all
 13 | # copies or substantial portions of the Software.
 14 | #
 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 21 | # SOFTWARE.
 22 | 
 23 | from mcp.server.fastmcp import FastMCP, Image
 24 | from PIL import Image as PILImage
 25 | 
 26 | from core import init, sendCommand, createCommand
 27 | import socket_client
 28 | import sys
 29 | import tempfile
 30 | import os
 31 | import io
 32 | 
 33 | 
 34 | #logger.log(f"Python path: {sys.executable}")
 35 | #logger.log(f"PYTHONPATH: {os.environ.get('PYTHONPATH')}")
 36 | #logger.log(f"Current working directory: {os.getcwd()}")
 37 | #logger.log(f"Sys.path: {sys.path}")
 38 | 
 39 | 
 40 | mcp_name = "Adobe Premiere MCP Server"
 41 | mcp = FastMCP(mcp_name, log_level="ERROR")
 42 | print(f"{mcp_name} running on stdio", file=sys.stderr)
 43 | 
 44 | APPLICATION = "premiere"
 45 | PROXY_URL = 'http://localhost:3001'
 46 | PROXY_TIMEOUT = 20
 47 | 
 48 | socket_client.configure(
 49 |     app=APPLICATION, 
 50 |     url=PROXY_URL,
 51 |     timeout=PROXY_TIMEOUT
 52 | )
 53 | 
 54 | init(APPLICATION, socket_client)
 55 | 
 56 | @mcp.tool()
 57 | def get_project_info():
 58 |     """
 59 |     Returns info on the currently active project in Premiere Pro.
 60 |     """
 61 | 
 62 |     command = createCommand("getProjectInfo", {
 63 |     })
 64 | 
 65 |     return sendCommand(command)
 66 | 
 67 | @mcp.tool()
 68 | def save_project():
 69 |     """
 70 |     Saves the active project in Premiere Pro.
 71 |     """
 72 | 
 73 |     command = createCommand("saveProject", {
 74 |     })
 75 | 
 76 |     return sendCommand(command)
 77 | 
 78 | @mcp.tool()
 79 | def save_project_as(file_path: str):
 80 |     """Saves the current Premiere project to the specified location.
 81 |     
 82 |     Args:
 83 |         file_path (str): The absolute path (including filename) where the file will be saved.
 84 |             Example: "/Users/username/Documents/project.prproj"
 85 | 
 86 |     """
 87 |     
 88 |     command = createCommand("saveProjectAs", {
 89 |         "filePath":file_path
 90 |     })
 91 | 
 92 |     return sendCommand(command)
 93 | 
 94 | @mcp.tool()
 95 | def open_project(file_path: str):
 96 |     """Opens the Premiere project at the specified path.
 97 |     
 98 |     Args:
 99 |         file_path (str): The absolute path (including filename) of the Premiere Pro project to open.
100 |             Example: "/Users/username/Documents/project.prproj"
101 | 
102 |     """
103 |     
104 |     command = createCommand("openProject", {
105 |         "filePath":file_path
106 |     })
107 | 
108 |     return sendCommand(command)
109 | 
110 | 
111 | @mcp.tool()
112 | def create_project(directory_path: str, project_name: str):
113 |     """
114 |     Create a new Premiere project.
115 | 
116 |     Creates a new Adobe Premiere project file, saves it to the specified location and then opens it in Premiere.
117 | 
118 |     The function initializes an empty project with default settings.
119 | 
120 |     Args:
121 |         directory_path (str): The full path to the directory where the project file will be saved. This directory must exist before calling the function.
122 |         project_name (str): The name to be given to the project file. The '.prproj' extension will be added.
123 |     """
124 | 
125 |     command = createCommand("createProject", {
126 |         "path":directory_path,
127 |         "name":project_name
128 |     })
129 | 
130 |     return sendCommand(command)
131 | 
132 | 
133 | @mcp.tool()
134 | def create_bin_in_active_project(bin_name:str):
135 |     """
136 |     Creates a new bin / folder in the root project.
137 | 
138 |     Args:
139 |         name (str) : The name of the bin to be created
140 |  
141 | 
142 |     """
143 | 
144 |     command = createCommand("createBinInActiveProject", {
145 |         "binName": bin_name
146 |     })
147 | 
148 |     return sendCommand(command)
149 | 
150 | @mcp.tool()
151 | def export_sequence(sequence_id: str, output_path: str, preset_path: str):
152 |     """
153 |     Exports a Premiere Pro sequence to a video file using specified export settings.
154 | 
155 |     This function renders and exports the specified sequence from the active Premiere Pro project
156 |     to a video file on the file system. The export process uses a preset file to determine
157 |     encoding settings, resolution, format, and other export parameters.
158 | 
159 |     Args:
160 |         sequence_id (str): The unique identifier of the sequence to export.
161 |             This should be the ID of an existing sequence in the current Premiere Pro project.
162 |             
163 |         output_path (str): The complete file system path where the exported video will be saved.
164 |             Must include the full directory path, filename, and appropriate file extension.
165 |             
166 |         preset_path (str): The file system path to the export preset file (.epr) that defines the export settings including codec, resolution, bitrate, and format.
167 |         
168 |         IMPORTANT: The export may take an extended period of time, so if the call times out, it most likely means the export is still in progress.
169 |     """
170 |     command = createCommand("exportSequence", {
171 |         "sequenceId": sequence_id,
172 |         "outputPath": output_path,
173 |         "presetPath": preset_path
174 |     })
175 |     
176 |     return sendCommand(command)
177 | 
178 | @mcp.tool()
179 | def move_project_items_to_bin(item_names: list[str], bin_name: str):
180 |     """
181 |     Moves specified project items to an existing bin/folder in the project.
182 | 
183 |     Args:
184 |         item_names (list[str]): A list of names of project items to move to the specified bin.
185 |             These should be the exact names of items as they appear in the project.
186 |         bin_name (str): The name of the existing bin to move the project items to.
187 |             The bin must already exist in the project.
188 |             
189 |     Returns:
190 |         dict: Response from the Premiere Pro operation indicating success status.
191 |         
192 |     Raises:
193 |         RuntimeError: If the bin doesn't exist, items don't exist, or the operation fails.
194 |         
195 |     Example:
196 |         move_project_items_to_bin(
197 |             item_names=["video1.mp4", "audio1.wav", "image1.png"], 
198 |             bin_name="Media Assets"
199 |         )
200 |     """
201 |     command = createCommand("moveProjectItemsToBin", {
202 |         "itemNames": item_names,
203 |         "binName": bin_name
204 |     })
205 | 
206 |     return sendCommand(command)
207 | 
208 | @mcp.tool()
209 | def set_audio_track_mute(sequence_id:str, audio_track_index: int, mute: bool):
210 |     """
211 |     Sets the mute property on the specified audio track. If mute is true, all clips on the track will be muted and not played.
212 | 
213 |     Args:
214 |         sequence_id (str) : The id of the sequence on which to set the audio track mute.
215 |         audio_track_index (int): The index of the audio track to mute or unmute. Indices start at 0 for the first audio track.
216 |         mute (bool): Whether the track should be muted.
217 |             - True: Mutes the track (audio will not be played)
218 |             - False: Unmutes the track (audio will be played normally)
219 | 
220 |     """
221 | 
222 |     command = createCommand("setAudioTrackMute", {
223 |         "sequenceId": sequence_id,
224 |         "audioTrackIndex":audio_track_index,
225 |         "mute":mute
226 |     })
227 | 
228 |     return sendCommand(command)
229 | 
230 | 
231 | @mcp.tool()
232 | def set_active_sequence(sequence_id: str):
233 |     """
234 |     Sets the sequence with the specified id as the active sequence within Premiere Pro (currently selected and visible in timeline)
235 |     
236 |     Args:
237 |         sequence_id (str): ID for the sequence to be set as active
238 |     """
239 | 
240 |     command = createCommand("setActiveSequence", {
241 |         "sequenceId":sequence_id
242 |     })
243 | 
244 |     return sendCommand(command)
245 | 
246 | 
247 | @mcp.tool()
248 | def create_sequence_from_media(item_names: list[str], sequence_name: str = "default"):
249 |     """
250 |     Creates a new sequence from the specified project items, placing clips on the timeline in the order they are provided.
251 |     
252 |     If there is not an active sequence the newly created sequence will be set as the active sequence when created.
253 |     
254 |     Args:
255 |         item_names (list[str]): A list of project item names to include in the sequence in the desired order.
256 |         sequence_name (str, optional): The name to give the new sequence. Defaults to "default".
257 |     """
258 | 
259 | 
260 |     command = createCommand("createSequenceFromMedia", {
261 |         "itemNames":item_names,
262 |         "sequenceName":sequence_name
263 |     })
264 | 
265 |     return sendCommand(command)
266 | 
267 | @mcp.tool()
268 | def close_gaps_on_sequence(sequence_id: str, track_index: int, track_type: str):
269 |     """
270 |     Closes gaps on the specified track(s) in a sequence's timeline.
271 | 
272 |     This function removes empty spaces (gaps) between clips on the timeline by moving
273 |     clips leftward to fill any empty areas. This is useful for cleaning up the timeline
274 |     after removing clips or when clips have been moved leaving gaps.
275 | 
276 |     Args:
277 |         sequence_id (str): The ID of the sequence to close gaps on.
278 |         track_index (int): The index of the track to close gaps on.
279 |             Track indices start at 0 for the first track and increment upward.
280 |             For video tracks, this refers to video track indices.
281 |             For audio tracks, this refers to audio track indices.
282 |         track_type (str): Specifies which type of tracks to close gaps on.
283 |             Valid values:
284 |             - "VIDEO": Close gaps only on the specified video track
285 |             - "AUDIO": Close gaps only on the specified audio track  
286 | 
287 |     """
288 |     
289 |     command = createCommand("closeGapsOnSequence", {
290 |         "sequenceId": sequence_id,
291 |         "trackIndex": track_index,
292 |         "trackType": track_type,
293 |     })
294 | 
295 |     return sendCommand(command)
296 | 
297 | 
298 | @mcp.tool()
299 | def remove_item_from_sequence(sequence_id: str, track_index:int, track_item_index: int, track_type:str, ripple_delete:bool=True):
300 |     """
301 |     Removes a specified media item from the sequence's timeline.
302 | 
303 |     Args:
304 |         sequence_id (str): The id for the sequence to remove the media from
305 |         track_index (int): The index of the track containing the target clip.
306 |             Track indices start at 0 for the first track and increment upward.
307 |         track_item_index (int): The index of the clip within the track to remove.
308 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
309 |         track_type (str): Specifies which type of tracks being removed.
310 |             Valid values:
311 |             - "VIDEO": Close gaps only on the specified video track
312 |             - "AUDIO": Close gaps only on the specified audio track
313 |         ripple_delete (bool, optional): Whether to perform a ripple delete operation. Defaults to True.
314 |             - True: Removes the clip and shifts all subsequent clips leftward to close the gap
315 |             - False: Removes the clip but leaves a gap in the timeline where the clip was located
316 |     """
317 |     
318 |     command = createCommand("removeItemFromSequence", {
319 |         "sequenceId": sequence_id,
320 |         "trackItemIndex":track_item_index,
321 |         "trackIndex":track_index,
322 |         "trackType":track_type,
323 |         "rippleDelete":ripple_delete
324 |     })
325 | 
326 |     return sendCommand(command)
327 | 
328 | @mcp.tool()
329 | def add_marker_to_sequence(sequence_id: str, 
330 |                            marker_name: str, 
331 |                            start_time_ticks: int, 
332 |                            duration_ticks: int, 
333 |                            comments: str,
334 |                            marker_type: str = "Comment"):
335 |     """
336 |     Adds a marker to the specified sequence.
337 | 
338 |     Args:
339 |         sequence_id (str): 
340 |             The ID of the sequence to which the marker will be added.
341 | 
342 |         marker_name (str): 
343 |             The name/title of the marker.
344 | 
345 |         start_time_ticks (int): 
346 |             The timeline position where the marker starts, in ticks.
347 |             (1 tick = 1/254016000000 of a day)
348 | 
349 |         duration_ticks (int): 
350 |             The length of the marker in ticks.
351 | 
352 |         comments (str): 
353 |             Optional text comment to store in the marker.
354 | 
355 |         marker_type (str, optional): 
356 |             The type of marker to add. Defaults to "Comment".
357 |             
358 |             Supported marker types include:
359 |                 - "Comment"      → General-purpose note marker.
360 | 
361 |     """
362 | 
363 |     command = createCommand("addMarkerToSequence", {
364 |         "sequenceId": sequence_id,
365 |         "markerName": marker_name,
366 |         "startTimeTicks": start_time_ticks,
367 |         "durationTicks": duration_ticks,
368 |         "comments": comments,
369 |         "markerType": marker_type
370 |     })
371 | 
372 |     return sendCommand(command)
373 | 
374 | 
375 | 
376 | @mcp.tool()
377 | def add_media_to_sequence(sequence_id:str, item_name: str, video_track_index: int, audio_track_index: int, insertion_time_ticks: int = 0, overwrite: bool = True):
378 |     """
379 |     Adds a specified media item to the active sequence's timeline.
380 | 
381 |     Args:
382 |         sequence_id (str) : The id for the sequence to add the media to
383 |         item_name (str): The name or identifier of the media item to add.
384 |         video_track_index (int): The index of the video track where the item should be inserted.
385 |         audio_track_index (int): The index of the audio track where the item should be inserted.
386 |         insertion_time_ticks (int): The position on the timeline in ticks, with 0 being the beginning. The API will return positions of existing clips in ticks
387 |         overwrite (bool, optional): Whether to overwrite existing content at the insertion point. Defaults to True. If False, any existing clips that overlap will be split and item inserted.
388 |     """
389 | 
390 | 
391 |     command = createCommand("addMediaToSequence", {
392 |         "sequenceId": sequence_id,
393 |         "itemName":item_name,
394 |         "videoTrackIndex":video_track_index,
395 |         "audioTrackIndex":audio_track_index,
396 |         "insertionTimeTicks":insertion_time_ticks,
397 |         "overwrite":overwrite
398 |     })
399 | 
400 |     return sendCommand(command)
401 | 
402 | 
403 | @mcp.tool()
404 | def set_clip_disabled(sequence_id:str, track_index: int, track_item_index: int, track_type:str, disabled: bool):
405 |     """
406 |     Enables or disables a clip in the timeline.
407 |     
408 |     Args:
409 |         sequence_id (str): The id for the sequence to set the clip disabled property.
410 |         track_index (int): The index of the track containing the target clip.
411 |             Track indices start at 0 for the first track and increment upward.
412 |             For video tracks, this refers to video track indices.
413 |             For audio tracks, this refers to audio track indices.
414 |         track_item_index (int): The index of the clip within the track to enable/disable.
415 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
416 |         track_type (str): Specifies which type of track to modify.
417 |             Valid values:
418 |             - "VIDEO": Modify clips on the specified video track
419 |             - "AUDIO": Modify clips on the specified audio track
420 |         disabled (bool): Whether to disable the clip.
421 |             - True: Disables the clip (clip will not be visible during playback or export)
422 |             - False: Enables the clip (normal visibility)
423 |     """
424 | 
425 |     command = createCommand("setClipDisabled", {
426 |         "sequenceId": sequence_id,
427 |         "trackIndex":track_index,
428 |         "trackItemIndex":track_item_index,
429 |         "trackType":track_type,
430 |         "disabled":disabled
431 |     })
432 | 
433 |     return sendCommand(command)
434 | 
435 | 
436 | @mcp.tool()
437 | def set_clip_start_end_times(
438 |     sequence_id: str, track_index: int, track_item_index: int, start_time_ticks: int, 
439 |         end_time_ticks: int, track_type: str):
440 |     """
441 |     Sets the start and end time boundaries for a specified clip in the timeline.
442 |     
443 |     This function allows you to modify the duration and timing of video clips, audio clips, 
444 |     and images that are already placed in the timeline by adjusting their in and out points. 
445 |     The clip can be trimmed to a shorter duration or extended to a longer duration.
446 |     
447 |     Args:
448 |         sequence_id (str): The id for the sequence containing the clip to modify.
449 |         track_index (int): The index of the track containing the target clip.
450 |             Track indices start at 0 for the first track and increment upward.
451 |             For video tracks, this refers to video track indices.
452 |             For audio tracks, this refers to audio track indices.
453 |         track_item_index (int): The index of the clip within the track to modify.
454 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
455 |         start_time_ticks (int): The new start time for the clip in ticks.
456 |         end_time_ticks (int): The new end time for the clip in ticks.
457 |         track_type (str): Specifies which type of tracks to modify clips on.
458 |             Valid values:
459 |             - "VIDEO": Modify clips only on the specified video track
460 |             - "AUDIO": Modify clips only on the specified audio track  
461 |         
462 |     Note:
463 |         - To trim a clip: Set start/end times within the original clip's duration
464 |         - To extend a clip: Set end time beyond the original clip's duration  
465 |         - Works with video clips, audio clips, and image files (like PSD files)
466 |         - Times are specified in ticks (Premiere Pro's internal time unit)
467 |     """
468 | 
469 |     command = createCommand("setClipStartEndTimes", {
470 |         "sequenceId": sequence_id,
471 |         "trackIndex": track_index,
472 |         "trackItemIndex": track_item_index,
473 |         "startTimeTicks": start_time_ticks,
474 |         "endTimeTicks": end_time_ticks,
475 |         "trackType": track_type
476 |     })
477 | 
478 |     return sendCommand(command)
479 | 
480 | @mcp.tool()
481 | def add_black_and_white_effect(sequence_id:str, video_track_index: int, track_item_index: int):
482 |     """
483 |     Adds a black and white effect to a clip at the specified track and position.
484 |     
485 |     Args:
486 |         sequence_id (str) : The id for the sequence to add the effect to
487 |         video_track_index (int): The index of the video track containing the target clip.
488 |             Track indices start at 0 for the first video track and increment upward.
489 |         track_item_index (int): The index of the clip within the track to apply the effect to.
490 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
491 |     """
492 | 
493 |     command = createCommand("appendVideoFilter", {
494 |         "sequenceId": sequence_id,
495 |         "videoTrackIndex":video_track_index,
496 |         "trackItemIndex":track_item_index,
497 |         "effectName":"AE.ADBE Black & White",
498 |         "properties":[
499 |         ]
500 |     })
501 | 
502 |     return sendCommand(command)
503 | 
504 | @mcp.tool()
505 | def get_sequence_frame_image(sequence_id: str, seconds: int):
506 |     """Returns a jpeg of the specified timestamp in the specified sequence in Premiere pro as an MCP Image object that can be displayed."""
507 |     
508 |     temp_dir = tempfile.gettempdir()
509 |     file_path = os.path.join(temp_dir, f"frame_{sequence_id}_{seconds}.png")
510 |     
511 |     command = createCommand("exportFrame", {
512 |         "sequenceId": sequence_id,
513 |         "filePath": file_path,
514 |         "seconds": seconds
515 |     })
516 |     
517 |     result = sendCommand(command)
518 |     
519 |     if not result.get("status") == "SUCCESS":
520 |         return result
521 |     
522 |     file_path = result["response"]["filePath"]
523 |     
524 |     with open(file_path, 'rb') as f:
525 |         png_image = PILImage.open(f)
526 |         
527 |         # Convert to RGB if necessary (removes alpha channel)
528 |         if png_image.mode in ("RGBA", "LA", "P"):
529 |             rgb_image = PILImage.new("RGB", png_image.size, (255, 255, 255))
530 |             rgb_image.paste(png_image, mask=png_image.split()[-1] if png_image.mode == "RGBA" else None)
531 |             png_image = rgb_image
532 |         
533 |         # Save as JPEG to bytes buffer
534 |         jpeg_buffer = io.BytesIO()
535 |         png_image.save(jpeg_buffer, format="JPEG", quality=85, optimize=True)
536 |         jpeg_bytes = jpeg_buffer.getvalue()
537 |     
538 |     image = Image(data=jpeg_bytes, format="jpeg")
539 |     
540 |     del result["response"]
541 |     
542 |     try:
543 |         os.remove(file_path)
544 |     except FileNotFoundError:
545 |         pass
546 |     
547 |     return [result, image]
548 | 
549 | @mcp.tool()
550 | def export_frame(sequence_id:str, file_path: str, seconds: int):
551 |     """Captures a specific frame from the sequence at the given timestamp
552 |     and exports it as a PNG or JPG (depending on file extension) image file to the specified path.
553 |     
554 |     Args:
555 |         sequence_id (str) : The id for the sequence to export the frame from
556 |         file_path (str): The destination path where the exported PNG / JPG image will be saved.
557 |             Must include the full directory path and filename with .png or .jpg extension.
558 |         seconds (int): The timestamp in seconds from the beginning of the sequence
559 |             where the frame should be captured. The frame closest to this time position
560 |             will be extracted.
561 |     """
562 |     
563 |     command = createCommand("exportFrame", {
564 |         "sequenceId": sequence_id,
565 |         "filePath": file_path,
566 |         "seconds":seconds
567 |         }
568 |     )
569 | 
570 |     return sendCommand(command)
571 | 
572 | 
573 | @mcp.tool()
574 | def add_gaussian_blur_effect(sequence_id: str, video_track_index: int, track_item_index: int, blurriness: float, blur_dimensions: str = "HORIZONTAL_VERTICAL"):
575 |     """
576 |     Adds a gaussian blur effect to a clip at the specified track and position.
577 | 
578 |     Args:
579 |         sequence_id (str) : The id for the sequence to add the effect to
580 |         video_track_index (int): The index of the video track containing the target clip.
581 |             Track indices start at 0 for the first video track and increment upward.
582 |             
583 |         track_item_index (int): The index of the clip within the track to apply the effect to.
584 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
585 |             
586 |         blurriness (float): The intensity of the blur effect. Higher values create stronger blur.
587 |             Recommended range is between 0.0 and 100.0 (Max 3000).
588 |             
589 |         blur_dimensions (str, optional): The direction of the blur effect. Defaults to "HORIZONTAL_VERTICAL".
590 |             Valid options are:
591 |             - "HORIZONTAL_VERTICAL": Blur in all directions
592 |             - "HORIZONTAL": Blur only horizontally
593 |             - "VERTICAL": Blur only vertically
594 |     """
595 |     dimensions = {"HORIZONTAL_VERTICAL": 0, "HORIZONTAL": 1, "VERTICAL": 2}
596 |     
597 |     # Validate blur_dimensions parameter
598 |     if blur_dimensions not in dimensions:
599 |         raise ValueError(f"Invalid blur_dimensions. ")
600 | 
601 |     command = createCommand("appendVideoFilter", {
602 |         "sequenceId": sequence_id,
603 |         "videoTrackIndex": video_track_index,
604 |         "trackItemIndex": track_item_index,
605 |         "effectName": "AE.ADBE Gaussian Blur 2",
606 |         "properties": [
607 |             {"name": "Blur Dimensions", "value": dimensions[blur_dimensions]},
608 |             {"name": "Blurriness", "value": blurriness}
609 |         ]
610 |     })
611 | 
612 |     return sendCommand(command)
613 | 
614 | def rgb_to_premiere_color3(rgb_color, alpha=1.0):
615 |     """Converts RGB (0–255) dict to Premiere Pro color format [r, g, b, a] with floats (0.0–1.0)."""
616 |     return [
617 |         rgb_color["red"] / 255.0,
618 |         rgb_color["green"] / 255.0,
619 |         rgb_color["blue"] / 255.0,
620 |         alpha
621 |     ]
622 | 
623 | def rgb_to_premiere_color(rgb_color, alpha=255):
624 |     """
625 |     Converts an RGB(A) dict (0–255) to a 64-bit Premiere Pro color parameter (as int).
626 |     Matches Adobe's internal ARGB 16-bit fixed-point format.
627 |     """
628 |     def to16bit(value):
629 |         return int(round(value * 256))
630 | 
631 |     r16 = to16bit(rgb_color["red"] / 255.0)
632 |     g16 = to16bit(rgb_color["green"] / 255.0)
633 |     b16 = to16bit(rgb_color["blue"] / 255.0)
634 |     a16 = to16bit(alpha / 255.0)
635 | 
636 |     high = (a16 << 16) | r16       # top 32 bits: A | R
637 |     low = (g16 << 16) | b16        # bottom 32 bits: G | B
638 | 
639 |     packed_color = (high << 32) | low
640 |     return packed_color
641 | 
642 | 
643 | 
644 | @mcp.tool()
645 | def add_tint_effect(sequence_id: str, video_track_index: int, track_item_index: int, black_map:dict = {"red":0, "green":0, "blue":0}, white_map:dict = {"red":255, "green":255, "blue":255}, amount:int = 100):
646 |     """
647 |     Adds the tint effect to a clip at the specified track and position.
648 |     
649 |     This function applies a tint effect that maps the dark and light areas of the clip to specified colors.
650 |     
651 |     Args:
652 |         sequence_id (str) : The id for the sequence to add the effect to
653 |         video_track_index (int): The index of the video track containing the target clip.
654 |             Track indices start at 0 for the first video track and increment upward.
655 |             
656 |         track_item_index (int): The index of the clip within the track to apply the effect to.
657 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
658 |             
659 |         black_map (dict): The RGB color values to map black/dark areas to, with keys "red", "green", and "blue".
660 |             Default is {"red":0, "green":0, "blue":0} (pure black).
661 |             
662 |         white_map (dict): The RGB color values to map white/light areas to, with keys "red", "green", and "blue".
663 |             Default is {"red":255, "green":255, "blue":255} (pure white).
664 |             
665 |         amount (int): The intensity of the tint effect as a percentage, ranging from 0 to 100.
666 |             Default is 100 (full tint effect).
667 |     """
668 | 
669 |     command = createCommand("appendVideoFilter", {
670 |         "sequenceId": sequence_id,
671 |         "videoTrackIndex":video_track_index,
672 |         "trackItemIndex":track_item_index,
673 |         "effectName":"AE.ADBE Tint",
674 |         "properties":[
675 |             #{"name":"Map White To", "value":rgb_to_premiere_color(white_map)},
676 |             #{"name":"Map Black To", "value":rgb_to_premiere_color(black_map)}
677 |             {"name":"Map Black To", "value":rgb_to_premiere_color(black_map)}
678 |             #{"name":"Amount to Tint", "value":amount / 100}
679 |         ]
680 |     })
681 | 
682 |     return sendCommand(command)
683 | 
684 | 
685 | 
686 | @mcp.tool()
687 | def add_motion_blur_effect(sequence_id: str, video_track_index: int, track_item_index: int, direction: int, length: int):
688 |     """
689 |     Adds the directional blur effect to a clip at the specified track and position.
690 |     
691 |     This function applies a motion blur effect that simulates movement in a specific direction.
692 |     
693 |     Args:
694 |         sequence_id (str) : The id for the sequence to add the effect to
695 |         video_track_index (int): The index of the video track containing the target clip.
696 |             Track indices start at 0 for the first video track and increment upward.
697 |             
698 |         track_item_index (int): The index of the clip within the track to apply the effect to.
699 |             Clip indices start at 0 for the first clip in the track and increment from left to right.
700 |             
701 |         direction (int): The angle of the directional blur in degrees, ranging from 0 to 360.
702 |             - 0/360: Vertical blur upward
703 |             - 90: Horizontal blur to the right 
704 |             - 180: Vertical blur downward
705 |             - 270: Horizontal blur to the left
706 |             
707 |         length (int): The intensity or distance of the blur effect, ranging from 0 to 1000.
708 |     """
709 | 
710 |     command = createCommand("appendVideoFilter", {
711 |         "sequenceId": sequence_id,
712 |         "videoTrackIndex":video_track_index,
713 |         "trackItemIndex":track_item_index,
714 |         "effectName":"AE.ADBE Motion Blur",
715 |         "properties":[
716 |             {"name":"Direction", "value":direction},
717 |             {"name":"Blur Length", "value":length}
718 |         ]
719 |     })
720 | 
721 |     return sendCommand(command)
722 | 
723 | @mcp.tool()
724 | def append_video_transition(sequence_id: str, video_track_index: int, track_item_index: int, transition_name: str, duration: float = 1.0, clip_alignment: float = 0.5):
725 |     """
726 |     Creates a transition between the specified clip and the adjacent clip on the timeline.
727 |     
728 |     In general, you should keep transitions short (no more than 2 seconds is a good rule).
729 | 
730 |     Args:
731 |         sequence_id (str) : The id for the sequence to add the transition to
732 |         video_track_index (int): The index of the video track containing the target clips.
733 |         track_item_index (int): The index of the clip within the track to apply the transition to.
734 |         transition_name (str): The name of the transition to apply. Must be a valid transition name (see below).
735 |         duration (float): The duration of the transition in seconds.
736 |         clip_alignment (float): Controls how the transition is distributed between the two clips.
737 |                                 Range: 0.0 to 1.0, where:
738 |                                 - 0.0 places transition entirely on the right (later) clip
739 |                                 - 0.5 centers the transition equally between both clips (default)
740 |                                 - 1.0 places transition entirely on the left (earlier) clip
741 |  
742 |     Valid Transition Names:
743 |         Basic Transitions (ADBE):
744 |             - "ADBE Additive Dissolve"
745 |             - "ADBE Cross Zoom"
746 |             - "ADBE Cube Spin"
747 |             - "ADBE Film Dissolve"
748 |             - "ADBE Flip Over"
749 |             - "ADBE Gradient Wipe"
750 |             - "ADBE Iris Cross"
751 |             - "ADBE Iris Diamond"
752 |             - "ADBE Iris Round"
753 |             - "ADBE Iris Square"
754 |             - "ADBE Page Peel"
755 |             - "ADBE Push"
756 |             - "ADBE Slide"
757 |             - "ADBE Wipe"
758 |             
759 |         After Effects Transitions (AE.ADBE):
760 |             - "AE.ADBE Center Split"
761 |             - "AE.ADBE Inset"
762 |             - "AE.ADBE Cross Dissolve New"
763 |             - "AE.ADBE Dip To White"
764 |             - "AE.ADBE Split"
765 |             - "AE.ADBE Whip"
766 |             - "AE.ADBE Non-Additive Dissolve"
767 |             - "AE.ADBE Dip To Black"
768 |             - "AE.ADBE Barn Doors"
769 |             - "AE.ADBE MorphCut"
770 |     """
771 | 
772 |     command = createCommand("appendVideoTransition", {
773 |         "sequenceId": sequence_id,
774 |         "videoTrackIndex":video_track_index,
775 |         "trackItemIndex":track_item_index,
776 |         "transitionName":transition_name,
777 |         "clipAlignment":clip_alignment,
778 |         "duration":duration
779 |     })
780 | 
781 |     return sendCommand(command)
782 | 
783 | 
784 | @mcp.tool()
785 | def set_video_clip_properties(sequence_id: str, video_track_index: int, track_item_index: int, opacity: int = 100, blend_mode: str = "NORMAL"):
786 |     """
787 |     Sets opacity and blend mode properties for a video clip in the timeline.
788 | 
789 |     This function modifies the visual properties of a specific clip located on a specific video track
790 |     in the active Premiere Pro sequence. The clip is identified by its track index and item index
791 |     within that track.
792 | 
793 |     Args:
794 |         sequence_id (str) : The id for the sequence to set the video clip properties
795 |         video_track_index (int): The index of the video track containing the target clip.
796 |             Track indices start at 0 for the first video track.
797 |         track_item_index (int): The index of the clip within the track to modify.
798 |             Clip indices start at 0 for the first clip on the track.
799 |         opacity (int, optional): The opacity value to set for the clip, as a percentage.
800 |             Valid values range from 0 (completely transparent) to 100 (completely opaque).
801 |             Defaults to 100.
802 |         blend_mode (str, optional): The blend mode to apply to the clip.
803 |             Must be one of the valid blend modes supported by Premiere Pro.
804 |             Defaults to "NORMAL".
805 |     """
806 | 
807 |     command = createCommand("setVideoClipProperties", {
808 |         "sequenceId": sequence_id,
809 |         "videoTrackIndex":video_track_index,
810 |         "trackItemIndex":track_item_index,
811 |         "opacity":opacity,
812 |         "blendMode":blend_mode
813 |     })
814 | 
815 |     return sendCommand(command)
816 | 
817 | @mcp.tool()
818 | def import_media(file_paths:list):
819 |     """
820 |     Imports a list of media files into the active Premiere project.
821 | 
822 |     Args:
823 |         file_paths (list): A list of file paths (strings) to import into the project.
824 |             Each path should be a complete, valid path to a media file supported by Premiere Pro.
825 |     """
826 | 
827 |     command = createCommand("importMedia", {
828 |         "filePaths":file_paths
829 |     })
830 | 
831 |     return sendCommand(command)
832 | 
833 | @mcp.resource("config://get_instructions")
834 | def get_instructions() -> str:
835 |     """Read this first! Returns information and instructions on how to use Photoshop and this API"""
836 | 
837 |     return f"""
838 |     You are a Premiere Pro and video expert who is creative and loves to help other people learn to use Premiere and create.
839 | 
840 |     Rules to follow:
841 | 
842 |     1. Think deeply about how to solve the task
843 |     2. Always check your work
844 |     3. Read the info for the API calls to make sure you understand the requirements and arguments
845 |     4. In general, add clips first, then effects, then transitions
846 |     5. As a general rule keep transitions short (no more that 2 seconds is a good rule), and there should not be a gap between clips (or else the transition may not work)
847 | 
848 |     IMPORTANT: To create a new project and add clips:
849 |     1. Create new project (create_project)
850 |     2. Add media to the project (import_media)
851 |     3. Create a new sequence with media (should always add video / image clips before audio.(create_sequence_from_media). This will create a sequence with the clips.
852 |     4. The first clip you add will determine the dimensions / resolution of the sequence
853 | 
854 |     Here are some general tips for when working with Premiere.
855 | 
856 |     Audio and Video clips are added on separate Audio / Video tracks, which you can access via their index.
857 | 
858 |     When adding a video clip that contains audio, the audio will be placed on a separate audio track.
859 | 
860 |     Once added you currently cannot remove a clip (audio or video) but you can disable it.
861 | 
862 |     If you want to do a transition between two clips, the clips must be on the same track and there should not be a gap between them. Place the transition of the first clip.
863 | 
864 |     Video clips with a higher track index will overlap and hide those with lower index if they overlap.
865 | 
866 |     When adding images to a sequence, they will have a duration of 5 seconds.
867 | 
868 |     blend_modes: {", ".join(BLEND_MODES)}
869 |     """
870 | 
871 | 
872 | BLEND_MODES = [
873 |     "COLOR",
874 |     "COLORBURN",
875 |     "COLORDODGE",
876 |     "DARKEN",
877 |     "DARKERCOLOR",
878 |     "DIFFERENCE",
879 |     "DISSOLVE",
880 |     "EXCLUSION",
881 |     "HARDLIGHT",
882 |     "HARDMIX",
883 |     "HUE",
884 |     "LIGHTEN",
885 |     "LIGHTERCOLOR",
886 |     "LINEARBURN",
887 |     "LINEARDODGE",
888 |     "LINEARLIGHT",
889 |     "LUMINOSITY",
890 |     "MULTIPLY",
891 |     "NORMAL",
892 |     "OVERLAY",
893 |     "PINLIGHT",
894 |     "SATURATION",
895 |     "SCREEN",
896 |     "SOFTLIGHT",
897 |     "VIVIDLIGHT",
898 |     "SUBTRACT",
899 |     "DIVIDE"
900 | ]
```
Page 2/6FirstPrevNextLast