#
tokens: 40057/50000 1/17 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 3. Use http://codebase.md/sonnylazuardi/cursor-talk-to-figma-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── bun.lock
├── Dockerfile
├── DRAGME.md
├── LICENSE
├── package.json
├── readme.md
├── scripts
│   └── setup.sh
├── smithery.yaml
├── src
│   ├── cursor_mcp_plugin
│   │   ├── code.js
│   │   ├── manifest.json
│   │   ├── setcharacters.js
│   │   └── ui.html
│   ├── socket.ts
│   └── talk_to_figma_mcp
│       ├── bun.lock
│       ├── package.json
│       ├── server.ts
│       └── tsconfig.json
├── tsconfig.json
└── tsup.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/code.js:
--------------------------------------------------------------------------------

```javascript
   1 | // This is the main code file for the Cursor MCP Figma plugin
   2 | // It handles Figma API commands
   3 | 
   4 | // Plugin state
   5 | const state = {
   6 |   serverPort: 3055, // Default port
   7 | };
   8 | 
   9 | 
  10 | // Helper function for progress updates
  11 | function sendProgressUpdate(
  12 |   commandId,
  13 |   commandType,
  14 |   status,
  15 |   progress,
  16 |   totalItems,
  17 |   processedItems,
  18 |   message,
  19 |   payload = null
  20 | ) {
  21 |   const update = {
  22 |     type: "command_progress",
  23 |     commandId,
  24 |     commandType,
  25 |     status,
  26 |     progress,
  27 |     totalItems,
  28 |     processedItems,
  29 |     message,
  30 |     timestamp: Date.now(),
  31 |   };
  32 | 
  33 |   // Add optional chunk information if present
  34 |   if (payload) {
  35 |     if (
  36 |       payload.currentChunk !== undefined &&
  37 |       payload.totalChunks !== undefined
  38 |     ) {
  39 |       update.currentChunk = payload.currentChunk;
  40 |       update.totalChunks = payload.totalChunks;
  41 |       update.chunkSize = payload.chunkSize;
  42 |     }
  43 |     update.payload = payload;
  44 |   }
  45 | 
  46 |   // Send to UI
  47 |   figma.ui.postMessage(update);
  48 |   console.log(`Progress update: ${status} - ${progress}% - ${message}`);
  49 | 
  50 |   return update;
  51 | }
  52 | 
  53 | // Show UI
  54 | figma.showUI(__html__, { width: 350, height: 450 });
  55 | 
  56 | // Plugin commands from UI
  57 | figma.ui.onmessage = async (msg) => {
  58 |   switch (msg.type) {
  59 |     case "update-settings":
  60 |       updateSettings(msg);
  61 |       break;
  62 |     case "notify":
  63 |       figma.notify(msg.message);
  64 |       break;
  65 |     case "close-plugin":
  66 |       figma.closePlugin();
  67 |       break;
  68 |     case "execute-command":
  69 |       // Execute commands received from UI (which gets them from WebSocket)
  70 |       try {
  71 |         const result = await handleCommand(msg.command, msg.params);
  72 |         // Send result back to UI
  73 |         figma.ui.postMessage({
  74 |           type: "command-result",
  75 |           id: msg.id,
  76 |           result,
  77 |         });
  78 |       } catch (error) {
  79 |         figma.ui.postMessage({
  80 |           type: "command-error",
  81 |           id: msg.id,
  82 |           error: error.message || "Error executing command",
  83 |         });
  84 |       }
  85 |       break;
  86 |   }
  87 | };
  88 | 
  89 | // Listen for plugin commands from menu
  90 | figma.on("run", ({ command }) => {
  91 |   figma.ui.postMessage({ type: "auto-connect" });
  92 | });
  93 | 
  94 | // Update plugin settings
  95 | function updateSettings(settings) {
  96 |   if (settings.serverPort) {
  97 |     state.serverPort = settings.serverPort;
  98 |   }
  99 | 
 100 |   figma.clientStorage.setAsync("settings", {
 101 |     serverPort: state.serverPort,
 102 |   });
 103 | }
 104 | 
 105 | // Handle commands from UI
 106 | async function handleCommand(command, params) {
 107 |   switch (command) {
 108 |     case "get_document_info":
 109 |       return await getDocumentInfo();
 110 |     case "get_selection":
 111 |       return await getSelection();
 112 |     case "get_node_info":
 113 |       if (!params || !params.nodeId) {
 114 |         throw new Error("Missing nodeId parameter");
 115 |       }
 116 |       return await getNodeInfo(params.nodeId);
 117 |     case "get_nodes_info":
 118 |       if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) {
 119 |         throw new Error("Missing or invalid nodeIds parameter");
 120 |       }
 121 |       return await getNodesInfo(params.nodeIds);
 122 |     case "read_my_design":
 123 |       return await readMyDesign();
 124 |     case "create_rectangle":
 125 |       return await createRectangle(params);
 126 |     case "create_frame":
 127 |       return await createFrame(params);
 128 |     case "create_text":
 129 |       return await createText(params);
 130 |     case "set_fill_color":
 131 |       return await setFillColor(params);
 132 |     case "set_stroke_color":
 133 |       return await setStrokeColor(params);
 134 |     case "move_node":
 135 |       return await moveNode(params);
 136 |     case "resize_node":
 137 |       return await resizeNode(params);
 138 |     case "delete_node":
 139 |       return await deleteNode(params);
 140 |     case "delete_multiple_nodes":
 141 |       return await deleteMultipleNodes(params);
 142 |     case "get_styles":
 143 |       return await getStyles();
 144 |     case "get_local_components":
 145 |       return await getLocalComponents();
 146 |     // case "get_team_components":
 147 |     //   return await getTeamComponents();
 148 |     case "create_component_instance":
 149 |       return await createComponentInstance(params);
 150 |     case "export_node_as_image":
 151 |       return await exportNodeAsImage(params);
 152 |     case "set_corner_radius":
 153 |       return await setCornerRadius(params);
 154 |     case "set_text_content":
 155 |       return await setTextContent(params);
 156 |     case "clone_node":
 157 |       return await cloneNode(params);
 158 |     case "scan_text_nodes":
 159 |       return await scanTextNodes(params);
 160 |     case "set_multiple_text_contents":
 161 |       return await setMultipleTextContents(params);
 162 |     case "get_annotations":
 163 |       return await getAnnotations(params);
 164 |     case "set_annotation":
 165 |       return await setAnnotation(params);
 166 |     case "scan_nodes_by_types":
 167 |       return await scanNodesByTypes(params);
 168 |     case "set_multiple_annotations":
 169 |       return await setMultipleAnnotations(params);
 170 |     case "get_instance_overrides":
 171 |       // Check if instanceNode parameter is provided
 172 |       if (params && params.instanceNodeId) {
 173 |         // Get the instance node by ID
 174 |         const instanceNode = await figma.getNodeByIdAsync(params.instanceNodeId);
 175 |         if (!instanceNode) {
 176 |           throw new Error(`Instance node not found with ID: ${params.instanceNodeId}`);
 177 |         }
 178 |         return await getInstanceOverrides(instanceNode);
 179 |       }
 180 |       // Call without instance node if not provided
 181 |       return await getInstanceOverrides();
 182 | 
 183 |     case "set_instance_overrides":
 184 |       // Check if instanceNodeIds parameter is provided
 185 |       if (params && params.targetNodeIds) {
 186 |         // Validate that targetNodeIds is an array
 187 |         if (!Array.isArray(params.targetNodeIds)) {
 188 |           throw new Error("targetNodeIds must be an array");
 189 |         }
 190 | 
 191 |         // Get the instance nodes by IDs
 192 |         const targetNodes = await getValidTargetInstances(params.targetNodeIds);
 193 |         if (!targetNodes.success) {
 194 |           figma.notify(targetNodes.message);
 195 |           return { success: false, message: targetNodes.message };
 196 |         }
 197 | 
 198 |         if (params.sourceInstanceId) {
 199 | 
 200 |           // get source instance data
 201 |           let sourceInstanceData = null;
 202 |           sourceInstanceData = await getSourceInstanceData(params.sourceInstanceId);
 203 | 
 204 |           if (!sourceInstanceData.success) {
 205 |             figma.notify(sourceInstanceData.message);
 206 |             return { success: false, message: sourceInstanceData.message };
 207 |           }
 208 |           return await setInstanceOverrides(targetNodes.targetInstances, sourceInstanceData);
 209 |         } else {
 210 |           throw new Error("Missing sourceInstanceId parameter");
 211 |         }
 212 |       }
 213 |     case "set_layout_mode":
 214 |       return await setLayoutMode(params);
 215 |     case "set_padding":
 216 |       return await setPadding(params);
 217 |     case "set_axis_align":
 218 |       return await setAxisAlign(params);
 219 |     case "set_layout_sizing":
 220 |       return await setLayoutSizing(params);
 221 |     case "set_item_spacing":
 222 |       return await setItemSpacing(params);
 223 |     case "get_reactions":
 224 |       if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) {
 225 |         throw new Error("Missing or invalid nodeIds parameter");
 226 |       }
 227 |       return await getReactions(params.nodeIds);  
 228 |     case "set_default_connector":
 229 |       return await setDefaultConnector(params);
 230 |     case "create_connections":
 231 |       return await createConnections(params);
 232 |     case "set_focus":
 233 |       return await setFocus(params);
 234 |     case "set_selections":
 235 |       return await setSelections(params);
 236 |     default:
 237 |       throw new Error(`Unknown command: ${command}`);
 238 |   }
 239 | }
 240 | 
 241 | // Command implementations
 242 | 
 243 | async function getDocumentInfo() {
 244 |   await figma.currentPage.loadAsync();
 245 |   const page = figma.currentPage;
 246 |   return {
 247 |     name: page.name,
 248 |     id: page.id,
 249 |     type: page.type,
 250 |     children: page.children.map((node) => ({
 251 |       id: node.id,
 252 |       name: node.name,
 253 |       type: node.type,
 254 |     })),
 255 |     currentPage: {
 256 |       id: page.id,
 257 |       name: page.name,
 258 |       childCount: page.children.length,
 259 |     },
 260 |     pages: [
 261 |       {
 262 |         id: page.id,
 263 |         name: page.name,
 264 |         childCount: page.children.length,
 265 |       },
 266 |     ],
 267 |   };
 268 | }
 269 | 
 270 | async function getSelection() {
 271 |   return {
 272 |     selectionCount: figma.currentPage.selection.length,
 273 |     selection: figma.currentPage.selection.map((node) => ({
 274 |       id: node.id,
 275 |       name: node.name,
 276 |       type: node.type,
 277 |       visible: node.visible,
 278 |     })),
 279 |   };
 280 | }
 281 | 
 282 | function rgbaToHex(color) {
 283 |   var r = Math.round(color.r * 255);
 284 |   var g = Math.round(color.g * 255);
 285 |   var b = Math.round(color.b * 255);
 286 |   var a = color.a !== undefined ? Math.round(color.a * 255) : 255;
 287 | 
 288 |   if (a === 255) {
 289 |     return (
 290 |       "#" +
 291 |       [r, g, b]
 292 |         .map((x) => {
 293 |           return x.toString(16).padStart(2, "0");
 294 |         })
 295 |         .join("")
 296 |     );
 297 |   }
 298 | 
 299 |   return (
 300 |     "#" +
 301 |     [r, g, b, a]
 302 |       .map((x) => {
 303 |         return x.toString(16).padStart(2, "0");
 304 |       })
 305 |       .join("")
 306 |   );
 307 | }
 308 | 
 309 | function filterFigmaNode(node) {
 310 |   if (node.type === "VECTOR") {
 311 |     return null;
 312 |   }
 313 | 
 314 |   var filtered = {
 315 |     id: node.id,
 316 |     name: node.name,
 317 |     type: node.type,
 318 |   };
 319 | 
 320 |   if (node.fills && node.fills.length > 0) {
 321 |     filtered.fills = node.fills.map((fill) => {
 322 |       var processedFill = Object.assign({}, fill);
 323 |       delete processedFill.boundVariables;
 324 |       delete processedFill.imageRef;
 325 | 
 326 |       if (processedFill.gradientStops) {
 327 |         processedFill.gradientStops = processedFill.gradientStops.map(
 328 |           (stop) => {
 329 |             var processedStop = Object.assign({}, stop);
 330 |             if (processedStop.color) {
 331 |               processedStop.color = rgbaToHex(processedStop.color);
 332 |             }
 333 |             delete processedStop.boundVariables;
 334 |             return processedStop;
 335 |           }
 336 |         );
 337 |       }
 338 | 
 339 |       if (processedFill.color) {
 340 |         processedFill.color = rgbaToHex(processedFill.color);
 341 |       }
 342 | 
 343 |       return processedFill;
 344 |     });
 345 |   }
 346 | 
 347 |   if (node.strokes && node.strokes.length > 0) {
 348 |     filtered.strokes = node.strokes.map((stroke) => {
 349 |       var processedStroke = Object.assign({}, stroke);
 350 |       delete processedStroke.boundVariables;
 351 |       if (processedStroke.color) {
 352 |         processedStroke.color = rgbaToHex(processedStroke.color);
 353 |       }
 354 |       return processedStroke;
 355 |     });
 356 |   }
 357 | 
 358 |   if (node.cornerRadius !== undefined) {
 359 |     filtered.cornerRadius = node.cornerRadius;
 360 |   }
 361 | 
 362 |   if (node.absoluteBoundingBox) {
 363 |     filtered.absoluteBoundingBox = node.absoluteBoundingBox;
 364 |   }
 365 | 
 366 |   if (node.characters) {
 367 |     filtered.characters = node.characters;
 368 |   }
 369 | 
 370 |   if (node.style) {
 371 |     filtered.style = {
 372 |       fontFamily: node.style.fontFamily,
 373 |       fontStyle: node.style.fontStyle,
 374 |       fontWeight: node.style.fontWeight,
 375 |       fontSize: node.style.fontSize,
 376 |       textAlignHorizontal: node.style.textAlignHorizontal,
 377 |       letterSpacing: node.style.letterSpacing,
 378 |       lineHeightPx: node.style.lineHeightPx,
 379 |     };
 380 |   }
 381 | 
 382 |   if (node.children) {
 383 |     filtered.children = node.children
 384 |       .map((child) => {
 385 |         return filterFigmaNode(child);
 386 |       })
 387 |       .filter((child) => {
 388 |         return child !== null;
 389 |       });
 390 |   }
 391 | 
 392 |   return filtered;
 393 | }
 394 | 
 395 | async function getNodeInfo(nodeId) {
 396 |   const node = await figma.getNodeByIdAsync(nodeId);
 397 | 
 398 |   if (!node) {
 399 |     throw new Error(`Node not found with ID: ${nodeId}`);
 400 |   }
 401 | 
 402 |   const response = await node.exportAsync({
 403 |     format: "JSON_REST_V1",
 404 |   });
 405 | 
 406 |   return filterFigmaNode(response.document);
 407 | }
 408 | 
 409 | async function getNodesInfo(nodeIds) {
 410 |   try {
 411 |     // Load all nodes in parallel
 412 |     const nodes = await Promise.all(
 413 |       nodeIds.map((id) => figma.getNodeByIdAsync(id))
 414 |     );
 415 | 
 416 |     // Filter out any null values (nodes that weren't found)
 417 |     const validNodes = nodes.filter((node) => node !== null);
 418 | 
 419 |     // Export all valid nodes in parallel
 420 |     const responses = await Promise.all(
 421 |       validNodes.map(async (node) => {
 422 |         const response = await node.exportAsync({
 423 |           format: "JSON_REST_V1",
 424 |         });
 425 |         return {
 426 |           nodeId: node.id,
 427 |           document: filterFigmaNode(response.document),
 428 |         };
 429 |       })
 430 |     );
 431 | 
 432 |     return responses;
 433 |   } catch (error) {
 434 |     throw new Error(`Error getting nodes info: ${error.message}`);
 435 |   }
 436 | }
 437 | 
 438 | async function getReactions(nodeIds) {
 439 |   try {
 440 |     const commandId = generateCommandId();
 441 |     sendProgressUpdate(
 442 |       commandId,
 443 |       "get_reactions",
 444 |       "started",
 445 |       0,
 446 |       nodeIds.length,
 447 |       0,
 448 |       `Starting deep search for reactions in ${nodeIds.length} nodes and their children`
 449 |     );
 450 | 
 451 |     // Function to find nodes with reactions from the node and all its children
 452 |     async function findNodesWithReactions(node, processedNodes = new Set(), depth = 0, results = []) {
 453 |       // Skip already processed nodes (prevent circular references)
 454 |       if (processedNodes.has(node.id)) {
 455 |         return results;
 456 |       }
 457 |       
 458 |       processedNodes.add(node.id);
 459 |       
 460 |       // Check if the current node has reactions
 461 |       let filteredReactions = [];
 462 |       if (node.reactions && node.reactions.length > 0) {
 463 |         // Filter out reactions with navigation === 'CHANGE_TO'
 464 |         filteredReactions = node.reactions.filter(r => {
 465 |           // Some reactions may have action or actions array
 466 |           if (r.action && r.action.navigation === 'CHANGE_TO') return false;
 467 |           if (Array.isArray(r.actions)) {
 468 |             // If any action in actions array is CHANGE_TO, exclude
 469 |             return !r.actions.some(a => a.navigation === 'CHANGE_TO');
 470 |           }
 471 |           return true;
 472 |         });
 473 |       }
 474 |       const hasFilteredReactions = filteredReactions.length > 0;
 475 |       
 476 |       // If the node has filtered reactions, add it to results and apply highlight effect
 477 |       if (hasFilteredReactions) {
 478 |         results.push({
 479 |           id: node.id,
 480 |           name: node.name,
 481 |           type: node.type,
 482 |           depth: depth,
 483 |           hasReactions: true,
 484 |           reactions: filteredReactions,
 485 |           path: getNodePath(node)
 486 |         });
 487 |         // Apply highlight effect (orange border)
 488 |         await highlightNodeWithAnimation(node);
 489 |       }
 490 |       
 491 |       // If node has children, recursively search them
 492 |       if (node.children) {
 493 |         for (const child of node.children) {
 494 |           await findNodesWithReactions(child, processedNodes, depth + 1, results);
 495 |         }
 496 |       }
 497 |       
 498 |       return results;
 499 |     }
 500 |     
 501 |     // Function to apply animated highlight effect to a node
 502 |     async function highlightNodeWithAnimation(node) {
 503 |       // Save original stroke properties
 504 |       const originalStrokeWeight = node.strokeWeight;
 505 |       const originalStrokes = node.strokes ? [...node.strokes] : [];
 506 |       
 507 |       try {
 508 |         // Apply orange border stroke
 509 |         node.strokeWeight = 4;
 510 |         node.strokes = [{
 511 |           type: 'SOLID',
 512 |           color: { r: 1, g: 0.5, b: 0 }, // Orange color
 513 |           opacity: 0.8
 514 |         }];
 515 |         
 516 |         // Set timeout for animation effect (restore to original after 1.5 seconds)
 517 |         setTimeout(() => {
 518 |           try {
 519 |             // Restore original stroke properties
 520 |             node.strokeWeight = originalStrokeWeight;
 521 |             node.strokes = originalStrokes;
 522 |           } catch (restoreError) {
 523 |             console.error(`Error restoring node stroke: ${restoreError.message}`);
 524 |           }
 525 |         }, 1500);
 526 |       } catch (highlightError) {
 527 |         console.error(`Error highlighting node: ${highlightError.message}`);
 528 |         // Continue even if highlighting fails
 529 |       }
 530 |     }
 531 |     
 532 |     // Get node hierarchy path as a string
 533 |     function getNodePath(node) {
 534 |       const path = [];
 535 |       let current = node;
 536 |       
 537 |       while (current && current.parent) {
 538 |         path.unshift(current.name);
 539 |         current = current.parent;
 540 |       }
 541 |       
 542 |       return path.join(' > ');
 543 |     }
 544 | 
 545 |     // Array to store all results
 546 |     let allResults = [];
 547 |     let processedCount = 0;
 548 |     const totalCount = nodeIds.length;
 549 |     
 550 |     // Iterate through each node and its children to search for reactions
 551 |     for (let i = 0; i < nodeIds.length; i++) {
 552 |       try {
 553 |         const nodeId = nodeIds[i];
 554 |         const node = await figma.getNodeByIdAsync(nodeId);
 555 |         
 556 |         if (!node) {
 557 |           processedCount++;
 558 |           sendProgressUpdate(
 559 |             commandId,
 560 |             "get_reactions",
 561 |             "in_progress",
 562 |             processedCount / totalCount,
 563 |             totalCount,
 564 |             processedCount,
 565 |             `Node not found: ${nodeId}`
 566 |           );
 567 |           continue;
 568 |         }
 569 |         
 570 |         // Search for reactions in the node and its children
 571 |         const processedNodes = new Set();
 572 |         const nodeResults = await findNodesWithReactions(node, processedNodes);
 573 |         
 574 |         // Add results
 575 |         allResults = allResults.concat(nodeResults);
 576 |         
 577 |         // Update progress
 578 |         processedCount++;
 579 |         sendProgressUpdate(
 580 |           commandId,
 581 |           "get_reactions",
 582 |           "in_progress",
 583 |           processedCount / totalCount,
 584 |           totalCount,
 585 |           processedCount,
 586 |           `Processed node ${processedCount}/${totalCount}, found ${nodeResults.length} nodes with reactions`
 587 |         );
 588 |       } catch (error) {
 589 |         processedCount++;
 590 |         sendProgressUpdate(
 591 |           commandId,
 592 |           "get_reactions",
 593 |           "in_progress",
 594 |           processedCount / totalCount,
 595 |           totalCount,
 596 |           processedCount,
 597 |           `Error processing node: ${error.message}`
 598 |         );
 599 |       }
 600 |     }
 601 | 
 602 |     // Completion update
 603 |     sendProgressUpdate(
 604 |       commandId,
 605 |       "get_reactions",
 606 |       "completed",
 607 |       1,
 608 |       totalCount,
 609 |       totalCount,
 610 |       `Completed deep search: found ${allResults.length} nodes with reactions.`
 611 |     );
 612 | 
 613 |     return {
 614 |       nodesCount: nodeIds.length,
 615 |       nodesWithReactions: allResults.length,
 616 |       nodes: allResults
 617 |     };
 618 |   } catch (error) {
 619 |     throw new Error(`Failed to get reactions: ${error.message}`);
 620 |   }
 621 | }
 622 | 
 623 | async function readMyDesign() {
 624 |   try {
 625 |     // Load all selected nodes in parallel
 626 |     const nodes = await Promise.all(
 627 |       figma.currentPage.selection.map((node) => figma.getNodeByIdAsync(node.id))
 628 |     );
 629 | 
 630 |     // Filter out any null values (nodes that weren't found)
 631 |     const validNodes = nodes.filter((node) => node !== null);
 632 | 
 633 |     // Export all valid nodes in parallel
 634 |     const responses = await Promise.all(
 635 |       validNodes.map(async (node) => {
 636 |         const response = await node.exportAsync({
 637 |           format: "JSON_REST_V1",
 638 |         });
 639 |         return {
 640 |           nodeId: node.id,
 641 |           document: filterFigmaNode(response.document),
 642 |         };
 643 |       })
 644 |     );
 645 | 
 646 |     return responses;
 647 |   } catch (error) {
 648 |     throw new Error(`Error getting nodes info: ${error.message}`);
 649 |   }
 650 | }
 651 | 
 652 | async function createRectangle(params) {
 653 |   const {
 654 |     x = 0,
 655 |     y = 0,
 656 |     width = 100,
 657 |     height = 100,
 658 |     name = "Rectangle",
 659 |     parentId,
 660 |   } = params || {};
 661 | 
 662 |   const rect = figma.createRectangle();
 663 |   rect.x = x;
 664 |   rect.y = y;
 665 |   rect.resize(width, height);
 666 |   rect.name = name;
 667 | 
 668 |   // If parentId is provided, append to that node, otherwise append to current page
 669 |   if (parentId) {
 670 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 671 |     if (!parentNode) {
 672 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 673 |     }
 674 |     if (!("appendChild" in parentNode)) {
 675 |       throw new Error(`Parent node does not support children: ${parentId}`);
 676 |     }
 677 |     parentNode.appendChild(rect);
 678 |   } else {
 679 |     figma.currentPage.appendChild(rect);
 680 |   }
 681 | 
 682 |   return {
 683 |     id: rect.id,
 684 |     name: rect.name,
 685 |     x: rect.x,
 686 |     y: rect.y,
 687 |     width: rect.width,
 688 |     height: rect.height,
 689 |     parentId: rect.parent ? rect.parent.id : undefined,
 690 |   };
 691 | }
 692 | 
 693 | async function createFrame(params) {
 694 |   const {
 695 |     x = 0,
 696 |     y = 0,
 697 |     width = 100,
 698 |     height = 100,
 699 |     name = "Frame",
 700 |     parentId,
 701 |     fillColor,
 702 |     strokeColor,
 703 |     strokeWeight,
 704 |     layoutMode = "NONE",
 705 |     layoutWrap = "NO_WRAP",
 706 |     paddingTop = 10,
 707 |     paddingRight = 10,
 708 |     paddingBottom = 10,
 709 |     paddingLeft = 10,
 710 |     primaryAxisAlignItems = "MIN",
 711 |     counterAxisAlignItems = "MIN",
 712 |     layoutSizingHorizontal = "FIXED",
 713 |     layoutSizingVertical = "FIXED",
 714 |     itemSpacing = 0,
 715 |   } = params || {};
 716 | 
 717 |   const frame = figma.createFrame();
 718 |   frame.x = x;
 719 |   frame.y = y;
 720 |   frame.resize(width, height);
 721 |   frame.name = name;
 722 | 
 723 |   // Set layout mode if provided
 724 |   if (layoutMode !== "NONE") {
 725 |     frame.layoutMode = layoutMode;
 726 |     frame.layoutWrap = layoutWrap;
 727 | 
 728 |     // Set padding values only when layoutMode is not NONE
 729 |     frame.paddingTop = paddingTop;
 730 |     frame.paddingRight = paddingRight;
 731 |     frame.paddingBottom = paddingBottom;
 732 |     frame.paddingLeft = paddingLeft;
 733 | 
 734 |     // Set axis alignment only when layoutMode is not NONE
 735 |     frame.primaryAxisAlignItems = primaryAxisAlignItems;
 736 |     frame.counterAxisAlignItems = counterAxisAlignItems;
 737 | 
 738 |     // Set layout sizing only when layoutMode is not NONE
 739 |     frame.layoutSizingHorizontal = layoutSizingHorizontal;
 740 |     frame.layoutSizingVertical = layoutSizingVertical;
 741 | 
 742 |     // Set item spacing only when layoutMode is not NONE
 743 |     frame.itemSpacing = itemSpacing;
 744 |   }
 745 | 
 746 |   // Set fill color if provided
 747 |   if (fillColor) {
 748 |     const paintStyle = {
 749 |       type: "SOLID",
 750 |       color: {
 751 |         r: parseFloat(fillColor.r) || 0,
 752 |         g: parseFloat(fillColor.g) || 0,
 753 |         b: parseFloat(fillColor.b) || 0,
 754 |       },
 755 |       opacity: parseFloat(fillColor.a) || 1,
 756 |     };
 757 |     frame.fills = [paintStyle];
 758 |   }
 759 | 
 760 |   // Set stroke color and weight if provided
 761 |   if (strokeColor) {
 762 |     const strokeStyle = {
 763 |       type: "SOLID",
 764 |       color: {
 765 |         r: parseFloat(strokeColor.r) || 0,
 766 |         g: parseFloat(strokeColor.g) || 0,
 767 |         b: parseFloat(strokeColor.b) || 0,
 768 |       },
 769 |       opacity: parseFloat(strokeColor.a) || 1,
 770 |     };
 771 |     frame.strokes = [strokeStyle];
 772 |   }
 773 | 
 774 |   // Set stroke weight if provided
 775 |   if (strokeWeight !== undefined) {
 776 |     frame.strokeWeight = strokeWeight;
 777 |   }
 778 | 
 779 |   // If parentId is provided, append to that node, otherwise append to current page
 780 |   if (parentId) {
 781 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 782 |     if (!parentNode) {
 783 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 784 |     }
 785 |     if (!("appendChild" in parentNode)) {
 786 |       throw new Error(`Parent node does not support children: ${parentId}`);
 787 |     }
 788 |     parentNode.appendChild(frame);
 789 |   } else {
 790 |     figma.currentPage.appendChild(frame);
 791 |   }
 792 | 
 793 |   return {
 794 |     id: frame.id,
 795 |     name: frame.name,
 796 |     x: frame.x,
 797 |     y: frame.y,
 798 |     width: frame.width,
 799 |     height: frame.height,
 800 |     fills: frame.fills,
 801 |     strokes: frame.strokes,
 802 |     strokeWeight: frame.strokeWeight,
 803 |     layoutMode: frame.layoutMode,
 804 |     layoutWrap: frame.layoutWrap,
 805 |     parentId: frame.parent ? frame.parent.id : undefined,
 806 |   };
 807 | }
 808 | 
 809 | async function createText(params) {
 810 |   const {
 811 |     x = 0,
 812 |     y = 0,
 813 |     text = "Text",
 814 |     fontSize = 14,
 815 |     fontWeight = 400,
 816 |     fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black
 817 |     name = "",
 818 |     parentId,
 819 |   } = params || {};
 820 | 
 821 |   // Map common font weights to Figma font styles
 822 |   const getFontStyle = (weight) => {
 823 |     switch (weight) {
 824 |       case 100:
 825 |         return "Thin";
 826 |       case 200:
 827 |         return "Extra Light";
 828 |       case 300:
 829 |         return "Light";
 830 |       case 400:
 831 |         return "Regular";
 832 |       case 500:
 833 |         return "Medium";
 834 |       case 600:
 835 |         return "Semi Bold";
 836 |       case 700:
 837 |         return "Bold";
 838 |       case 800:
 839 |         return "Extra Bold";
 840 |       case 900:
 841 |         return "Black";
 842 |       default:
 843 |         return "Regular";
 844 |     }
 845 |   };
 846 | 
 847 |   const textNode = figma.createText();
 848 |   textNode.x = x;
 849 |   textNode.y = y;
 850 |   textNode.name = name || text;
 851 |   try {
 852 |     await figma.loadFontAsync({
 853 |       family: "Inter",
 854 |       style: getFontStyle(fontWeight),
 855 |     });
 856 |     textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
 857 |     textNode.fontSize = parseInt(fontSize);
 858 |   } catch (error) {
 859 |     console.error("Error setting font size", error);
 860 |   }
 861 |   setCharacters(textNode, text);
 862 | 
 863 |   // Set text color
 864 |   const paintStyle = {
 865 |     type: "SOLID",
 866 |     color: {
 867 |       r: parseFloat(fontColor.r) || 0,
 868 |       g: parseFloat(fontColor.g) || 0,
 869 |       b: parseFloat(fontColor.b) || 0,
 870 |     },
 871 |     opacity: parseFloat(fontColor.a) || 1,
 872 |   };
 873 |   textNode.fills = [paintStyle];
 874 | 
 875 |   // If parentId is provided, append to that node, otherwise append to current page
 876 |   if (parentId) {
 877 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 878 |     if (!parentNode) {
 879 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 880 |     }
 881 |     if (!("appendChild" in parentNode)) {
 882 |       throw new Error(`Parent node does not support children: ${parentId}`);
 883 |     }
 884 |     parentNode.appendChild(textNode);
 885 |   } else {
 886 |     figma.currentPage.appendChild(textNode);
 887 |   }
 888 | 
 889 |   return {
 890 |     id: textNode.id,
 891 |     name: textNode.name,
 892 |     x: textNode.x,
 893 |     y: textNode.y,
 894 |     width: textNode.width,
 895 |     height: textNode.height,
 896 |     characters: textNode.characters,
 897 |     fontSize: textNode.fontSize,
 898 |     fontWeight: fontWeight,
 899 |     fontColor: fontColor,
 900 |     fontName: textNode.fontName,
 901 |     fills: textNode.fills,
 902 |     parentId: textNode.parent ? textNode.parent.id : undefined,
 903 |   };
 904 | }
 905 | 
 906 | async function setFillColor(params) {
 907 |   console.log("setFillColor", params);
 908 |   const {
 909 |     nodeId,
 910 |     color: { r, g, b, a },
 911 |   } = params || {};
 912 | 
 913 |   if (!nodeId) {
 914 |     throw new Error("Missing nodeId parameter");
 915 |   }
 916 | 
 917 |   const node = await figma.getNodeByIdAsync(nodeId);
 918 |   if (!node) {
 919 |     throw new Error(`Node not found with ID: ${nodeId}`);
 920 |   }
 921 | 
 922 |   if (!("fills" in node)) {
 923 |     throw new Error(`Node does not support fills: ${nodeId}`);
 924 |   }
 925 | 
 926 |   // Create RGBA color
 927 |   const rgbColor = {
 928 |     r: parseFloat(r) || 0,
 929 |     g: parseFloat(g) || 0,
 930 |     b: parseFloat(b) || 0,
 931 |     a: parseFloat(a) || 1,
 932 |   };
 933 | 
 934 |   // Set fill
 935 |   const paintStyle = {
 936 |     type: "SOLID",
 937 |     color: {
 938 |       r: parseFloat(rgbColor.r),
 939 |       g: parseFloat(rgbColor.g),
 940 |       b: parseFloat(rgbColor.b),
 941 |     },
 942 |     opacity: parseFloat(rgbColor.a),
 943 |   };
 944 | 
 945 |   console.log("paintStyle", paintStyle);
 946 | 
 947 |   node.fills = [paintStyle];
 948 | 
 949 |   return {
 950 |     id: node.id,
 951 |     name: node.name,
 952 |     fills: [paintStyle],
 953 |   };
 954 | }
 955 | 
 956 | async function setStrokeColor(params) {
 957 |   const {
 958 |     nodeId,
 959 |     color: { r, g, b, a },
 960 |     weight = 1,
 961 |   } = params || {};
 962 | 
 963 |   if (!nodeId) {
 964 |     throw new Error("Missing nodeId parameter");
 965 |   }
 966 | 
 967 |   const node = await figma.getNodeByIdAsync(nodeId);
 968 |   if (!node) {
 969 |     throw new Error(`Node not found with ID: ${nodeId}`);
 970 |   }
 971 | 
 972 |   if (!("strokes" in node)) {
 973 |     throw new Error(`Node does not support strokes: ${nodeId}`);
 974 |   }
 975 | 
 976 |   // Create RGBA color
 977 |   const rgbColor = {
 978 |     r: r !== undefined ? r : 0,
 979 |     g: g !== undefined ? g : 0,
 980 |     b: b !== undefined ? b : 0,
 981 |     a: a !== undefined ? a : 1,
 982 |   };
 983 | 
 984 |   // Set stroke
 985 |   const paintStyle = {
 986 |     type: "SOLID",
 987 |     color: {
 988 |       r: rgbColor.r,
 989 |       g: rgbColor.g,
 990 |       b: rgbColor.b,
 991 |     },
 992 |     opacity: rgbColor.a,
 993 |   };
 994 | 
 995 |   node.strokes = [paintStyle];
 996 | 
 997 |   // Set stroke weight if available
 998 |   if ("strokeWeight" in node) {
 999 |     node.strokeWeight = weight;
1000 |   }
1001 | 
1002 |   return {
1003 |     id: node.id,
1004 |     name: node.name,
1005 |     strokes: node.strokes,
1006 |     strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined,
1007 |   };
1008 | }
1009 | 
1010 | async function moveNode(params) {
1011 |   const { nodeId, x, y } = params || {};
1012 | 
1013 |   if (!nodeId) {
1014 |     throw new Error("Missing nodeId parameter");
1015 |   }
1016 | 
1017 |   if (x === undefined || y === undefined) {
1018 |     throw new Error("Missing x or y parameters");
1019 |   }
1020 | 
1021 |   const node = await figma.getNodeByIdAsync(nodeId);
1022 |   if (!node) {
1023 |     throw new Error(`Node not found with ID: ${nodeId}`);
1024 |   }
1025 | 
1026 |   if (!("x" in node) || !("y" in node)) {
1027 |     throw new Error(`Node does not support position: ${nodeId}`);
1028 |   }
1029 | 
1030 |   node.x = x;
1031 |   node.y = y;
1032 | 
1033 |   return {
1034 |     id: node.id,
1035 |     name: node.name,
1036 |     x: node.x,
1037 |     y: node.y,
1038 |   };
1039 | }
1040 | 
1041 | async function resizeNode(params) {
1042 |   const { nodeId, width, height } = params || {};
1043 | 
1044 |   if (!nodeId) {
1045 |     throw new Error("Missing nodeId parameter");
1046 |   }
1047 | 
1048 |   if (width === undefined || height === undefined) {
1049 |     throw new Error("Missing width or height parameters");
1050 |   }
1051 | 
1052 |   const node = await figma.getNodeByIdAsync(nodeId);
1053 |   if (!node) {
1054 |     throw new Error(`Node not found with ID: ${nodeId}`);
1055 |   }
1056 | 
1057 |   if (!("resize" in node)) {
1058 |     throw new Error(`Node does not support resizing: ${nodeId}`);
1059 |   }
1060 | 
1061 |   node.resize(width, height);
1062 | 
1063 |   return {
1064 |     id: node.id,
1065 |     name: node.name,
1066 |     width: node.width,
1067 |     height: node.height,
1068 |   };
1069 | }
1070 | 
1071 | async function deleteNode(params) {
1072 |   const { nodeId } = params || {};
1073 | 
1074 |   if (!nodeId) {
1075 |     throw new Error("Missing nodeId parameter");
1076 |   }
1077 | 
1078 |   const node = await figma.getNodeByIdAsync(nodeId);
1079 |   if (!node) {
1080 |     throw new Error(`Node not found with ID: ${nodeId}`);
1081 |   }
1082 | 
1083 |   // Save node info before deleting
1084 |   const nodeInfo = {
1085 |     id: node.id,
1086 |     name: node.name,
1087 |     type: node.type,
1088 |   };
1089 | 
1090 |   node.remove();
1091 | 
1092 |   return nodeInfo;
1093 | }
1094 | 
1095 | async function getStyles() {
1096 |   const styles = {
1097 |     colors: await figma.getLocalPaintStylesAsync(),
1098 |     texts: await figma.getLocalTextStylesAsync(),
1099 |     effects: await figma.getLocalEffectStylesAsync(),
1100 |     grids: await figma.getLocalGridStylesAsync(),
1101 |   };
1102 | 
1103 |   return {
1104 |     colors: styles.colors.map((style) => ({
1105 |       id: style.id,
1106 |       name: style.name,
1107 |       key: style.key,
1108 |       paint: style.paints[0],
1109 |     })),
1110 |     texts: styles.texts.map((style) => ({
1111 |       id: style.id,
1112 |       name: style.name,
1113 |       key: style.key,
1114 |       fontSize: style.fontSize,
1115 |       fontName: style.fontName,
1116 |     })),
1117 |     effects: styles.effects.map((style) => ({
1118 |       id: style.id,
1119 |       name: style.name,
1120 |       key: style.key,
1121 |     })),
1122 |     grids: styles.grids.map((style) => ({
1123 |       id: style.id,
1124 |       name: style.name,
1125 |       key: style.key,
1126 |     })),
1127 |   };
1128 | }
1129 | 
1130 | async function getLocalComponents() {
1131 |   await figma.loadAllPagesAsync();
1132 | 
1133 |   const components = figma.root.findAllWithCriteria({
1134 |     types: ["COMPONENT"],
1135 |   });
1136 | 
1137 |   return {
1138 |     count: components.length,
1139 |     components: components.map((component) => ({
1140 |       id: component.id,
1141 |       name: component.name,
1142 |       key: "key" in component ? component.key : null,
1143 |     })),
1144 |   };
1145 | }
1146 | 
1147 | // async function getTeamComponents() {
1148 | //   try {
1149 | //     const teamComponents =
1150 | //       await figma.teamLibrary.getAvailableComponentsAsync();
1151 | 
1152 | //     return {
1153 | //       count: teamComponents.length,
1154 | //       components: teamComponents.map((component) => ({
1155 | //         key: component.key,
1156 | //         name: component.name,
1157 | //         description: component.description,
1158 | //         libraryName: component.libraryName,
1159 | //       })),
1160 | //     };
1161 | //   } catch (error) {
1162 | //     throw new Error(`Error getting team components: ${error.message}`);
1163 | //   }
1164 | // }
1165 | 
1166 | async function createComponentInstance(params) {
1167 |   const { componentKey, x = 0, y = 0 } = params || {};
1168 | 
1169 |   if (!componentKey) {
1170 |     throw new Error("Missing componentKey parameter");
1171 |   }
1172 | 
1173 |   try {
1174 |     const component = await figma.importComponentByKeyAsync(componentKey);
1175 |     const instance = component.createInstance();
1176 | 
1177 |     instance.x = x;
1178 |     instance.y = y;
1179 | 
1180 |     figma.currentPage.appendChild(instance);
1181 | 
1182 |     return {
1183 |       id: instance.id,
1184 |       name: instance.name,
1185 |       x: instance.x,
1186 |       y: instance.y,
1187 |       width: instance.width,
1188 |       height: instance.height,
1189 |       componentId: instance.componentId,
1190 |     };
1191 |   } catch (error) {
1192 |     throw new Error(`Error creating component instance: ${error.message}`);
1193 |   }
1194 | }
1195 | 
1196 | async function exportNodeAsImage(params) {
1197 |   const { nodeId, scale = 1 } = params || {};
1198 | 
1199 |   const format = "PNG";
1200 | 
1201 |   if (!nodeId) {
1202 |     throw new Error("Missing nodeId parameter");
1203 |   }
1204 | 
1205 |   const node = await figma.getNodeByIdAsync(nodeId);
1206 |   if (!node) {
1207 |     throw new Error(`Node not found with ID: ${nodeId}`);
1208 |   }
1209 | 
1210 |   if (!("exportAsync" in node)) {
1211 |     throw new Error(`Node does not support exporting: ${nodeId}`);
1212 |   }
1213 | 
1214 |   try {
1215 |     const settings = {
1216 |       format: format,
1217 |       constraint: { type: "SCALE", value: scale },
1218 |     };
1219 | 
1220 |     const bytes = await node.exportAsync(settings);
1221 | 
1222 |     let mimeType;
1223 |     switch (format) {
1224 |       case "PNG":
1225 |         mimeType = "image/png";
1226 |         break;
1227 |       case "JPG":
1228 |         mimeType = "image/jpeg";
1229 |         break;
1230 |       case "SVG":
1231 |         mimeType = "image/svg+xml";
1232 |         break;
1233 |       case "PDF":
1234 |         mimeType = "application/pdf";
1235 |         break;
1236 |       default:
1237 |         mimeType = "application/octet-stream";
1238 |     }
1239 | 
1240 |     // Proper way to convert Uint8Array to base64
1241 |     const base64 = customBase64Encode(bytes);
1242 |     // const imageData = `data:${mimeType};base64,${base64}`;
1243 | 
1244 |     return {
1245 |       nodeId,
1246 |       format,
1247 |       scale,
1248 |       mimeType,
1249 |       imageData: base64,
1250 |     };
1251 |   } catch (error) {
1252 |     throw new Error(`Error exporting node as image: ${error.message}`);
1253 |   }
1254 | }
1255 | function customBase64Encode(bytes) {
1256 |   const chars =
1257 |     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1258 |   let base64 = "";
1259 | 
1260 |   const byteLength = bytes.byteLength;
1261 |   const byteRemainder = byteLength % 3;
1262 |   const mainLength = byteLength - byteRemainder;
1263 | 
1264 |   let a, b, c, d;
1265 |   let chunk;
1266 | 
1267 |   // Main loop deals with bytes in chunks of 3
1268 |   for (let i = 0; i < mainLength; i = i + 3) {
1269 |     // Combine the three bytes into a single integer
1270 |     chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
1271 | 
1272 |     // Use bitmasks to extract 6-bit segments from the triplet
1273 |     a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
1274 |     b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
1275 |     c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
1276 |     d = chunk & 63; // 63 = 2^6 - 1
1277 | 
1278 |     // Convert the raw binary segments to the appropriate ASCII encoding
1279 |     base64 += chars[a] + chars[b] + chars[c] + chars[d];
1280 |   }
1281 | 
1282 |   // Deal with the remaining bytes and padding
1283 |   if (byteRemainder === 1) {
1284 |     chunk = bytes[mainLength];
1285 | 
1286 |     a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
1287 | 
1288 |     // Set the 4 least significant bits to zero
1289 |     b = (chunk & 3) << 4; // 3 = 2^2 - 1
1290 | 
1291 |     base64 += chars[a] + chars[b] + "==";
1292 |   } else if (byteRemainder === 2) {
1293 |     chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
1294 | 
1295 |     a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
1296 |     b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
1297 | 
1298 |     // Set the 2 least significant bits to zero
1299 |     c = (chunk & 15) << 2; // 15 = 2^4 - 1
1300 | 
1301 |     base64 += chars[a] + chars[b] + chars[c] + "=";
1302 |   }
1303 | 
1304 |   return base64;
1305 | }
1306 | 
1307 | async function setCornerRadius(params) {
1308 |   const { nodeId, radius, corners } = params || {};
1309 | 
1310 |   if (!nodeId) {
1311 |     throw new Error("Missing nodeId parameter");
1312 |   }
1313 | 
1314 |   if (radius === undefined) {
1315 |     throw new Error("Missing radius parameter");
1316 |   }
1317 | 
1318 |   const node = await figma.getNodeByIdAsync(nodeId);
1319 |   if (!node) {
1320 |     throw new Error(`Node not found with ID: ${nodeId}`);
1321 |   }
1322 | 
1323 |   // Check if node supports corner radius
1324 |   if (!("cornerRadius" in node)) {
1325 |     throw new Error(`Node does not support corner radius: ${nodeId}`);
1326 |   }
1327 | 
1328 |   // If corners array is provided, set individual corner radii
1329 |   if (corners && Array.isArray(corners) && corners.length === 4) {
1330 |     if ("topLeftRadius" in node) {
1331 |       // Node supports individual corner radii
1332 |       if (corners[0]) node.topLeftRadius = radius;
1333 |       if (corners[1]) node.topRightRadius = radius;
1334 |       if (corners[2]) node.bottomRightRadius = radius;
1335 |       if (corners[3]) node.bottomLeftRadius = radius;
1336 |     } else {
1337 |       // Node only supports uniform corner radius
1338 |       node.cornerRadius = radius;
1339 |     }
1340 |   } else {
1341 |     // Set uniform corner radius
1342 |     node.cornerRadius = radius;
1343 |   }
1344 | 
1345 |   return {
1346 |     id: node.id,
1347 |     name: node.name,
1348 |     cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined,
1349 |     topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined,
1350 |     topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined,
1351 |     bottomRightRadius:
1352 |       "bottomRightRadius" in node ? node.bottomRightRadius : undefined,
1353 |     bottomLeftRadius:
1354 |       "bottomLeftRadius" in node ? node.bottomLeftRadius : undefined,
1355 |   };
1356 | }
1357 | 
1358 | async function setTextContent(params) {
1359 |   const { nodeId, text } = params || {};
1360 | 
1361 |   if (!nodeId) {
1362 |     throw new Error("Missing nodeId parameter");
1363 |   }
1364 | 
1365 |   if (text === undefined) {
1366 |     throw new Error("Missing text parameter");
1367 |   }
1368 | 
1369 |   const node = await figma.getNodeByIdAsync(nodeId);
1370 |   if (!node) {
1371 |     throw new Error(`Node not found with ID: ${nodeId}`);
1372 |   }
1373 | 
1374 |   if (node.type !== "TEXT") {
1375 |     throw new Error(`Node is not a text node: ${nodeId}`);
1376 |   }
1377 | 
1378 |   try {
1379 |     await figma.loadFontAsync(node.fontName);
1380 | 
1381 |     await setCharacters(node, text);
1382 | 
1383 |     return {
1384 |       id: node.id,
1385 |       name: node.name,
1386 |       characters: node.characters,
1387 |       fontName: node.fontName,
1388 |     };
1389 |   } catch (error) {
1390 |     throw new Error(`Error setting text content: ${error.message}`);
1391 |   }
1392 | }
1393 | 
1394 | // Initialize settings on load
1395 | (async function initializePlugin() {
1396 |   try {
1397 |     const savedSettings = await figma.clientStorage.getAsync("settings");
1398 |     if (savedSettings) {
1399 |       if (savedSettings.serverPort) {
1400 |         state.serverPort = savedSettings.serverPort;
1401 |       }
1402 |     }
1403 | 
1404 |     // Send initial settings to UI
1405 |     figma.ui.postMessage({
1406 |       type: "init-settings",
1407 |       settings: {
1408 |         serverPort: state.serverPort,
1409 |       },
1410 |     });
1411 |   } catch (error) {
1412 |     console.error("Error loading settings:", error);
1413 |   }
1414 | })();
1415 | 
1416 | function uniqBy(arr, predicate) {
1417 |   const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
1418 |   return [
1419 |     ...arr
1420 |       .reduce((map, item) => {
1421 |         const key = item === null || item === undefined ? item : cb(item);
1422 | 
1423 |         map.has(key) || map.set(key, item);
1424 | 
1425 |         return map;
1426 |       }, new Map())
1427 |       .values(),
1428 |   ];
1429 | }
1430 | const setCharacters = async (node, characters, options) => {
1431 |   const fallbackFont = (options && options.fallbackFont) || {
1432 |     family: "Inter",
1433 |     style: "Regular",
1434 |   };
1435 |   try {
1436 |     if (node.fontName === figma.mixed) {
1437 |       if (options && options.smartStrategy === "prevail") {
1438 |         const fontHashTree = {};
1439 |         for (let i = 1; i < node.characters.length; i++) {
1440 |           const charFont = node.getRangeFontName(i - 1, i);
1441 |           const key = `${charFont.family}::${charFont.style}`;
1442 |           fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
1443 |         }
1444 |         const prevailedTreeItem = Object.entries(fontHashTree).sort(
1445 |           (a, b) => b[1] - a[1]
1446 |         )[0];
1447 |         const [family, style] = prevailedTreeItem[0].split("::");
1448 |         const prevailedFont = {
1449 |           family,
1450 |           style,
1451 |         };
1452 |         await figma.loadFontAsync(prevailedFont);
1453 |         node.fontName = prevailedFont;
1454 |       } else if (options && options.smartStrategy === "strict") {
1455 |         return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
1456 |       } else if (options && options.smartStrategy === "experimental") {
1457 |         return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
1458 |       } else {
1459 |         const firstCharFont = node.getRangeFontName(0, 1);
1460 |         await figma.loadFontAsync(firstCharFont);
1461 |         node.fontName = firstCharFont;
1462 |       }
1463 |     } else {
1464 |       await figma.loadFontAsync({
1465 |         family: node.fontName.family,
1466 |         style: node.fontName.style,
1467 |       });
1468 |     }
1469 |   } catch (err) {
1470 |     console.warn(
1471 |       `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
1472 |       err
1473 |     );
1474 |     await figma.loadFontAsync(fallbackFont);
1475 |     node.fontName = fallbackFont;
1476 |   }
1477 |   try {
1478 |     node.characters = characters;
1479 |     return true;
1480 |   } catch (err) {
1481 |     console.warn(`Failed to set characters. Skipped.`, err);
1482 |     return false;
1483 |   }
1484 | };
1485 | 
1486 | const setCharactersWithStrictMatchFont = async (
1487 |   node,
1488 |   characters,
1489 |   fallbackFont
1490 | ) => {
1491 |   const fontHashTree = {};
1492 |   for (let i = 1; i < node.characters.length; i++) {
1493 |     const startIdx = i - 1;
1494 |     const startCharFont = node.getRangeFontName(startIdx, i);
1495 |     const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
1496 |     while (i < node.characters.length) {
1497 |       i++;
1498 |       const charFont = node.getRangeFontName(i - 1, i);
1499 |       if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
1500 |         break;
1501 |       }
1502 |     }
1503 |     fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
1504 |   }
1505 |   await figma.loadFontAsync(fallbackFont);
1506 |   node.fontName = fallbackFont;
1507 |   node.characters = characters;
1508 |   console.log(fontHashTree);
1509 |   await Promise.all(
1510 |     Object.keys(fontHashTree).map(async (range) => {
1511 |       console.log(range, fontHashTree[range]);
1512 |       const [start, end] = range.split("_");
1513 |       const [family, style] = fontHashTree[range].split("::");
1514 |       const matchedFont = {
1515 |         family,
1516 |         style,
1517 |       };
1518 |       await figma.loadFontAsync(matchedFont);
1519 |       return node.setRangeFontName(Number(start), Number(end), matchedFont);
1520 |     })
1521 |   );
1522 |   return true;
1523 | };
1524 | 
1525 | const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
1526 |   const indices = [];
1527 |   let temp = startIdx;
1528 |   for (let i = startIdx; i < endIdx; i++) {
1529 |     if (
1530 |       str[i] === delimiter &&
1531 |       i + startIdx !== endIdx &&
1532 |       temp !== i + startIdx
1533 |     ) {
1534 |       indices.push([temp, i + startIdx]);
1535 |       temp = i + startIdx + 1;
1536 |     }
1537 |   }
1538 |   temp !== endIdx && indices.push([temp, endIdx]);
1539 |   return indices.filter(Boolean);
1540 | };
1541 | 
1542 | const buildLinearOrder = (node) => {
1543 |   const fontTree = [];
1544 |   const newLinesPos = getDelimiterPos(node.characters, "\n");
1545 |   newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
1546 |     const newLinesRangeFont = node.getRangeFontName(
1547 |       newLinesRangeStart,
1548 |       newLinesRangeEnd
1549 |     );
1550 |     if (newLinesRangeFont === figma.mixed) {
1551 |       const spacesPos = getDelimiterPos(
1552 |         node.characters,
1553 |         " ",
1554 |         newLinesRangeStart,
1555 |         newLinesRangeEnd
1556 |       );
1557 |       spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
1558 |         const spacesRangeFont = node.getRangeFontName(
1559 |           spacesRangeStart,
1560 |           spacesRangeEnd
1561 |         );
1562 |         if (spacesRangeFont === figma.mixed) {
1563 |           const spacesRangeFont = node.getRangeFontName(
1564 |             spacesRangeStart,
1565 |             spacesRangeStart[0]
1566 |           );
1567 |           fontTree.push({
1568 |             start: spacesRangeStart,
1569 |             delimiter: " ",
1570 |             family: spacesRangeFont.family,
1571 |             style: spacesRangeFont.style,
1572 |           });
1573 |         } else {
1574 |           fontTree.push({
1575 |             start: spacesRangeStart,
1576 |             delimiter: " ",
1577 |             family: spacesRangeFont.family,
1578 |             style: spacesRangeFont.style,
1579 |           });
1580 |         }
1581 |       });
1582 |     } else {
1583 |       fontTree.push({
1584 |         start: newLinesRangeStart,
1585 |         delimiter: "\n",
1586 |         family: newLinesRangeFont.family,
1587 |         style: newLinesRangeFont.style,
1588 |       });
1589 |     }
1590 |   });
1591 |   return fontTree
1592 |     .sort((a, b) => +a.start - +b.start)
1593 |     .map(({ family, style, delimiter }) => ({ family, style, delimiter }));
1594 | };
1595 | 
1596 | const setCharactersWithSmartMatchFont = async (
1597 |   node,
1598 |   characters,
1599 |   fallbackFont
1600 | ) => {
1601 |   const rangeTree = buildLinearOrder(node);
1602 |   const fontsToLoad = uniqBy(
1603 |     rangeTree,
1604 |     ({ family, style }) => `${family}::${style}`
1605 |   ).map(({ family, style }) => ({
1606 |     family,
1607 |     style,
1608 |   }));
1609 | 
1610 |   await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
1611 | 
1612 |   node.fontName = fallbackFont;
1613 |   node.characters = characters;
1614 | 
1615 |   let prevPos = 0;
1616 |   rangeTree.forEach(({ family, style, delimiter }) => {
1617 |     if (prevPos < node.characters.length) {
1618 |       const delimeterPos = node.characters.indexOf(delimiter, prevPos);
1619 |       const endPos =
1620 |         delimeterPos > prevPos ? delimeterPos : node.characters.length;
1621 |       const matchedFont = {
1622 |         family,
1623 |         style,
1624 |       };
1625 |       node.setRangeFontName(prevPos, endPos, matchedFont);
1626 |       prevPos = endPos + 1;
1627 |     }
1628 |   });
1629 |   return true;
1630 | };
1631 | 
1632 | // Add the cloneNode function implementation
1633 | async function cloneNode(params) {
1634 |   const { nodeId, x, y } = params || {};
1635 | 
1636 |   if (!nodeId) {
1637 |     throw new Error("Missing nodeId parameter");
1638 |   }
1639 | 
1640 |   const node = await figma.getNodeByIdAsync(nodeId);
1641 |   if (!node) {
1642 |     throw new Error(`Node not found with ID: ${nodeId}`);
1643 |   }
1644 | 
1645 |   // Clone the node
1646 |   const clone = node.clone();
1647 | 
1648 |   // If x and y are provided, move the clone to that position
1649 |   if (x !== undefined && y !== undefined) {
1650 |     if (!("x" in clone) || !("y" in clone)) {
1651 |       throw new Error(`Cloned node does not support position: ${nodeId}`);
1652 |     }
1653 |     clone.x = x;
1654 |     clone.y = y;
1655 |   }
1656 | 
1657 |   // Add the clone to the same parent as the original node
1658 |   if (node.parent) {
1659 |     node.parent.appendChild(clone);
1660 |   } else {
1661 |     figma.currentPage.appendChild(clone);
1662 |   }
1663 | 
1664 |   return {
1665 |     id: clone.id,
1666 |     name: clone.name,
1667 |     x: "x" in clone ? clone.x : undefined,
1668 |     y: "y" in clone ? clone.y : undefined,
1669 |     width: "width" in clone ? clone.width : undefined,
1670 |     height: "height" in clone ? clone.height : undefined,
1671 |   };
1672 | }
1673 | 
1674 | async function scanTextNodes(params) {
1675 |   console.log(`Starting to scan text nodes from node ID: ${params.nodeId}`);
1676 |   const {
1677 |     nodeId,
1678 |     useChunking = true,
1679 |     chunkSize = 10,
1680 |     commandId = generateCommandId(),
1681 |   } = params || {};
1682 | 
1683 |   const node = await figma.getNodeByIdAsync(nodeId);
1684 | 
1685 |   if (!node) {
1686 |     console.error(`Node with ID ${nodeId} not found`);
1687 |     // Send error progress update
1688 |     sendProgressUpdate(
1689 |       commandId,
1690 |       "scan_text_nodes",
1691 |       "error",
1692 |       0,
1693 |       0,
1694 |       0,
1695 |       `Node with ID ${nodeId} not found`,
1696 |       { error: `Node not found: ${nodeId}` }
1697 |     );
1698 |     throw new Error(`Node with ID ${nodeId} not found`);
1699 |   }
1700 | 
1701 |   // If chunking is not enabled, use the original implementation
1702 |   if (!useChunking) {
1703 |     const textNodes = [];
1704 |     try {
1705 |       // Send started progress update
1706 |       sendProgressUpdate(
1707 |         commandId,
1708 |         "scan_text_nodes",
1709 |         "started",
1710 |         0,
1711 |         1, // Not known yet how many nodes there are
1712 |         0,
1713 |         `Starting scan of node "${node.name || nodeId}" without chunking`,
1714 |         null
1715 |       );
1716 | 
1717 |       await findTextNodes(node, [], 0, textNodes);
1718 | 
1719 |       // Send completed progress update
1720 |       sendProgressUpdate(
1721 |         commandId,
1722 |         "scan_text_nodes",
1723 |         "completed",
1724 |         100,
1725 |         textNodes.length,
1726 |         textNodes.length,
1727 |         `Scan complete. Found ${textNodes.length} text nodes.`,
1728 |         { textNodes }
1729 |       );
1730 | 
1731 |       return {
1732 |         success: true,
1733 |         message: `Scanned ${textNodes.length} text nodes.`,
1734 |         count: textNodes.length,
1735 |         textNodes: textNodes,
1736 |         commandId,
1737 |       };
1738 |     } catch (error) {
1739 |       console.error("Error scanning text nodes:", error);
1740 | 
1741 |       // Send error progress update
1742 |       sendProgressUpdate(
1743 |         commandId,
1744 |         "scan_text_nodes",
1745 |         "error",
1746 |         0,
1747 |         0,
1748 |         0,
1749 |         `Error scanning text nodes: ${error.message}`,
1750 |         { error: error.message }
1751 |       );
1752 | 
1753 |       throw new Error(`Error scanning text nodes: ${error.message}`);
1754 |     }
1755 |   }
1756 | 
1757 |   // Chunked implementation
1758 |   console.log(`Using chunked scanning with chunk size: ${chunkSize}`);
1759 | 
1760 |   // First, collect all nodes to process (without processing them yet)
1761 |   const nodesToProcess = [];
1762 | 
1763 |   // Send started progress update
1764 |   sendProgressUpdate(
1765 |     commandId,
1766 |     "scan_text_nodes",
1767 |     "started",
1768 |     0,
1769 |     0, // Not known yet how many nodes there are
1770 |     0,
1771 |     `Starting chunked scan of node "${node.name || nodeId}"`,
1772 |     { chunkSize }
1773 |   );
1774 | 
1775 |   await collectNodesToProcess(node, [], 0, nodesToProcess);
1776 | 
1777 |   const totalNodes = nodesToProcess.length;
1778 |   console.log(`Found ${totalNodes} total nodes to process`);
1779 | 
1780 |   // Calculate number of chunks needed
1781 |   const totalChunks = Math.ceil(totalNodes / chunkSize);
1782 |   console.log(`Will process in ${totalChunks} chunks`);
1783 | 
1784 |   // Send update after node collection
1785 |   sendProgressUpdate(
1786 |     commandId,
1787 |     "scan_text_nodes",
1788 |     "in_progress",
1789 |     5, // 5% progress for collection phase
1790 |     totalNodes,
1791 |     0,
1792 |     `Found ${totalNodes} nodes to scan. Will process in ${totalChunks} chunks.`,
1793 |     {
1794 |       totalNodes,
1795 |       totalChunks,
1796 |       chunkSize,
1797 |     }
1798 |   );
1799 | 
1800 |   // Process nodes in chunks
1801 |   const allTextNodes = [];
1802 |   let processedNodes = 0;
1803 |   let chunksProcessed = 0;
1804 | 
1805 |   for (let i = 0; i < totalNodes; i += chunkSize) {
1806 |     const chunkEnd = Math.min(i + chunkSize, totalNodes);
1807 |     console.log(
1808 |       `Processing chunk ${chunksProcessed + 1}/${totalChunks} (nodes ${i} to ${chunkEnd - 1
1809 |       })`
1810 |     );
1811 | 
1812 |     // Send update before processing chunk
1813 |     sendProgressUpdate(
1814 |       commandId,
1815 |       "scan_text_nodes",
1816 |       "in_progress",
1817 |       Math.round(5 + (chunksProcessed / totalChunks) * 90), // 5-95% for processing
1818 |       totalNodes,
1819 |       processedNodes,
1820 |       `Processing chunk ${chunksProcessed + 1}/${totalChunks}`,
1821 |       {
1822 |         currentChunk: chunksProcessed + 1,
1823 |         totalChunks,
1824 |         textNodesFound: allTextNodes.length,
1825 |       }
1826 |     );
1827 | 
1828 |     const chunkNodes = nodesToProcess.slice(i, chunkEnd);
1829 |     const chunkTextNodes = [];
1830 | 
1831 |     // Process each node in this chunk
1832 |     for (const nodeInfo of chunkNodes) {
1833 |       if (nodeInfo.node.type === "TEXT") {
1834 |         try {
1835 |           const textNodeInfo = await processTextNode(
1836 |             nodeInfo.node,
1837 |             nodeInfo.parentPath,
1838 |             nodeInfo.depth
1839 |           );
1840 |           if (textNodeInfo) {
1841 |             chunkTextNodes.push(textNodeInfo);
1842 |           }
1843 |         } catch (error) {
1844 |           console.error(`Error processing text node: ${error.message}`);
1845 |           // Continue with other nodes
1846 |         }
1847 |       }
1848 | 
1849 |       // Brief delay to allow UI updates and prevent freezing
1850 |       await delay(5);
1851 |     }
1852 | 
1853 |     // Add results from this chunk
1854 |     allTextNodes.push(...chunkTextNodes);
1855 |     processedNodes += chunkNodes.length;
1856 |     chunksProcessed++;
1857 | 
1858 |     // Send update after processing chunk
1859 |     sendProgressUpdate(
1860 |       commandId,
1861 |       "scan_text_nodes",
1862 |       "in_progress",
1863 |       Math.round(5 + (chunksProcessed / totalChunks) * 90), // 5-95% for processing
1864 |       totalNodes,
1865 |       processedNodes,
1866 |       `Processed chunk ${chunksProcessed}/${totalChunks}. Found ${allTextNodes.length} text nodes so far.`,
1867 |       {
1868 |         currentChunk: chunksProcessed,
1869 |         totalChunks,
1870 |         processedNodes,
1871 |         textNodesFound: allTextNodes.length,
1872 |         chunkResult: chunkTextNodes,
1873 |       }
1874 |     );
1875 | 
1876 |     // Small delay between chunks to prevent UI freezing
1877 |     if (i + chunkSize < totalNodes) {
1878 |       await delay(50);
1879 |     }
1880 |   }
1881 | 
1882 |   // Send completed progress update
1883 |   sendProgressUpdate(
1884 |     commandId,
1885 |     "scan_text_nodes",
1886 |     "completed",
1887 |     100,
1888 |     totalNodes,
1889 |     processedNodes,
1890 |     `Scan complete. Found ${allTextNodes.length} text nodes.`,
1891 |     {
1892 |       textNodes: allTextNodes,
1893 |       processedNodes,
1894 |       chunks: chunksProcessed,
1895 |     }
1896 |   );
1897 | 
1898 |   return {
1899 |     success: true,
1900 |     message: `Chunked scan complete. Found ${allTextNodes.length} text nodes.`,
1901 |     totalNodes: allTextNodes.length,
1902 |     processedNodes: processedNodes,
1903 |     chunks: chunksProcessed,
1904 |     textNodes: allTextNodes,
1905 |     commandId,
1906 |   };
1907 | }
1908 | 
1909 | // Helper function to collect all nodes that need to be processed
1910 | async function collectNodesToProcess(
1911 |   node,
1912 |   parentPath = [],
1913 |   depth = 0,
1914 |   nodesToProcess = []
1915 | ) {
1916 |   // Skip invisible nodes
1917 |   if (node.visible === false) return;
1918 | 
1919 |   // Get the path to this node
1920 |   const nodePath = [...parentPath, node.name || `Unnamed ${node.type}`];
1921 | 
1922 |   // Add this node to the processing list
1923 |   nodesToProcess.push({
1924 |     node: node,
1925 |     parentPath: nodePath,
1926 |     depth: depth,
1927 |   });
1928 | 
1929 |   // Recursively add children
1930 |   if ("children" in node) {
1931 |     for (const child of node.children) {
1932 |       await collectNodesToProcess(child, nodePath, depth + 1, nodesToProcess);
1933 |     }
1934 |   }
1935 | }
1936 | 
1937 | // Process a single text node
1938 | async function processTextNode(node, parentPath, depth) {
1939 |   if (node.type !== "TEXT") return null;
1940 | 
1941 |   try {
1942 |     // Safely extract font information
1943 |     let fontFamily = "";
1944 |     let fontStyle = "";
1945 | 
1946 |     if (node.fontName) {
1947 |       if (typeof node.fontName === "object") {
1948 |         if ("family" in node.fontName) fontFamily = node.fontName.family;
1949 |         if ("style" in node.fontName) fontStyle = node.fontName.style;
1950 |       }
1951 |     }
1952 | 
1953 |     // Create a safe representation of the text node
1954 |     const safeTextNode = {
1955 |       id: node.id,
1956 |       name: node.name || "Text",
1957 |       type: node.type,
1958 |       characters: node.characters,
1959 |       fontSize: typeof node.fontSize === "number" ? node.fontSize : 0,
1960 |       fontFamily: fontFamily,
1961 |       fontStyle: fontStyle,
1962 |       x: typeof node.x === "number" ? node.x : 0,
1963 |       y: typeof node.y === "number" ? node.y : 0,
1964 |       width: typeof node.width === "number" ? node.width : 0,
1965 |       height: typeof node.height === "number" ? node.height : 0,
1966 |       path: parentPath.join(" > "),
1967 |       depth: depth,
1968 |     };
1969 | 
1970 |     // Highlight the node briefly (optional visual feedback)
1971 |     try {
1972 |       const originalFills = JSON.parse(JSON.stringify(node.fills));
1973 |       node.fills = [
1974 |         {
1975 |           type: "SOLID",
1976 |           color: { r: 1, g: 0.5, b: 0 },
1977 |           opacity: 0.3,
1978 |         },
1979 |       ];
1980 | 
1981 |       // Brief delay for the highlight to be visible
1982 |       await delay(100);
1983 | 
1984 |       try {
1985 |         node.fills = originalFills;
1986 |       } catch (err) {
1987 |         console.error("Error resetting fills:", err);
1988 |       }
1989 |     } catch (highlightErr) {
1990 |       console.error("Error highlighting text node:", highlightErr);
1991 |       // Continue anyway, highlighting is just visual feedback
1992 |     }
1993 | 
1994 |     return safeTextNode;
1995 |   } catch (nodeErr) {
1996 |     console.error("Error processing text node:", nodeErr);
1997 |     return null;
1998 |   }
1999 | }
2000 | 
2001 | // A delay function that returns a promise
2002 | function delay(ms) {
2003 |   return new Promise((resolve) => setTimeout(resolve, ms));
2004 | }
2005 | 
2006 | // Keep the original findTextNodes for backward compatibility
2007 | async function findTextNodes(node, parentPath = [], depth = 0, textNodes = []) {
2008 |   // Skip invisible nodes
2009 |   if (node.visible === false) return;
2010 | 
2011 |   // Get the path to this node including its name
2012 |   const nodePath = [...parentPath, node.name || `Unnamed ${node.type}`];
2013 | 
2014 |   if (node.type === "TEXT") {
2015 |     try {
2016 |       // Safely extract font information to avoid Symbol serialization issues
2017 |       let fontFamily = "";
2018 |       let fontStyle = "";
2019 | 
2020 |       if (node.fontName) {
2021 |         if (typeof node.fontName === "object") {
2022 |           if ("family" in node.fontName) fontFamily = node.fontName.family;
2023 |           if ("style" in node.fontName) fontStyle = node.fontName.style;
2024 |         }
2025 |       }
2026 | 
2027 |       // Create a safe representation of the text node with only serializable properties
2028 |       const safeTextNode = {
2029 |         id: node.id,
2030 |         name: node.name || "Text",
2031 |         type: node.type,
2032 |         characters: node.characters,
2033 |         fontSize: typeof node.fontSize === "number" ? node.fontSize : 0,
2034 |         fontFamily: fontFamily,
2035 |         fontStyle: fontStyle,
2036 |         x: typeof node.x === "number" ? node.x : 0,
2037 |         y: typeof node.y === "number" ? node.y : 0,
2038 |         width: typeof node.width === "number" ? node.width : 0,
2039 |         height: typeof node.height === "number" ? node.height : 0,
2040 |         path: nodePath.join(" > "),
2041 |         depth: depth,
2042 |       };
2043 | 
2044 |       // Only highlight the node if it's not being done via API
2045 |       try {
2046 |         // Safe way to create a temporary highlight without causing serialization issues
2047 |         const originalFills = JSON.parse(JSON.stringify(node.fills));
2048 |         node.fills = [
2049 |           {
2050 |             type: "SOLID",
2051 |             color: { r: 1, g: 0.5, b: 0 },
2052 |             opacity: 0.3,
2053 |           },
2054 |         ];
2055 | 
2056 |         // Promise-based delay instead of setTimeout
2057 |         await delay(500);
2058 | 
2059 |         try {
2060 |           node.fills = originalFills;
2061 |         } catch (err) {
2062 |           console.error("Error resetting fills:", err);
2063 |         }
2064 |       } catch (highlightErr) {
2065 |         console.error("Error highlighting text node:", highlightErr);
2066 |         // Continue anyway, highlighting is just visual feedback
2067 |       }
2068 | 
2069 |       textNodes.push(safeTextNode);
2070 |     } catch (nodeErr) {
2071 |       console.error("Error processing text node:", nodeErr);
2072 |       // Skip this node but continue with others
2073 |     }
2074 |   }
2075 | 
2076 |   // Recursively process children of container nodes
2077 |   if ("children" in node) {
2078 |     for (const child of node.children) {
2079 |       await findTextNodes(child, nodePath, depth + 1, textNodes);
2080 |     }
2081 |   }
2082 | }
2083 | 
2084 | // Replace text in a specific node
2085 | async function setMultipleTextContents(params) {
2086 |   const { nodeId, text } = params || {};
2087 |   const commandId = params.commandId || generateCommandId();
2088 | 
2089 |   if (!nodeId || !text || !Array.isArray(text)) {
2090 |     const errorMsg = "Missing required parameters: nodeId and text array";
2091 | 
2092 |     // Send error progress update
2093 |     sendProgressUpdate(
2094 |       commandId,
2095 |       "set_multiple_text_contents",
2096 |       "error",
2097 |       0,
2098 |       0,
2099 |       0,
2100 |       errorMsg,
2101 |       { error: errorMsg }
2102 |     );
2103 | 
2104 |     throw new Error(errorMsg);
2105 |   }
2106 | 
2107 |   console.log(
2108 |     `Starting text replacement for node: ${nodeId} with ${text.length} text replacements`
2109 |   );
2110 | 
2111 |   // Send started progress update
2112 |   sendProgressUpdate(
2113 |     commandId,
2114 |     "set_multiple_text_contents",
2115 |     "started",
2116 |     0,
2117 |     text.length,
2118 |     0,
2119 |     `Starting text replacement for ${text.length} nodes`,
2120 |     { totalReplacements: text.length }
2121 |   );
2122 | 
2123 |   // Define the results array and counters
2124 |   const results = [];
2125 |   let successCount = 0;
2126 |   let failureCount = 0;
2127 | 
2128 |   // Split text replacements into chunks of 5
2129 |   const CHUNK_SIZE = 5;
2130 |   const chunks = [];
2131 | 
2132 |   for (let i = 0; i < text.length; i += CHUNK_SIZE) {
2133 |     chunks.push(text.slice(i, i + CHUNK_SIZE));
2134 |   }
2135 | 
2136 |   console.log(`Split ${text.length} replacements into ${chunks.length} chunks`);
2137 | 
2138 |   // Send chunking info update
2139 |   sendProgressUpdate(
2140 |     commandId,
2141 |     "set_multiple_text_contents",
2142 |     "in_progress",
2143 |     5, // 5% progress for planning phase
2144 |     text.length,
2145 |     0,
2146 |     `Preparing to replace text in ${text.length} nodes using ${chunks.length} chunks`,
2147 |     {
2148 |       totalReplacements: text.length,
2149 |       chunks: chunks.length,
2150 |       chunkSize: CHUNK_SIZE,
2151 |     }
2152 |   );
2153 | 
2154 |   // Process each chunk sequentially
2155 |   for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
2156 |     const chunk = chunks[chunkIndex];
2157 |     console.log(
2158 |       `Processing chunk ${chunkIndex + 1}/${chunks.length} with ${chunk.length
2159 |       } replacements`
2160 |     );
2161 | 
2162 |     // Send chunk processing start update
2163 |     sendProgressUpdate(
2164 |       commandId,
2165 |       "set_multiple_text_contents",
2166 |       "in_progress",
2167 |       Math.round(5 + (chunkIndex / chunks.length) * 90), // 5-95% for processing
2168 |       text.length,
2169 |       successCount + failureCount,
2170 |       `Processing text replacements chunk ${chunkIndex + 1}/${chunks.length}`,
2171 |       {
2172 |         currentChunk: chunkIndex + 1,
2173 |         totalChunks: chunks.length,
2174 |         successCount,
2175 |         failureCount,
2176 |       }
2177 |     );
2178 | 
2179 |     // Process replacements within a chunk in parallel
2180 |     const chunkPromises = chunk.map(async (replacement) => {
2181 |       if (!replacement.nodeId || replacement.text === undefined) {
2182 |         console.error(`Missing nodeId or text for replacement`);
2183 |         return {
2184 |           success: false,
2185 |           nodeId: replacement.nodeId || "unknown",
2186 |           error: "Missing nodeId or text in replacement entry",
2187 |         };
2188 |       }
2189 | 
2190 |       try {
2191 |         console.log(
2192 |           `Attempting to replace text in node: ${replacement.nodeId}`
2193 |         );
2194 | 
2195 |         // Get the text node to update (just to check it exists and get original text)
2196 |         const textNode = await figma.getNodeByIdAsync(replacement.nodeId);
2197 | 
2198 |         if (!textNode) {
2199 |           console.error(`Text node not found: ${replacement.nodeId}`);
2200 |           return {
2201 |             success: false,
2202 |             nodeId: replacement.nodeId,
2203 |             error: `Node not found: ${replacement.nodeId}`,
2204 |           };
2205 |         }
2206 | 
2207 |         if (textNode.type !== "TEXT") {
2208 |           console.error(
2209 |             `Node is not a text node: ${replacement.nodeId} (type: ${textNode.type})`
2210 |           );
2211 |           return {
2212 |             success: false,
2213 |             nodeId: replacement.nodeId,
2214 |             error: `Node is not a text node: ${replacement.nodeId} (type: ${textNode.type})`,
2215 |           };
2216 |         }
2217 | 
2218 |         // Save original text for the result
2219 |         const originalText = textNode.characters;
2220 |         console.log(`Original text: "${originalText}"`);
2221 |         console.log(`Will translate to: "${replacement.text}"`);
2222 | 
2223 |         // Highlight the node before changing text
2224 |         let originalFills;
2225 |         try {
2226 |           // Save original fills for restoration later
2227 |           originalFills = JSON.parse(JSON.stringify(textNode.fills));
2228 |           // Apply highlight color (orange with 30% opacity)
2229 |           textNode.fills = [
2230 |             {
2231 |               type: "SOLID",
2232 |               color: { r: 1, g: 0.5, b: 0 },
2233 |               opacity: 0.3,
2234 |             },
2235 |           ];
2236 |         } catch (highlightErr) {
2237 |           console.error(
2238 |             `Error highlighting text node: ${highlightErr.message}`
2239 |           );
2240 |           // Continue anyway, highlighting is just visual feedback
2241 |         }
2242 | 
2243 |         // Use the existing setTextContent function to handle font loading and text setting
2244 |         await setTextContent({
2245 |           nodeId: replacement.nodeId,
2246 |           text: replacement.text,
2247 |         });
2248 | 
2249 |         // Keep highlight for a moment after text change, then restore original fills
2250 |         if (originalFills) {
2251 |           try {
2252 |             // Use delay function for consistent timing
2253 |             await delay(500);
2254 |             textNode.fills = originalFills;
2255 |           } catch (restoreErr) {
2256 |             console.error(`Error restoring fills: ${restoreErr.message}`);
2257 |           }
2258 |         }
2259 | 
2260 |         console.log(
2261 |           `Successfully replaced text in node: ${replacement.nodeId}`
2262 |         );
2263 |         return {
2264 |           success: true,
2265 |           nodeId: replacement.nodeId,
2266 |           originalText: originalText,
2267 |           translatedText: replacement.text,
2268 |         };
2269 |       } catch (error) {
2270 |         console.error(
2271 |           `Error replacing text in node ${replacement.nodeId}: ${error.message}`
2272 |         );
2273 |         return {
2274 |           success: false,
2275 |           nodeId: replacement.nodeId,
2276 |           error: `Error applying replacement: ${error.message}`,
2277 |         };
2278 |       }
2279 |     });
2280 | 
2281 |     // Wait for all replacements in this chunk to complete
2282 |     const chunkResults = await Promise.all(chunkPromises);
2283 | 
2284 |     // Process results for this chunk
2285 |     chunkResults.forEach((result) => {
2286 |       if (result.success) {
2287 |         successCount++;
2288 |       } else {
2289 |         failureCount++;
2290 |       }
2291 |       results.push(result);
2292 |     });
2293 | 
2294 |     // Send chunk processing complete update with partial results
2295 |     sendProgressUpdate(
2296 |       commandId,
2297 |       "set_multiple_text_contents",
2298 |       "in_progress",
2299 |       Math.round(5 + ((chunkIndex + 1) / chunks.length) * 90), // 5-95% for processing
2300 |       text.length,
2301 |       successCount + failureCount,
2302 |       `Completed chunk ${chunkIndex + 1}/${chunks.length
2303 |       }. ${successCount} successful, ${failureCount} failed so far.`,
2304 |       {
2305 |         currentChunk: chunkIndex + 1,
2306 |         totalChunks: chunks.length,
2307 |         successCount,
2308 |         failureCount,
2309 |         chunkResults: chunkResults,
2310 |       }
2311 |     );
2312 | 
2313 |     // Add a small delay between chunks to avoid overloading Figma
2314 |     if (chunkIndex < chunks.length - 1) {
2315 |       console.log("Pausing between chunks to avoid overloading Figma...");
2316 |       await delay(1000); // 1 second delay between chunks
2317 |     }
2318 |   }
2319 | 
2320 |   console.log(
2321 |     `Replacement complete: ${successCount} successful, ${failureCount} failed`
2322 |   );
2323 | 
2324 |   // Send completed progress update
2325 |   sendProgressUpdate(
2326 |     commandId,
2327 |     "set_multiple_text_contents",
2328 |     "completed",
2329 |     100,
2330 |     text.length,
2331 |     successCount + failureCount,
2332 |     `Text replacement complete: ${successCount} successful, ${failureCount} failed`,
2333 |     {
2334 |       totalReplacements: text.length,
2335 |       replacementsApplied: successCount,
2336 |       replacementsFailed: failureCount,
2337 |       completedInChunks: chunks.length,
2338 |       results: results,
2339 |     }
2340 |   );
2341 | 
2342 |   return {
2343 |     success: successCount > 0,
2344 |     nodeId: nodeId,
2345 |     replacementsApplied: successCount,
2346 |     replacementsFailed: failureCount,
2347 |     totalReplacements: text.length,
2348 |     results: results,
2349 |     completedInChunks: chunks.length,
2350 |     commandId,
2351 |   };
2352 | }
2353 | 
2354 | // Function to generate simple UUIDs for command IDs
2355 | function generateCommandId() {
2356 |   return (
2357 |     "cmd_" +
2358 |     Math.random().toString(36).substring(2, 15) +
2359 |     Math.random().toString(36).substring(2, 15)
2360 |   );
2361 | }
2362 | 
2363 | async function getAnnotations(params) {
2364 |   try {
2365 |     const { nodeId, includeCategories = true } = params;
2366 | 
2367 |     // Get categories first if needed
2368 |     let categoriesMap = {};
2369 |     if (includeCategories) {
2370 |       const categories = await figma.annotations.getAnnotationCategoriesAsync();
2371 |       categoriesMap = categories.reduce((map, category) => {
2372 |         map[category.id] = {
2373 |           id: category.id,
2374 |           label: category.label,
2375 |           color: category.color,
2376 |           isPreset: category.isPreset,
2377 |         };
2378 |         return map;
2379 |       }, {});
2380 |     }
2381 | 
2382 |     if (nodeId) {
2383 |       // Get annotations for a specific node
2384 |       const node = await figma.getNodeByIdAsync(nodeId);
2385 |       if (!node) {
2386 |         throw new Error(`Node not found: ${nodeId}`);
2387 |       }
2388 | 
2389 |       if (!("annotations" in node)) {
2390 |         throw new Error(`Node type ${node.type} does not support annotations`);
2391 |       }
2392 | 
2393 |       // Collect annotations from this node and all its descendants
2394 |       const mergedAnnotations = [];
2395 |       const collect = async (n) => {
2396 |         if ("annotations" in n && n.annotations && n.annotations.length > 0) {
2397 |           for (const a of n.annotations) {
2398 |             mergedAnnotations.push({ nodeId: n.id, annotation: a });
2399 |           }
2400 |         }
2401 |         if ("children" in n) {
2402 |           for (const child of n.children) {
2403 |             await collect(child);
2404 |           }
2405 |         }
2406 |       };
2407 |       await collect(node);
2408 | 
2409 |       const result = {
2410 |         nodeId: node.id,
2411 |         name: node.name,
2412 |         annotations: mergedAnnotations,
2413 |       };
2414 | 
2415 |       if (includeCategories) {
2416 |         result.categories = Object.values(categoriesMap);
2417 |       }
2418 | 
2419 |       return result;
2420 |     } else {
2421 |       // Get all annotations in the current page
2422 |       const annotations = [];
2423 |       const processNode = async (node) => {
2424 |         if (
2425 |           "annotations" in node &&
2426 |           node.annotations &&
2427 |           node.annotations.length > 0
2428 |         ) {
2429 |           annotations.push({
2430 |             nodeId: node.id,
2431 |             name: node.name,
2432 |             annotations: node.annotations,
2433 |           });
2434 |         }
2435 |         if ("children" in node) {
2436 |           for (const child of node.children) {
2437 |             await processNode(child);
2438 |           }
2439 |         }
2440 |       };
2441 | 
2442 |       // Start from current page
2443 |       await processNode(figma.currentPage);
2444 | 
2445 |       const result = {
2446 |         annotatedNodes: annotations,
2447 |       };
2448 | 
2449 |       if (includeCategories) {
2450 |         result.categories = Object.values(categoriesMap);
2451 |       }
2452 | 
2453 |       return result;
2454 |     }
2455 |   } catch (error) {
2456 |     console.error("Error in getAnnotations:", error);
2457 |     throw error;
2458 |   }
2459 | }
2460 | 
2461 | async function setAnnotation(params) {
2462 |   try {
2463 |     console.log("=== setAnnotation Debug Start ===");
2464 |     console.log("Input params:", JSON.stringify(params, null, 2));
2465 | 
2466 |     const { nodeId, annotationId, labelMarkdown, categoryId, properties } =
2467 |       params;
2468 | 
2469 |     // Validate required parameters
2470 |     if (!nodeId) {
2471 |       console.error("Validation failed: Missing nodeId");
2472 |       return { success: false, error: "Missing nodeId" };
2473 |     }
2474 | 
2475 |     if (!labelMarkdown) {
2476 |       console.error("Validation failed: Missing labelMarkdown");
2477 |       return { success: false, error: "Missing labelMarkdown" };
2478 |     }
2479 | 
2480 |     console.log("Attempting to get node:", nodeId);
2481 |     // Get and validate node
2482 |     const node = await figma.getNodeByIdAsync(nodeId);
2483 |     console.log("Node lookup result:", {
2484 |       id: nodeId,
2485 |       found: !!node,
2486 |       type: node ? node.type : undefined,
2487 |       name: node ? node.name : undefined,
2488 |       hasAnnotations: node ? "annotations" in node : false,
2489 |     });
2490 | 
2491 |     if (!node) {
2492 |       console.error("Node lookup failed:", nodeId);
2493 |       return { success: false, error: `Node not found: ${nodeId}` };
2494 |     }
2495 | 
2496 |     // Validate node supports annotations
2497 |     if (!("annotations" in node)) {
2498 |       console.error("Node annotation support check failed:", {
2499 |         nodeType: node.type,
2500 |         nodeId: node.id,
2501 |       });
2502 |       return {
2503 |         success: false,
2504 |         error: `Node type ${node.type} does not support annotations`,
2505 |       };
2506 |     }
2507 | 
2508 |     // Create the annotation object
2509 |     const newAnnotation = {
2510 |       labelMarkdown,
2511 |     };
2512 | 
2513 |     // Validate and add categoryId if provided
2514 |     if (categoryId) {
2515 |       console.log("Adding categoryId to annotation:", categoryId);
2516 |       newAnnotation.categoryId = categoryId;
2517 |     }
2518 | 
2519 |     // Validate and add properties if provided
2520 |     if (properties && Array.isArray(properties) && properties.length > 0) {
2521 |       console.log(
2522 |         "Adding properties to annotation:",
2523 |         JSON.stringify(properties, null, 2)
2524 |       );
2525 |       newAnnotation.properties = properties;
2526 |     }
2527 | 
2528 |     // Log current annotations before update
2529 |     console.log("Current node annotations:", node.annotations);
2530 | 
2531 |     // Overwrite annotations
2532 |     console.log(
2533 |       "Setting new annotation:",
2534 |       JSON.stringify(newAnnotation, null, 2)
2535 |     );
2536 |     node.annotations = [newAnnotation];
2537 | 
2538 |     // Verify the update
2539 |     console.log("Updated node annotations:", node.annotations);
2540 |     console.log("=== setAnnotation Debug End ===");
2541 | 
2542 |     return {
2543 |       success: true,
2544 |       nodeId: node.id,
2545 |       name: node.name,
2546 |       annotations: node.annotations,
2547 |     };
2548 |   } catch (error) {
2549 |     console.error("=== setAnnotation Error ===");
2550 |     console.error("Error details:", {
2551 |       message: error.message,
2552 |       stack: error.stack,
2553 |       params: JSON.stringify(params, null, 2),
2554 |     });
2555 |     return { success: false, error: error.message };
2556 |   }
2557 | }
2558 | 
2559 | /**
2560 |  * Scan for nodes with specific types within a node
2561 |  * @param {Object} params - Parameters object
2562 |  * @param {string} params.nodeId - ID of the node to scan within
2563 |  * @param {Array<string>} params.types - Array of node types to find (e.g. ['COMPONENT', 'FRAME'])
2564 |  * @returns {Object} - Object containing found nodes
2565 |  */
2566 | async function scanNodesByTypes(params) {
2567 |   console.log(`Starting to scan nodes by types from node ID: ${params.nodeId}`);
2568 |   const { nodeId, types = [] } = params || {};
2569 | 
2570 |   if (!types || types.length === 0) {
2571 |     throw new Error("No types specified to search for");
2572 |   }
2573 | 
2574 |   const node = await figma.getNodeByIdAsync(nodeId);
2575 | 
2576 |   if (!node) {
2577 |     throw new Error(`Node with ID ${nodeId} not found`);
2578 |   }
2579 | 
2580 |   // Simple implementation without chunking
2581 |   const matchingNodes = [];
2582 | 
2583 |   // Send a single progress update to notify start
2584 |   const commandId = generateCommandId();
2585 |   sendProgressUpdate(
2586 |     commandId,
2587 |     "scan_nodes_by_types",
2588 |     "started",
2589 |     0,
2590 |     1,
2591 |     0,
2592 |     `Starting scan of node "${node.name || nodeId}" for types: ${types.join(
2593 |       ", "
2594 |     )}`,
2595 |     null
2596 |   );
2597 | 
2598 |   // Recursively find nodes with specified types
2599 |   await findNodesByTypes(node, types, matchingNodes);
2600 | 
2601 |   // Send completion update
2602 |   sendProgressUpdate(
2603 |     commandId,
2604 |     "scan_nodes_by_types",
2605 |     "completed",
2606 |     100,
2607 |     matchingNodes.length,
2608 |     matchingNodes.length,
2609 |     `Scan complete. Found ${matchingNodes.length} matching nodes.`,
2610 |     { matchingNodes }
2611 |   );
2612 | 
2613 |   return {
2614 |     success: true,
2615 |     message: `Found ${matchingNodes.length} matching nodes.`,
2616 |     count: matchingNodes.length,
2617 |     matchingNodes: matchingNodes,
2618 |     searchedTypes: types,
2619 |   };
2620 | }
2621 | 
2622 | /**
2623 |  * Helper function to recursively find nodes with specific types
2624 |  * @param {SceneNode} node - The root node to start searching from
2625 |  * @param {Array<string>} types - Array of node types to find
2626 |  * @param {Array} matchingNodes - Array to store found nodes
2627 |  */
2628 | async function findNodesByTypes(node, types, matchingNodes = []) {
2629 |   // Skip invisible nodes
2630 |   if (node.visible === false) return;
2631 | 
2632 |   // Check if this node is one of the specified types
2633 |   if (types.includes(node.type)) {
2634 |     // Create a minimal representation with just ID, type and bbox
2635 |     matchingNodes.push({
2636 |       id: node.id,
2637 |       name: node.name || `Unnamed ${node.type}`,
2638 |       type: node.type,
2639 |       // Basic bounding box info
2640 |       bbox: {
2641 |         x: typeof node.x === "number" ? node.x : 0,
2642 |         y: typeof node.y === "number" ? node.y : 0,
2643 |         width: typeof node.width === "number" ? node.width : 0,
2644 |         height: typeof node.height === "number" ? node.height : 0,
2645 |       },
2646 |     });
2647 |   }
2648 | 
2649 |   // Recursively process children of container nodes
2650 |   if ("children" in node) {
2651 |     for (const child of node.children) {
2652 |       await findNodesByTypes(child, types, matchingNodes);
2653 |     }
2654 |   }
2655 | }
2656 | 
2657 | // Set multiple annotations with async progress updates
2658 | async function setMultipleAnnotations(params) {
2659 |   console.log("=== setMultipleAnnotations Debug Start ===");
2660 |   console.log("Input params:", JSON.stringify(params, null, 2));
2661 | 
2662 |   const { nodeId, annotations } = params;
2663 | 
2664 |   if (!annotations || annotations.length === 0) {
2665 |     console.error("Validation failed: No annotations provided");
2666 |     return { success: false, error: "No annotations provided" };
2667 |   }
2668 | 
2669 |   console.log(
2670 |     `Processing ${annotations.length} annotations for node ${nodeId}`
2671 |   );
2672 | 
2673 |   const results = [];
2674 |   let successCount = 0;
2675 |   let failureCount = 0;
2676 | 
2677 |   // Process annotations sequentially
2678 |   for (let i = 0; i < annotations.length; i++) {
2679 |     const annotation = annotations[i];
2680 |     console.log(
2681 |       `\nProcessing annotation ${i + 1}/${annotations.length}:`,
2682 |       JSON.stringify(annotation, null, 2)
2683 |     );
2684 | 
2685 |     try {
2686 |       console.log("Calling setAnnotation with params:", {
2687 |         nodeId: annotation.nodeId,
2688 |         labelMarkdown: annotation.labelMarkdown,
2689 |         categoryId: annotation.categoryId,
2690 |         properties: annotation.properties,
2691 |       });
2692 | 
2693 |       const result = await setAnnotation({
2694 |         nodeId: annotation.nodeId,
2695 |         labelMarkdown: annotation.labelMarkdown,
2696 |         categoryId: annotation.categoryId,
2697 |         properties: annotation.properties,
2698 |       });
2699 | 
2700 |       console.log("setAnnotation result:", JSON.stringify(result, null, 2));
2701 | 
2702 |       if (result.success) {
2703 |         successCount++;
2704 |         results.push({ success: true, nodeId: annotation.nodeId });
2705 |         console.log(`✓ Annotation ${i + 1} applied successfully`);
2706 |       } else {
2707 |         failureCount++;
2708 |         results.push({
2709 |           success: false,
2710 |           nodeId: annotation.nodeId,
2711 |           error: result.error,
2712 |         });
2713 |         console.error(`✗ Annotation ${i + 1} failed:`, result.error);
2714 |       }
2715 |     } catch (error) {
2716 |       failureCount++;
2717 |       const errorResult = {
2718 |         success: false,
2719 |         nodeId: annotation.nodeId,
2720 |         error: error.message,
2721 |       };
2722 |       results.push(errorResult);
2723 |       console.error(`✗ Annotation ${i + 1} failed with error:`, error);
2724 |       console.error("Error details:", {
2725 |         message: error.message,
2726 |         stack: error.stack,
2727 |       });
2728 |     }
2729 |   }
2730 | 
2731 |   const summary = {
2732 |     success: successCount > 0,
2733 |     annotationsApplied: successCount,
2734 |     annotationsFailed: failureCount,
2735 |     totalAnnotations: annotations.length,
2736 |     results: results,
2737 |   };
2738 | 
2739 |   console.log("\n=== setMultipleAnnotations Summary ===");
2740 |   console.log(JSON.stringify(summary, null, 2));
2741 |   console.log("=== setMultipleAnnotations Debug End ===");
2742 | 
2743 |   return summary;
2744 | }
2745 | 
2746 | async function deleteMultipleNodes(params) {
2747 |   const { nodeIds } = params || {};
2748 |   const commandId = generateCommandId();
2749 | 
2750 |   if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
2751 |     const errorMsg = "Missing or invalid nodeIds parameter";
2752 |     sendProgressUpdate(
2753 |       commandId,
2754 |       "delete_multiple_nodes",
2755 |       "error",
2756 |       0,
2757 |       0,
2758 |       0,
2759 |       errorMsg,
2760 |       { error: errorMsg }
2761 |     );
2762 |     throw new Error(errorMsg);
2763 |   }
2764 | 
2765 |   console.log(`Starting deletion of ${nodeIds.length} nodes`);
2766 | 
2767 |   // Send started progress update
2768 |   sendProgressUpdate(
2769 |     commandId,
2770 |     "delete_multiple_nodes",
2771 |     "started",
2772 |     0,
2773 |     nodeIds.length,
2774 |     0,
2775 |     `Starting deletion of ${nodeIds.length} nodes`,
2776 |     { totalNodes: nodeIds.length }
2777 |   );
2778 | 
2779 |   const results = [];
2780 |   let successCount = 0;
2781 |   let failureCount = 0;
2782 | 
2783 |   // Process nodes in chunks of 5 to avoid overwhelming Figma
2784 |   const CHUNK_SIZE = 5;
2785 |   const chunks = [];
2786 | 
2787 |   for (let i = 0; i < nodeIds.length; i += CHUNK_SIZE) {
2788 |     chunks.push(nodeIds.slice(i, i + CHUNK_SIZE));
2789 |   }
2790 | 
2791 |   console.log(`Split ${nodeIds.length} deletions into ${chunks.length} chunks`);
2792 | 
2793 |   // Send chunking info update
2794 |   sendProgressUpdate(
2795 |     commandId,
2796 |     "delete_multiple_nodes",
2797 |     "in_progress",
2798 |     5,
2799 |     nodeIds.length,
2800 |     0,
2801 |     `Preparing to delete ${nodeIds.length} nodes using ${chunks.length} chunks`,
2802 |     {
2803 |       totalNodes: nodeIds.length,
2804 |       chunks: chunks.length,
2805 |       chunkSize: CHUNK_SIZE,
2806 |     }
2807 |   );
2808 | 
2809 |   // Process each chunk sequentially
2810 |   for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
2811 |     const chunk = chunks[chunkIndex];
2812 |     console.log(
2813 |       `Processing chunk ${chunkIndex + 1}/${chunks.length} with ${chunk.length
2814 |       } nodes`
2815 |     );
2816 | 
2817 |     // Send chunk processing start update
2818 |     sendProgressUpdate(
2819 |       commandId,
2820 |       "delete_multiple_nodes",
2821 |       "in_progress",
2822 |       Math.round(5 + (chunkIndex / chunks.length) * 90),
2823 |       nodeIds.length,
2824 |       successCount + failureCount,
2825 |       `Processing deletion chunk ${chunkIndex + 1}/${chunks.length}`,
2826 |       {
2827 |         currentChunk: chunkIndex + 1,
2828 |         totalChunks: chunks.length,
2829 |         successCount,
2830 |         failureCount,
2831 |       }
2832 |     );
2833 | 
2834 |     // Process deletions within a chunk in parallel
2835 |     const chunkPromises = chunk.map(async (nodeId) => {
2836 |       try {
2837 |         const node = await figma.getNodeByIdAsync(nodeId);
2838 | 
2839 |         if (!node) {
2840 |           console.error(`Node not found: ${nodeId}`);
2841 |           return {
2842 |             success: false,
2843 |             nodeId: nodeId,
2844 |             error: `Node not found: ${nodeId}`,
2845 |           };
2846 |         }
2847 | 
2848 |         // Save node info before deleting
2849 |         const nodeInfo = {
2850 |           id: node.id,
2851 |           name: node.name,
2852 |           type: node.type,
2853 |         };
2854 | 
2855 |         // Delete the node
2856 |         node.remove();
2857 | 
2858 |         console.log(`Successfully deleted node: ${nodeId}`);
2859 |         return {
2860 |           success: true,
2861 |           nodeId: nodeId,
2862 |           nodeInfo: nodeInfo,
2863 |         };
2864 |       } catch (error) {
2865 |         console.error(`Error deleting node ${nodeId}: ${error.message}`);
2866 |         return {
2867 |           success: false,
2868 |           nodeId: nodeId,
2869 |           error: error.message,
2870 |         };
2871 |       }
2872 |     });
2873 | 
2874 |     // Wait for all deletions in this chunk to complete
2875 |     const chunkResults = await Promise.all(chunkPromises);
2876 | 
2877 |     // Process results for this chunk
2878 |     chunkResults.forEach((result) => {
2879 |       if (result.success) {
2880 |         successCount++;
2881 |       } else {
2882 |         failureCount++;
2883 |       }
2884 |       results.push(result);
2885 |     });
2886 | 
2887 |     // Send chunk processing complete update
2888 |     sendProgressUpdate(
2889 |       commandId,
2890 |       "delete_multiple_nodes",
2891 |       "in_progress",
2892 |       Math.round(5 + ((chunkIndex + 1) / chunks.length) * 90),
2893 |       nodeIds.length,
2894 |       successCount + failureCount,
2895 |       `Completed chunk ${chunkIndex + 1}/${chunks.length
2896 |       }. ${successCount} successful, ${failureCount} failed so far.`,
2897 |       {
2898 |         currentChunk: chunkIndex + 1,
2899 |         totalChunks: chunks.length,
2900 |         successCount,
2901 |         failureCount,
2902 |         chunkResults: chunkResults,
2903 |       }
2904 |     );
2905 | 
2906 |     // Add a small delay between chunks
2907 |     if (chunkIndex < chunks.length - 1) {
2908 |       console.log("Pausing between chunks...");
2909 |       await delay(1000);
2910 |     }
2911 |   }
2912 | 
2913 |   console.log(
2914 |     `Deletion complete: ${successCount} successful, ${failureCount} failed`
2915 |   );
2916 | 
2917 |   // Send completed progress update
2918 |   sendProgressUpdate(
2919 |     commandId,
2920 |     "delete_multiple_nodes",
2921 |     "completed",
2922 |     100,
2923 |     nodeIds.length,
2924 |     successCount + failureCount,
2925 |     `Node deletion complete: ${successCount} successful, ${failureCount} failed`,
2926 |     {
2927 |       totalNodes: nodeIds.length,
2928 |       nodesDeleted: successCount,
2929 |       nodesFailed: failureCount,
2930 |       completedInChunks: chunks.length,
2931 |       results: results,
2932 |     }
2933 |   );
2934 | 
2935 |   return {
2936 |     success: successCount > 0,
2937 |     nodesDeleted: successCount,
2938 |     nodesFailed: failureCount,
2939 |     totalNodes: nodeIds.length,
2940 |     results: results,
2941 |     completedInChunks: chunks.length,
2942 |     commandId,
2943 |   };
2944 | }
2945 | 
2946 | // Implementation for getInstanceOverrides function
2947 | async function getInstanceOverrides(instanceNode = null) {
2948 |   console.log("=== getInstanceOverrides called ===");
2949 | 
2950 |   let sourceInstance = null;
2951 | 
2952 |   // Check if an instance node was passed directly
2953 |   if (instanceNode) {
2954 |     console.log("Using provided instance node");
2955 | 
2956 |     // Validate that the provided node is an instance
2957 |     if (instanceNode.type !== "INSTANCE") {
2958 |       console.error("Provided node is not an instance");
2959 |       figma.notify("Provided node is not a component instance");
2960 |       return { success: false, message: "Provided node is not a component instance" };
2961 |     }
2962 | 
2963 |     sourceInstance = instanceNode;
2964 |   } else {
2965 |     // No node provided, use selection
2966 |     console.log("No node provided, using current selection");
2967 | 
2968 |     // Get the current selection
2969 |     const selection = figma.currentPage.selection;
2970 | 
2971 |     // Check if there's anything selected
2972 |     if (selection.length === 0) {
2973 |       console.log("No nodes selected");
2974 |       figma.notify("Please select at least one instance");
2975 |       return { success: false, message: "No nodes selected" };
2976 |     }
2977 | 
2978 |     // Filter for instances in the selection
2979 |     const instances = selection.filter(node => node.type === "INSTANCE");
2980 | 
2981 |     if (instances.length === 0) {
2982 |       console.log("No instances found in selection");
2983 |       figma.notify("Please select at least one component instance");
2984 |       return { success: false, message: "No instances found in selection" };
2985 |     }
2986 | 
2987 |     // Take the first instance from the selection
2988 |     sourceInstance = instances[0];
2989 |   }
2990 | 
2991 |   try {
2992 |     console.log(`Getting instance information:`);
2993 |     console.log(sourceInstance);
2994 | 
2995 |     // Get component overrides and main component
2996 |     const overrides = sourceInstance.overrides || [];
2997 |     console.log(`  Raw Overrides:`, overrides);
2998 | 
2999 |     // Get main component
3000 |     const mainComponent = await sourceInstance.getMainComponentAsync();
3001 |     if (!mainComponent) {
3002 |       console.error("Failed to get main component");
3003 |       figma.notify("Failed to get main component");
3004 |       return { success: false, message: "Failed to get main component" };
3005 |     }
3006 | 
3007 |     // return data to MCP server
3008 |     const returnData = {
3009 |       success: true,
3010 |       message: `Got component information from "${sourceInstance.name}" for overrides.length: ${overrides.length}`,
3011 |       sourceInstanceId: sourceInstance.id,
3012 |       mainComponentId: mainComponent.id,
3013 |       overridesCount: overrides.length
3014 |     };
3015 | 
3016 |     console.log("Data to return to MCP server:", returnData);
3017 |     figma.notify(`Got component information from "${sourceInstance.name}"`);
3018 | 
3019 |     return returnData;
3020 |   } catch (error) {
3021 |     console.error("Error in getInstanceOverrides:", error);
3022 |     figma.notify(`Error: ${error.message}`);
3023 |     return {
3024 |       success: false,
3025 |       message: `Error: ${error.message}`
3026 |     };
3027 |   }
3028 | }
3029 | 
3030 | /**
3031 |  * Helper function to validate and get target instances
3032 |  * @param {string[]} targetNodeIds - Array of instance node IDs
3033 |  * @returns {instanceNode[]} targetInstances - Array of target instances
3034 |  */
3035 | async function getValidTargetInstances(targetNodeIds) {
3036 |   let targetInstances = [];
3037 | 
3038 |   // Handle array of instances or single instance
3039 |   if (Array.isArray(targetNodeIds)) {
3040 |     if (targetNodeIds.length === 0) {
3041 |       return { success: false, message: "No instances provided" };
3042 |     }
3043 |     for (const targetNodeId of targetNodeIds) {
3044 |       const targetNode = await figma.getNodeByIdAsync(targetNodeId);
3045 |       if (targetNode && targetNode.type === "INSTANCE") {
3046 |         targetInstances.push(targetNode);
3047 |       }
3048 |     }
3049 |     if (targetInstances.length === 0) {
3050 |       return { success: false, message: "No valid instances provided" };
3051 |     }
3052 |   } else {
3053 |     return { success: false, message: "Invalid target node IDs provided" };
3054 |   }
3055 | 
3056 | 
3057 |   return { success: true, message: "Valid target instances provided", targetInstances };
3058 | }
3059 | 
3060 | /**
3061 |  * Helper function to validate and get saved override data
3062 |  * @param {string} sourceInstanceId - Source instance ID
3063 |  * @returns {Promise<Object>} - Validation result with source instance data or error
3064 |  */
3065 | async function getSourceInstanceData(sourceInstanceId) {
3066 |   if (!sourceInstanceId) {
3067 |     return { success: false, message: "Missing source instance ID" };
3068 |   }
3069 | 
3070 |   // Get source instance by ID
3071 |   const sourceInstance = await figma.getNodeByIdAsync(sourceInstanceId);
3072 |   if (!sourceInstance) {
3073 |     return {
3074 |       success: false,
3075 |       message: "Source instance not found. The original instance may have been deleted."
3076 |     };
3077 |   }
3078 | 
3079 |   // Verify it's an instance
3080 |   if (sourceInstance.type !== "INSTANCE") {
3081 |     return {
3082 |       success: false,
3083 |       message: "Source node is not a component instance."
3084 |     };
3085 |   }
3086 | 
3087 |   // Get main component
3088 |   const mainComponent = await sourceInstance.getMainComponentAsync();
3089 |   if (!mainComponent) {
3090 |     return {
3091 |       success: false,
3092 |       message: "Failed to get main component from source instance."
3093 |     };
3094 |   }
3095 | 
3096 |   return {
3097 |     success: true,
3098 |     sourceInstance,
3099 |     mainComponent,
3100 |     overrides: sourceInstance.overrides || []
3101 |   };
3102 | }
3103 | 
3104 | /**
3105 |  * Sets saved overrides to the selected component instance(s)
3106 |  * @param {InstanceNode[] | null} targetInstances - Array of instance nodes to set overrides to
3107 |  * @param {Object} sourceResult - Source instance data from getSourceInstanceData
3108 |  * @returns {Promise<Object>} - Result of the set operation
3109 |  */
3110 | async function setInstanceOverrides(targetInstances, sourceResult) {
3111 |   try {
3112 | 
3113 | 
3114 |     const { sourceInstance, mainComponent, overrides } = sourceResult;
3115 | 
3116 |     console.log(`Processing ${targetInstances.length} instances with ${overrides.length} overrides`);
3117 |     console.log(`Source instance: ${sourceInstance.id}, Main component: ${mainComponent.id}`);
3118 |     console.log(`Overrides:`, overrides);
3119 | 
3120 |     // Process all instances
3121 |     const results = [];
3122 |     let totalAppliedCount = 0;
3123 | 
3124 |     for (const targetInstance of targetInstances) {
3125 |       try {
3126 |         // // Skip if trying to apply to the source instance itself
3127 |         // if (targetInstance.id === sourceInstance.id) {
3128 |         //   console.log(`Skipping source instance itself: ${targetInstance.id}`);
3129 |         //   results.push({
3130 |         //     success: false,
3131 |         //     instanceId: targetInstance.id,
3132 |         //     instanceName: targetInstance.name,
3133 |         //     message: "This is the source instance itself, skipping"
3134 |         //   });
3135 |         //   continue;
3136 |         // }
3137 | 
3138 |         // Swap component
3139 |         try {
3140 |           targetInstance.swapComponent(mainComponent);
3141 |           console.log(`Swapped component for instance "${targetInstance.name}"`);
3142 |         } catch (error) {
3143 |           console.error(`Error swapping component for instance "${targetInstance.name}":`, error);
3144 |           results.push({
3145 |             success: false,
3146 |             instanceId: targetInstance.id,
3147 |             instanceName: targetInstance.name,
3148 |             message: `Error: ${error.message}`
3149 |           });
3150 |         }
3151 | 
3152 |         // Prepare overrides by replacing node IDs
3153 |         let appliedCount = 0;
3154 | 
3155 |         // Apply each override
3156 |         for (const override of overrides) {
3157 |           // Skip if no ID or overriddenFields
3158 |           if (!override.id || !override.overriddenFields || override.overriddenFields.length === 0) {
3159 |             continue;
3160 |           }
3161 | 
3162 |           // Replace source instance ID with target instance ID in the node path
3163 |           const overrideNodeId = override.id.replace(sourceInstance.id, targetInstance.id);
3164 |           const overrideNode = await figma.getNodeByIdAsync(overrideNodeId);
3165 | 
3166 |           if (!overrideNode) {
3167 |             console.log(`Override node not found: ${overrideNodeId}`);
3168 |             continue;
3169 |           }
3170 | 
3171 |           // Get source node to copy properties from
3172 |           const sourceNode = await figma.getNodeByIdAsync(override.id);
3173 |           if (!sourceNode) {
3174 |             console.log(`Source node not found: ${override.id}`);
3175 |             continue;
3176 |           }
3177 | 
3178 |           // Apply each overridden field
3179 |           let fieldApplied = false;
3180 |           for (const field of override.overriddenFields) {
3181 |             try {
3182 |               if (field === "componentProperties") {
3183 |                 // Apply component properties
3184 |                 if (sourceNode.componentProperties && overrideNode.componentProperties) {
3185 |                   const properties = {};
3186 |                   for (const key in sourceNode.componentProperties) {
3187 |                     // if INSTANCE_SWAP use id, otherwise use value
3188 |                     if (sourceNode.componentProperties[key].type === 'INSTANCE_SWAP') {
3189 |                       properties[key] = sourceNode.componentProperties[key].value;
3190 |                     
3191 |                     } else {
3192 |                       properties[key] = sourceNode.componentProperties[key].value;
3193 |                     }
3194 |                   }
3195 |                   overrideNode.setProperties(properties);
3196 |                   fieldApplied = true;
3197 |                 }
3198 |               } else if (field === "characters" && overrideNode.type === "TEXT") {
3199 |                 // For text nodes, need to load fonts first
3200 |                 await figma.loadFontAsync(overrideNode.fontName);
3201 |                 overrideNode.characters = sourceNode.characters;
3202 |                 fieldApplied = true;
3203 |               } else if (field in overrideNode) {
3204 |                 // Direct property assignment
3205 |                 overrideNode[field] = sourceNode[field];
3206 |                 fieldApplied = true;
3207 |               }
3208 |             } catch (fieldError) {
3209 |               console.error(`Error applying field ${field}:`, fieldError);
3210 |             }
3211 |           }
3212 | 
3213 |           if (fieldApplied) {
3214 |             appliedCount++;
3215 |           }
3216 |         }
3217 | 
3218 |         if (appliedCount > 0) {
3219 |           totalAppliedCount += appliedCount;
3220 |           results.push({
3221 |             success: true,
3222 |             instanceId: targetInstance.id,
3223 |             instanceName: targetInstance.name,
3224 |             appliedCount
3225 |           });
3226 |           console.log(`Applied ${appliedCount} overrides to "${targetInstance.name}"`);
3227 |         } else {
3228 |           results.push({
3229 |             success: false,
3230 |             instanceId: targetInstance.id,
3231 |             instanceName: targetInstance.name,
3232 |             message: "No overrides were applied"
3233 |           });
3234 |         }
3235 |       } catch (instanceError) {
3236 |         console.error(`Error processing instance "${targetInstance.name}":`, instanceError);
3237 |         results.push({
3238 |           success: false,
3239 |           instanceId: targetInstance.id,
3240 |           instanceName: targetInstance.name,
3241 |           message: `Error: ${instanceError.message}`
3242 |         });
3243 |       }
3244 |     }
3245 | 
3246 |     // Return results
3247 |     if (totalAppliedCount > 0) {
3248 |       const instanceCount = results.filter(r => r.success).length;
3249 |       const message = `Applied ${totalAppliedCount} overrides to ${instanceCount} instances`;
3250 |       figma.notify(message);
3251 |       return {
3252 |         success: true,
3253 |         message,
3254 |         totalCount: totalAppliedCount,
3255 |         results
3256 |       };
3257 |     } else {
3258 |       const message = "No overrides applied to any instance";
3259 |       figma.notify(message);
3260 |       return { success: false, message, results };
3261 |     }
3262 | 
3263 |   } catch (error) {
3264 |     console.error("Error in setInstanceOverrides:", error);
3265 |     const message = `Error: ${error.message}`;
3266 |     figma.notify(message);
3267 |     return { success: false, message };
3268 |   }
3269 | }
3270 | 
3271 | async function setLayoutMode(params) {
3272 |   const { nodeId, layoutMode = "NONE", layoutWrap = "NO_WRAP" } = params || {};
3273 | 
3274 |   // Get the target node
3275 |   const node = await figma.getNodeByIdAsync(nodeId);
3276 |   if (!node) {
3277 |     throw new Error(`Node with ID ${nodeId} not found`);
3278 |   }
3279 | 
3280 |   // Check if node is a frame or component that supports layoutMode
3281 |   if (
3282 |     node.type !== "FRAME" &&
3283 |     node.type !== "COMPONENT" &&
3284 |     node.type !== "COMPONENT_SET" &&
3285 |     node.type !== "INSTANCE"
3286 |   ) {
3287 |     throw new Error(`Node type ${node.type} does not support layoutMode`);
3288 |   }
3289 | 
3290 |   // Set layout mode
3291 |   node.layoutMode = layoutMode;
3292 | 
3293 |   // Set layoutWrap if applicable
3294 |   if (layoutMode !== "NONE") {
3295 |     node.layoutWrap = layoutWrap;
3296 |   }
3297 | 
3298 |   return {
3299 |     id: node.id,
3300 |     name: node.name,
3301 |     layoutMode: node.layoutMode,
3302 |     layoutWrap: node.layoutWrap,
3303 |   };
3304 | }
3305 | 
3306 | async function setPadding(params) {
3307 |   const { nodeId, paddingTop, paddingRight, paddingBottom, paddingLeft } =
3308 |     params || {};
3309 | 
3310 |   // Get the target node
3311 |   const node = await figma.getNodeByIdAsync(nodeId);
3312 |   if (!node) {
3313 |     throw new Error(`Node with ID ${nodeId} not found`);
3314 |   }
3315 | 
3316 |   // Check if node is a frame or component that supports padding
3317 |   if (
3318 |     node.type !== "FRAME" &&
3319 |     node.type !== "COMPONENT" &&
3320 |     node.type !== "COMPONENT_SET" &&
3321 |     node.type !== "INSTANCE"
3322 |   ) {
3323 |     throw new Error(`Node type ${node.type} does not support padding`);
3324 |   }
3325 | 
3326 |   // Check if the node has auto-layout enabled
3327 |   if (node.layoutMode === "NONE") {
3328 |     throw new Error(
3329 |       "Padding can only be set on auto-layout frames (layoutMode must not be NONE)"
3330 |     );
3331 |   }
3332 | 
3333 |   // Set padding values if provided
3334 |   if (paddingTop !== undefined) node.paddingTop = paddingTop;
3335 |   if (paddingRight !== undefined) node.paddingRight = paddingRight;
3336 |   if (paddingBottom !== undefined) node.paddingBottom = paddingBottom;
3337 |   if (paddingLeft !== undefined) node.paddingLeft = paddingLeft;
3338 | 
3339 |   return {
3340 |     id: node.id,
3341 |     name: node.name,
3342 |     paddingTop: node.paddingTop,
3343 |     paddingRight: node.paddingRight,
3344 |     paddingBottom: node.paddingBottom,
3345 |     paddingLeft: node.paddingLeft,
3346 |   };
3347 | }
3348 | 
3349 | async function setAxisAlign(params) {
3350 |   const { nodeId, primaryAxisAlignItems, counterAxisAlignItems } = params || {};
3351 | 
3352 |   // Get the target node
3353 |   const node = await figma.getNodeByIdAsync(nodeId);
3354 |   if (!node) {
3355 |     throw new Error(`Node with ID ${nodeId} not found`);
3356 |   }
3357 | 
3358 |   // Check if node is a frame or component that supports axis alignment
3359 |   if (
3360 |     node.type !== "FRAME" &&
3361 |     node.type !== "COMPONENT" &&
3362 |     node.type !== "COMPONENT_SET" &&
3363 |     node.type !== "INSTANCE"
3364 |   ) {
3365 |     throw new Error(`Node type ${node.type} does not support axis alignment`);
3366 |   }
3367 | 
3368 |   // Check if the node has auto-layout enabled
3369 |   if (node.layoutMode === "NONE") {
3370 |     throw new Error(
3371 |       "Axis alignment can only be set on auto-layout frames (layoutMode must not be NONE)"
3372 |     );
3373 |   }
3374 | 
3375 |   // Validate and set primaryAxisAlignItems if provided
3376 |   if (primaryAxisAlignItems !== undefined) {
3377 |     if (
3378 |       !["MIN", "MAX", "CENTER", "SPACE_BETWEEN"].includes(primaryAxisAlignItems)
3379 |     ) {
3380 |       throw new Error(
3381 |         "Invalid primaryAxisAlignItems value. Must be one of: MIN, MAX, CENTER, SPACE_BETWEEN"
3382 |       );
3383 |     }
3384 |     node.primaryAxisAlignItems = primaryAxisAlignItems;
3385 |   }
3386 | 
3387 |   // Validate and set counterAxisAlignItems if provided
3388 |   if (counterAxisAlignItems !== undefined) {
3389 |     if (!["MIN", "MAX", "CENTER", "BASELINE"].includes(counterAxisAlignItems)) {
3390 |       throw new Error(
3391 |         "Invalid counterAxisAlignItems value. Must be one of: MIN, MAX, CENTER, BASELINE"
3392 |       );
3393 |     }
3394 |     // BASELINE is only valid for horizontal layout
3395 |     if (
3396 |       counterAxisAlignItems === "BASELINE" &&
3397 |       node.layoutMode !== "HORIZONTAL"
3398 |     ) {
3399 |       throw new Error(
3400 |         "BASELINE alignment is only valid for horizontal auto-layout frames"
3401 |       );
3402 |     }
3403 |     node.counterAxisAlignItems = counterAxisAlignItems;
3404 |   }
3405 | 
3406 |   return {
3407 |     id: node.id,
3408 |     name: node.name,
3409 |     primaryAxisAlignItems: node.primaryAxisAlignItems,
3410 |     counterAxisAlignItems: node.counterAxisAlignItems,
3411 |     layoutMode: node.layoutMode,
3412 |   };
3413 | }
3414 | 
3415 | async function setLayoutSizing(params) {
3416 |   const { nodeId, layoutSizingHorizontal, layoutSizingVertical } = params || {};
3417 | 
3418 |   // Get the target node
3419 |   const node = await figma.getNodeByIdAsync(nodeId);
3420 |   if (!node) {
3421 |     throw new Error(`Node with ID ${nodeId} not found`);
3422 |   }
3423 | 
3424 |   // Check if node is a frame or component that supports layout sizing
3425 |   if (
3426 |     node.type !== "FRAME" &&
3427 |     node.type !== "COMPONENT" &&
3428 |     node.type !== "COMPONENT_SET" &&
3429 |     node.type !== "INSTANCE"
3430 |   ) {
3431 |     throw new Error(`Node type ${node.type} does not support layout sizing`);
3432 |   }
3433 | 
3434 |   // Check if the node has auto-layout enabled
3435 |   if (node.layoutMode === "NONE") {
3436 |     throw new Error(
3437 |       "Layout sizing can only be set on auto-layout frames (layoutMode must not be NONE)"
3438 |     );
3439 |   }
3440 | 
3441 |   // Validate and set layoutSizingHorizontal if provided
3442 |   if (layoutSizingHorizontal !== undefined) {
3443 |     if (!["FIXED", "HUG", "FILL"].includes(layoutSizingHorizontal)) {
3444 |       throw new Error(
3445 |         "Invalid layoutSizingHorizontal value. Must be one of: FIXED, HUG, FILL"
3446 |       );
3447 |     }
3448 |     // HUG is only valid on auto-layout frames and text nodes
3449 |     if (
3450 |       layoutSizingHorizontal === "HUG" &&
3451 |       !["FRAME", "TEXT"].includes(node.type)
3452 |     ) {
3453 |       throw new Error(
3454 |         "HUG sizing is only valid on auto-layout frames and text nodes"
3455 |       );
3456 |     }
3457 |     // FILL is only valid on auto-layout children
3458 |     if (
3459 |       layoutSizingHorizontal === "FILL" &&
3460 |       (!node.parent || node.parent.layoutMode === "NONE")
3461 |     ) {
3462 |       throw new Error("FILL sizing is only valid on auto-layout children");
3463 |     }
3464 |     node.layoutSizingHorizontal = layoutSizingHorizontal;
3465 |   }
3466 | 
3467 |   // Validate and set layoutSizingVertical if provided
3468 |   if (layoutSizingVertical !== undefined) {
3469 |     if (!["FIXED", "HUG", "FILL"].includes(layoutSizingVertical)) {
3470 |       throw new Error(
3471 |         "Invalid layoutSizingVertical value. Must be one of: FIXED, HUG, FILL"
3472 |       );
3473 |     }
3474 |     // HUG is only valid on auto-layout frames and text nodes
3475 |     if (
3476 |       layoutSizingVertical === "HUG" &&
3477 |       !["FRAME", "TEXT"].includes(node.type)
3478 |     ) {
3479 |       throw new Error(
3480 |         "HUG sizing is only valid on auto-layout frames and text nodes"
3481 |       );
3482 |     }
3483 |     // FILL is only valid on auto-layout children
3484 |     if (
3485 |       layoutSizingVertical === "FILL" &&
3486 |       (!node.parent || node.parent.layoutMode === "NONE")
3487 |     ) {
3488 |       throw new Error("FILL sizing is only valid on auto-layout children");
3489 |     }
3490 |     node.layoutSizingVertical = layoutSizingVertical;
3491 |   }
3492 | 
3493 |   return {
3494 |     id: node.id,
3495 |     name: node.name,
3496 |     layoutSizingHorizontal: node.layoutSizingHorizontal,
3497 |     layoutSizingVertical: node.layoutSizingVertical,
3498 |     layoutMode: node.layoutMode,
3499 |   };
3500 | }
3501 | 
3502 | async function setItemSpacing(params) {
3503 |   const { nodeId, itemSpacing, counterAxisSpacing } = params || {};
3504 | 
3505 |   // Validate that at least one spacing parameter is provided
3506 |   if (itemSpacing === undefined && counterAxisSpacing === undefined) {
3507 |     throw new Error("At least one of itemSpacing or counterAxisSpacing must be provided");
3508 |   }
3509 | 
3510 |   // Get the target node
3511 |   const node = await figma.getNodeByIdAsync(nodeId);
3512 |   if (!node) {
3513 |     throw new Error(`Node with ID ${nodeId} not found`);
3514 |   }
3515 | 
3516 |   // Check if node is a frame or component that supports item spacing
3517 |   if (
3518 |     node.type !== "FRAME" &&
3519 |     node.type !== "COMPONENT" &&
3520 |     node.type !== "COMPONENT_SET" &&
3521 |     node.type !== "INSTANCE"
3522 |   ) {
3523 |     throw new Error(`Node type ${node.type} does not support item spacing`);
3524 |   }
3525 | 
3526 |   // Check if the node has auto-layout enabled
3527 |   if (node.layoutMode === "NONE") {
3528 |     throw new Error(
3529 |       "Item spacing can only be set on auto-layout frames (layoutMode must not be NONE)"
3530 |     );
3531 |   }
3532 | 
3533 |   // Set item spacing if provided
3534 |   if (itemSpacing !== undefined) {
3535 |     if (typeof itemSpacing !== "number") {
3536 |       throw new Error("Item spacing must be a number");
3537 |     }
3538 |     node.itemSpacing = itemSpacing;
3539 |   }
3540 | 
3541 |   // Set counter axis spacing if provided
3542 |   if (counterAxisSpacing !== undefined) {
3543 |     if (typeof counterAxisSpacing !== "number") {
3544 |       throw new Error("Counter axis spacing must be a number");
3545 |     }
3546 |     // counterAxisSpacing only applies when layoutWrap is WRAP
3547 |     if (node.layoutWrap !== "WRAP") {
3548 |       throw new Error(
3549 |         "Counter axis spacing can only be set on frames with layoutWrap set to WRAP"
3550 |       );
3551 |     }
3552 |     node.counterAxisSpacing = counterAxisSpacing;
3553 |   }
3554 | 
3555 |   return {
3556 |     id: node.id,
3557 |     name: node.name,
3558 |     itemSpacing: node.itemSpacing || undefined,
3559 |     counterAxisSpacing: node.counterAxisSpacing || undefined,
3560 |     layoutMode: node.layoutMode,
3561 |     layoutWrap: node.layoutWrap,
3562 |   };
3563 | }
3564 | 
3565 | async function setDefaultConnector(params) {
3566 |   const { connectorId } = params || {};
3567 |   
3568 |   // If connectorId is provided, search and set by that ID (do not check existing storage)
3569 |   if (connectorId) {
3570 |     // Get node by specified ID
3571 |     const node = await figma.getNodeByIdAsync(connectorId);
3572 |     if (!node) {
3573 |       throw new Error(`Connector node not found with ID: ${connectorId}`);
3574 |     }
3575 |     
3576 |     // Check node type
3577 |     if (node.type !== 'CONNECTOR') {
3578 |       throw new Error(`Node is not a connector: ${connectorId}`);
3579 |     }
3580 |     
3581 |     // Set the found connector as the default connector
3582 |     await figma.clientStorage.setAsync('defaultConnectorId', connectorId);
3583 |     
3584 |     return {
3585 |       success: true,
3586 |       message: `Default connector set to: ${connectorId}`,
3587 |       connectorId: connectorId
3588 |     };
3589 |   } 
3590 |   // If connectorId is not provided, check existing storage
3591 |   else {
3592 |     // Check if there is an existing default connector in client storage
3593 |     try {
3594 |       const existingConnectorId = await figma.clientStorage.getAsync('defaultConnectorId');
3595 |       
3596 |       // If there is an existing connector ID, check if the node is still valid
3597 |       if (existingConnectorId) {
3598 |         try {
3599 |           const existingConnector = await figma.getNodeByIdAsync(existingConnectorId);
3600 |           
3601 |           // If the stored connector still exists and is of type CONNECTOR
3602 |           if (existingConnector && existingConnector.type === 'CONNECTOR') {
3603 |             return {
3604 |               success: true,
3605 |               message: `Default connector is already set to: ${existingConnectorId}`,
3606 |               connectorId: existingConnectorId,
3607 |               exists: true
3608 |             };
3609 |           }
3610 |           // The stored connector is no longer valid - find a new connector
3611 |           else {
3612 |             console.log(`Stored connector ID ${existingConnectorId} is no longer valid, finding a new connector...`);
3613 |           }
3614 |         } catch (error) {
3615 |           console.log(`Error finding stored connector: ${error.message}. Will try to set a new one.`);
3616 |         }
3617 |       }
3618 |     } catch (error) {
3619 |       console.log(`Error checking for existing connector: ${error.message}`);
3620 |     }
3621 |     
3622 |     // If there is no stored default connector or it is invalid, find one in the current page
3623 |     try {
3624 |       // Find CONNECTOR type nodes in the current page
3625 |       const currentPageConnectors = figma.currentPage.findAllWithCriteria({ types: ['CONNECTOR'] });
3626 |       
3627 |       if (currentPageConnectors && currentPageConnectors.length > 0) {
3628 |         // Use the first connector found
3629 |         const foundConnector = currentPageConnectors[0];
3630 |         const autoFoundId = foundConnector.id;
3631 |         
3632 |         // Set the found connector as the default connector
3633 |         await figma.clientStorage.setAsync('defaultConnectorId', autoFoundId);
3634 |         
3635 |         return {
3636 |           success: true,
3637 |           message: `Automatically found and set default connector to: ${autoFoundId}`,
3638 |           connectorId: autoFoundId,
3639 |           autoSelected: true
3640 |         };
3641 |       } else {
3642 |         // If no connector is found in the current page, show a guide message
3643 |         throw new Error('No connector found in the current page. Please create a connector in Figma first or specify a connector ID.');
3644 |       }
3645 |     } catch (error) {
3646 |       // Error occurred while running findAllWithCriteria
3647 |       throw new Error(`Failed to find a connector: ${error.message}`);
3648 |     }
3649 |   }
3650 | }
3651 | 
3652 | async function createCursorNode(targetNodeId) {
3653 |   const svgString = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
3654 |     <path d="M16 8V35.2419L22 28.4315L27 39.7823C27 39.7823 28.3526 40.2722 29 39.7823C29.6474 39.2924 30.2913 38.3057 30 37.5121C28.6247 33.7654 25 26.1613 25 26.1613H32L16 8Z" fill="#202125" />
3655 |   </svg>`;
3656 |   try {
3657 |     const targetNode = await figma.getNodeByIdAsync(targetNodeId);
3658 |     if (!targetNode) throw new Error("Target node not found");
3659 | 
3660 |     // The targetNodeId has semicolons since it is a nested node.
3661 |     // So we need to get the parent node ID from the target node ID and check if we can appendChild to it or not.
3662 |     let parentNodeId = targetNodeId.includes(';') 
3663 |       ? targetNodeId.split(';')[0] 
3664 |       : targetNodeId;
3665 |     if (!parentNodeId) throw new Error("Could not determine parent node ID");
3666 | 
3667 |     // Find the parent node to append cursor node as child
3668 |     let parentNode = await figma.getNodeByIdAsync(parentNodeId);
3669 |     if (!parentNode) throw new Error("Parent node not found");
3670 | 
3671 |     // If the parent node is not eligible to appendChild, set the parentNode to the parent of the parentNode
3672 |     if (parentNode.type === 'INSTANCE' || parentNode.type === 'COMPONENT' || parentNode.type === 'COMPONENT_SET') {
3673 |       parentNode = parentNode.parent;
3674 |       if (!parentNode) throw new Error("Parent node not found");
3675 |     }
3676 | 
3677 |     // Create the cursor node
3678 |     const importedNode = await figma.createNodeFromSvg(svgString);
3679 |     if (!importedNode || !importedNode.id) {
3680 |       throw new Error("Failed to create imported cursor node");
3681 |     }
3682 |     importedNode.name = "TTF_Connector / Mouse Cursor";
3683 |     importedNode.resize(48, 48);
3684 | 
3685 |     const cursorNode = importedNode.findOne(node => node.type === 'VECTOR');
3686 |     if (cursorNode) {
3687 |       cursorNode.fills = [{
3688 |         type: 'SOLID',
3689 |         color: { r: 0, g: 0, b: 0 },
3690 |         opacity: 1
3691 |       }];
3692 |       cursorNode.strokes = [{
3693 |         type: 'SOLID',
3694 |         color: { r: 1, g: 1, b: 1 },
3695 |         opacity: 1
3696 |       }];
3697 |       cursorNode.strokeWeight = 2;
3698 |       cursorNode.strokeAlign = 'OUTSIDE';
3699 |       cursorNode.effects = [{
3700 |         type: "DROP_SHADOW",
3701 |         color: { r: 0, g: 0, b: 0, a: 0.3 },
3702 |         offset: { x: 1, y: 1 },
3703 |         radius: 2,
3704 |         spread: 0,
3705 |         visible: true,
3706 |         blendMode: "NORMAL"
3707 |       }];
3708 |     }
3709 | 
3710 |     // Append the cursor node to the parent node
3711 |     parentNode.appendChild(importedNode);
3712 | 
3713 |     // if the parentNode has auto-layout enabled, set the layoutPositioning to ABSOLUTE
3714 |     if ('layoutMode' in parentNode && parentNode.layoutMode !== 'NONE') {
3715 |       importedNode.layoutPositioning = 'ABSOLUTE';
3716 |     }
3717 | 
3718 |     // Adjust the importedNode's position to the targetNode's position
3719 |     if (
3720 |       targetNode.absoluteBoundingBox &&
3721 |       parentNode.absoluteBoundingBox
3722 |     ) {
3723 |       // if the targetNode has absoluteBoundingBox, set the importedNode's absoluteBoundingBox to the targetNode's absoluteBoundingBox
3724 |       console.log('targetNode.absoluteBoundingBox', targetNode.absoluteBoundingBox);
3725 |       console.log('parentNode.absoluteBoundingBox', parentNode.absoluteBoundingBox);
3726 |       importedNode.x = targetNode.absoluteBoundingBox.x - parentNode.absoluteBoundingBox.x  + targetNode.absoluteBoundingBox.width / 2 - 48 / 2
3727 |       importedNode.y = targetNode.absoluteBoundingBox.y - parentNode.absoluteBoundingBox.y + targetNode.absoluteBoundingBox.height / 2 - 48 / 2;
3728 |     } else if (
3729 |       'x' in targetNode && 'y' in targetNode && 'width' in targetNode && 'height' in targetNode) {
3730 |         // if the targetNode has x, y, width, height, calculate center based on relative position
3731 |         console.log('targetNode.x/y/width/height', targetNode.x, targetNode.y, targetNode.width, targetNode.height);
3732 |         importedNode.x = targetNode.x + targetNode.width / 2 - 48 / 2;
3733 |         importedNode.y = targetNode.y + targetNode.height / 2 - 48 / 2;
3734 |     } else {
3735 |       // Fallback: Place at top-left of target if possible, otherwise at (0,0) relative to parent
3736 |       if ('x' in targetNode && 'y' in targetNode) {
3737 |         console.log('Fallback to targetNode x/y');
3738 |         importedNode.x = targetNode.x;
3739 |         importedNode.y = targetNode.y;
3740 |       } else {
3741 |         console.log('Fallback to (0,0)');
3742 |         importedNode.x = 0;
3743 |         importedNode.y = 0;
3744 |       }
3745 |     }
3746 | 
3747 |     // get the importedNode ID and the importedNode
3748 |     console.log('importedNode', importedNode);
3749 | 
3750 | 
3751 |     return { id: importedNode.id, node: importedNode };
3752 |     
3753 |   } catch (error) {
3754 |     console.error("Error creating cursor from SVG:", error);
3755 |     return { id: null, node: null, error: error.message };
3756 |   }
3757 | }
3758 | 
3759 | async function createConnections(params) {
3760 |   if (!params || !params.connections || !Array.isArray(params.connections)) {
3761 |     throw new Error('Missing or invalid connections parameter');
3762 |   }
3763 |   
3764 |   const { connections } = params;
3765 |   
3766 |   // Command ID for progress tracking
3767 |   const commandId = generateCommandId();
3768 |   sendProgressUpdate(
3769 |     commandId,
3770 |     "create_connections",
3771 |     "started",
3772 |     0,
3773 |     connections.length,
3774 |     0,
3775 |     `Starting to create ${connections.length} connections`
3776 |   );
3777 |   
3778 |   // Get default connector ID from client storage
3779 |   const defaultConnectorId = await figma.clientStorage.getAsync('defaultConnectorId');
3780 |   if (!defaultConnectorId) {
3781 |     throw new Error('No default connector set. Please try one of the following options to create connections:\n1. Create a connector in FigJam and copy/paste it to your current page, then run the "set_default_connector" command.\n2. Select an existing connector on the current page, then run the "set_default_connector" command.');
3782 |   }
3783 |   
3784 |   // Get the default connector
3785 |   const defaultConnector = await figma.getNodeByIdAsync(defaultConnectorId);
3786 |   if (!defaultConnector) {
3787 |     throw new Error(`Default connector not found with ID: ${defaultConnectorId}`);
3788 |   }
3789 |   if (defaultConnector.type !== 'CONNECTOR') {
3790 |     throw new Error(`Node is not a connector: ${defaultConnectorId}`);
3791 |   }
3792 |   
3793 |   // Results array for connection creation
3794 |   const results = [];
3795 |   let processedCount = 0;
3796 |   const totalCount = connections.length;
3797 |   
3798 |   // Preload fonts (used for text if provided)
3799 |   let fontLoaded = false;
3800 |   
3801 |   for (let i = 0; i < connections.length; i++) {
3802 |     try {
3803 |       const { startNodeId: originalStartId, endNodeId: originalEndId, text } = connections[i];
3804 |       let startId = originalStartId;
3805 |       let endId = originalEndId;
3806 | 
3807 |       // Check and potentially replace start node ID
3808 |       if (startId.includes(';')) {
3809 |         console.log(`Nested start node detected: ${startId}. Creating cursor node.`);
3810 |         const cursorResult = await createCursorNode(startId);
3811 |         if (!cursorResult || !cursorResult.id) {
3812 |           throw new Error(`Failed to create cursor node for nested start node: ${startId}`);
3813 |         }
3814 |         startId = cursorResult.id; 
3815 |       }  
3816 |       
3817 |       const startNode = await figma.getNodeByIdAsync(startId);
3818 |       if (!startNode) throw new Error(`Start node not found with ID: ${startId}`);
3819 | 
3820 |       // Check and potentially replace end node ID
3821 |       if (endId.includes(';')) {
3822 |         console.log(`Nested end node detected: ${endId}. Creating cursor node.`);
3823 |         const cursorResult = await createCursorNode(endId);
3824 |         if (!cursorResult || !cursorResult.id) {
3825 |           throw new Error(`Failed to create cursor node for nested end node: ${endId}`);
3826 |         }
3827 |         endId = cursorResult.id;
3828 |       }
3829 |       const endNode = await figma.getNodeByIdAsync(endId);
3830 |       if (!endNode) throw new Error(`End node not found with ID: ${endId}`);
3831 | 
3832 |       
3833 |       // Clone the default connector
3834 |       const clonedConnector = defaultConnector.clone();
3835 |       
3836 |       // Update connector name using potentially replaced node names
3837 |       clonedConnector.name = `TTF_Connector/${startNode.id}/${endNode.id}`;
3838 |       
3839 |       // Set start and end points using potentially replaced IDs
3840 |       clonedConnector.connectorStart = {
3841 |         endpointNodeId: startId,
3842 |         magnet: 'AUTO'
3843 |       };
3844 |       
3845 |       clonedConnector.connectorEnd = {
3846 |         endpointNodeId: endId,
3847 |         magnet: 'AUTO'
3848 |       };
3849 |       
3850 |       // Add text (if provided)
3851 |       if (text) {
3852 |         try {
3853 |           // Try to load the necessary fonts
3854 |           try {
3855 |             // First check if default connector has font and use the same
3856 |             if (defaultConnector.text && defaultConnector.text.fontName) {
3857 |               const fontName = defaultConnector.text.fontName;
3858 |               await figma.loadFontAsync(fontName);
3859 |               clonedConnector.text.fontName = fontName;
3860 |             } else {
3861 |               // Try default Inter font
3862 |               await figma.loadFontAsync({ family: "Inter", style: "Regular" });
3863 |             }
3864 |           } catch (fontError) {
3865 |             // If first font load fails, try another font style
3866 |             try {
3867 |               await figma.loadFontAsync({ family: "Inter", style: "Medium" });
3868 |             } catch (mediumFontError) {
3869 |               // If second font fails, try system font
3870 |               try {
3871 |                 await figma.loadFontAsync({ family: "System", style: "Regular" });
3872 |               } catch (systemFontError) {
3873 |                 // If all font loading attempts fail, throw error
3874 |                 throw new Error(`Failed to load any font: ${fontError.message}`);
3875 |               }
3876 |             }
3877 |           }
3878 |           
3879 |           // Set the text
3880 |           clonedConnector.text.characters = text;
3881 |         } catch (textError) {
3882 |           console.error("Error setting text:", textError);
3883 |           // Continue with connection even if text setting fails
3884 |           results.push({
3885 |             id: clonedConnector.id,
3886 |             startNodeId: startNodeId,
3887 |             endNodeId: endNodeId,
3888 |             text: "",
3889 |             textError: textError.message
3890 |           });
3891 |           
3892 |           // Continue to next connection
3893 |           continue;
3894 |         }
3895 |       }
3896 |       
3897 |       // Add to results (using the *original* IDs for reference if needed)
3898 |       results.push({
3899 |         id: clonedConnector.id,
3900 |         originalStartNodeId: originalStartId,
3901 |         originalEndNodeId: originalEndId,
3902 |         usedStartNodeId: startId, // ID actually used for connection
3903 |         usedEndNodeId: endId,     // ID actually used for connection
3904 |         text: text || ""
3905 |       });
3906 |       
3907 |       // Update progress
3908 |       processedCount++;
3909 |       sendProgressUpdate(
3910 |         commandId,
3911 |         "create_connections",
3912 |         "in_progress",
3913 |         processedCount / totalCount,
3914 |         totalCount,
3915 |         processedCount,
3916 |         `Created connection ${processedCount}/${totalCount}`
3917 |       );
3918 |       
3919 |     } catch (error) {
3920 |       console.error("Error creating connection", error);
3921 |       // Continue processing remaining connections even if an error occurs
3922 |       processedCount++;
3923 |       sendProgressUpdate(
3924 |         commandId,
3925 |         "create_connections",
3926 |         "in_progress",
3927 |         processedCount / totalCount,
3928 |         totalCount,
3929 |         processedCount,
3930 |         `Error creating connection: ${error.message}`
3931 |       );
3932 |       
3933 |       results.push({
3934 |         error: error.message,
3935 |         connectionInfo: connections[i]
3936 |       });
3937 |     }
3938 |   }
3939 |   
3940 |   // Completion update
3941 |   sendProgressUpdate(
3942 |     commandId,
3943 |     "create_connections",
3944 |     "completed",
3945 |     1,
3946 |     totalCount,
3947 |     totalCount,
3948 |     `Completed creating ${results.length} connections`
3949 |   );
3950 |   
3951 |   return {
3952 |     success: true,
3953 |     count: results.length,
3954 |     connections: results
3955 |   };
3956 | }
3957 | 
3958 | // Set focus on a specific node
3959 | async function setFocus(params) {
3960 |   if (!params || !params.nodeId) {
3961 |     throw new Error("Missing nodeId parameter");
3962 |   }
3963 | 
3964 |   const node = await figma.getNodeByIdAsync(params.nodeId);
3965 |   if (!node) {
3966 |     throw new Error(`Node with ID ${params.nodeId} not found`);
3967 |   }
3968 | 
3969 |   // Set selection to the node
3970 |   figma.currentPage.selection = [node];
3971 |   
3972 |   // Scroll and zoom to show the node in viewport
3973 |   figma.viewport.scrollAndZoomIntoView([node]);
3974 | 
3975 |   return {
3976 |     success: true,
3977 |     name: node.name,
3978 |     id: node.id,
3979 |     message: `Focused on node "${node.name}"`
3980 |   };
3981 | }
3982 | 
3983 | // Set selection to multiple nodes
3984 | async function setSelections(params) {
3985 |   if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) {
3986 |     throw new Error("Missing or invalid nodeIds parameter");
3987 |   }
3988 | 
3989 |   if (params.nodeIds.length === 0) {
3990 |     throw new Error("nodeIds array cannot be empty");
3991 |   }
3992 | 
3993 |   // Get all valid nodes
3994 |   const nodes = [];
3995 |   const notFoundIds = [];
3996 |   
3997 |   for (const nodeId of params.nodeIds) {
3998 |     const node = await figma.getNodeByIdAsync(nodeId);
3999 |     if (node) {
4000 |       nodes.push(node);
4001 |     } else {
4002 |       notFoundIds.push(nodeId);
4003 |     }
4004 |   }
4005 | 
4006 |   if (nodes.length === 0) {
4007 |     throw new Error(`No valid nodes found for the provided IDs: ${params.nodeIds.join(', ')}`);
4008 |   }
4009 | 
4010 |   // Set selection to the nodes
4011 |   figma.currentPage.selection = nodes;
4012 |   
4013 |   // Scroll and zoom to show all nodes in viewport
4014 |   figma.viewport.scrollAndZoomIntoView(nodes);
4015 | 
4016 |   const selectedNodes = nodes.map(node => ({
4017 |     name: node.name,
4018 |     id: node.id
4019 |   }));
4020 | 
4021 |   return {
4022 |     success: true,
4023 |     count: nodes.length,
4024 |     selectedNodes: selectedNodes,
4025 |     notFoundIds: notFoundIds,
4026 |     message: `Selected ${nodes.length} nodes${notFoundIds.length > 0 ? ` (${notFoundIds.length} not found)` : ''}`
4027 |   };
4028 | }
4029 | 
```
Page 3/3FirstPrevNextLast