@autobest-ui/agent 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4423 @@
1
+ /**
2
+ * Mutation tools - tools that modify the Figma document
3
+ */
4
+
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { Buffer } from 'node:buffer';
9
+
10
+ // File extension per export format, for the default output path
11
+ const EXPORT_EXTENSIONS = { PNG: 'png', SVG: 'svg', JPG: 'jpg', PDF: 'pdf' };
12
+
13
+ /**
14
+ * Build the default on-disk destination for an export.
15
+ * os.tmpdir()/figma-mcp-bridge/<sanitized-node-id>-<timestamp>.<ext>
16
+ */
17
+ function defaultExportPath(nodeId, format) {
18
+ const ext = EXPORT_EXTENSIONS[format] || 'bin';
19
+ const safeId = String(nodeId).replace(/[^A-Za-z0-9._-]+/g, '-');
20
+ return path.join(os.tmpdir(), 'figma-mcp-bridge', `${safeId}-${Date.now()}.${ext}`);
21
+ }
22
+
23
+ /**
24
+ * Set fills on a node
25
+ */
26
+ export async function handleSetFills(bridge, args) {
27
+ if (!bridge.isConnected()) {
28
+ return {
29
+ content: [{
30
+ type: 'text',
31
+ text: JSON.stringify({
32
+ error: {
33
+ code: 'NOT_CONNECTED',
34
+ message: 'Figma plugin is not connected.'
35
+ }
36
+ }, null, 2)
37
+ }],
38
+ isError: true
39
+ };
40
+ }
41
+
42
+ const { nodeId, fills } = args;
43
+
44
+ if (!nodeId) {
45
+ return {
46
+ content: [{
47
+ type: 'text',
48
+ text: JSON.stringify({
49
+ error: {
50
+ code: 'INVALID_PARAMS',
51
+ message: 'nodeId is required'
52
+ }
53
+ }, null, 2)
54
+ }],
55
+ isError: true
56
+ };
57
+ }
58
+
59
+ try {
60
+ const result = await bridge.sendCommand('set_fills', { nodeId, fills });
61
+ return {
62
+ content: [{
63
+ type: 'text',
64
+ text: JSON.stringify(result, null, 2)
65
+ }]
66
+ };
67
+ } catch (error) {
68
+ return {
69
+ content: [{
70
+ type: 'text',
71
+ text: JSON.stringify({
72
+ error: {
73
+ code: error.code || 'UNKNOWN_ERROR',
74
+ message: error.message
75
+ }
76
+ }, null, 2)
77
+ }],
78
+ isError: true
79
+ };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Set strokes (colors and/or uniform + per-side weights) on a node
85
+ */
86
+ export async function handleSetStrokes(bridge, args) {
87
+ if (!bridge.isConnected()) {
88
+ return {
89
+ content: [{
90
+ type: 'text',
91
+ text: JSON.stringify({
92
+ error: {
93
+ code: 'NOT_CONNECTED',
94
+ message: 'Figma plugin is not connected.'
95
+ }
96
+ }, null, 2)
97
+ }],
98
+ isError: true
99
+ };
100
+ }
101
+
102
+ const { nodeId } = args;
103
+
104
+ if (!nodeId) {
105
+ return {
106
+ content: [{
107
+ type: 'text',
108
+ text: JSON.stringify({
109
+ error: {
110
+ code: 'INVALID_PARAMS',
111
+ message: 'nodeId is required'
112
+ }
113
+ }, null, 2)
114
+ }],
115
+ isError: true
116
+ };
117
+ }
118
+
119
+ try {
120
+ // Pass args through so strokes, strokeWeight and the four per-side weights all reach the plugin
121
+ const result = await bridge.sendCommand('set_strokes', args);
122
+ return {
123
+ content: [{
124
+ type: 'text',
125
+ text: JSON.stringify(result, null, 2)
126
+ }]
127
+ };
128
+ } catch (error) {
129
+ return {
130
+ content: [{
131
+ type: 'text',
132
+ text: JSON.stringify({
133
+ error: {
134
+ code: error.code || 'UNKNOWN_ERROR',
135
+ message: error.message
136
+ }
137
+ }, null, 2)
138
+ }],
139
+ isError: true
140
+ };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Create a rectangle
146
+ */
147
+ export async function handleCreateRectangle(bridge, args) {
148
+ if (!bridge.isConnected()) {
149
+ return {
150
+ content: [{
151
+ type: 'text',
152
+ text: JSON.stringify({
153
+ error: {
154
+ code: 'NOT_CONNECTED',
155
+ message: 'Figma plugin is not connected.'
156
+ }
157
+ }, null, 2)
158
+ }],
159
+ isError: true
160
+ };
161
+ }
162
+
163
+ try {
164
+ const result = await bridge.sendCommand('create_rectangle', args);
165
+ return {
166
+ content: [{
167
+ type: 'text',
168
+ text: JSON.stringify(result, null, 2)
169
+ }]
170
+ };
171
+ } catch (error) {
172
+ return {
173
+ content: [{
174
+ type: 'text',
175
+ text: JSON.stringify({
176
+ error: {
177
+ code: error.code || 'UNKNOWN_ERROR',
178
+ message: error.message
179
+ }
180
+ }, null, 2)
181
+ }],
182
+ isError: true
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Set text content
189
+ */
190
+ export async function handleSetText(bridge, args) {
191
+ if (!bridge.isConnected()) {
192
+ return {
193
+ content: [{
194
+ type: 'text',
195
+ text: JSON.stringify({
196
+ error: {
197
+ code: 'NOT_CONNECTED',
198
+ message: 'Figma plugin is not connected.'
199
+ }
200
+ }, null, 2)
201
+ }],
202
+ isError: true
203
+ };
204
+ }
205
+
206
+ const { nodeId, text } = args;
207
+
208
+ if (!nodeId || text === undefined) {
209
+ return {
210
+ content: [{
211
+ type: 'text',
212
+ text: JSON.stringify({
213
+ error: {
214
+ code: 'INVALID_PARAMS',
215
+ message: 'nodeId and text are required'
216
+ }
217
+ }, null, 2)
218
+ }],
219
+ isError: true
220
+ };
221
+ }
222
+
223
+ try {
224
+ const result = await bridge.sendCommand('set_text', { nodeId, text });
225
+ return {
226
+ content: [{
227
+ type: 'text',
228
+ text: JSON.stringify(result, null, 2)
229
+ }]
230
+ };
231
+ } catch (error) {
232
+ return {
233
+ content: [{
234
+ type: 'text',
235
+ text: JSON.stringify({
236
+ error: {
237
+ code: error.code || 'UNKNOWN_ERROR',
238
+ message: error.message
239
+ }
240
+ }, null, 2)
241
+ }],
242
+ isError: true
243
+ };
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Clone nodes
249
+ */
250
+ export async function handleCloneNodes(bridge, args) {
251
+ if (!bridge.isConnected()) {
252
+ return {
253
+ content: [{
254
+ type: 'text',
255
+ text: JSON.stringify({
256
+ error: {
257
+ code: 'NOT_CONNECTED',
258
+ message: 'Figma plugin is not connected.'
259
+ }
260
+ }, null, 2)
261
+ }],
262
+ isError: true
263
+ };
264
+ }
265
+
266
+ const { nodeIds, parentId, offset } = args;
267
+
268
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
269
+ return {
270
+ content: [{
271
+ type: 'text',
272
+ text: JSON.stringify({
273
+ error: {
274
+ code: 'INVALID_PARAMS',
275
+ message: 'nodeIds must be a non-empty array'
276
+ }
277
+ }, null, 2)
278
+ }],
279
+ isError: true
280
+ };
281
+ }
282
+
283
+ try {
284
+ const result = await bridge.sendCommand('clone_nodes', { nodeIds, parentId, offset });
285
+ return {
286
+ content: [{
287
+ type: 'text',
288
+ text: JSON.stringify(result, null, 2)
289
+ }]
290
+ };
291
+ } catch (error) {
292
+ return {
293
+ content: [{
294
+ type: 'text',
295
+ text: JSON.stringify({
296
+ error: {
297
+ code: error.code || 'UNKNOWN_ERROR',
298
+ message: error.message
299
+ }
300
+ }, null, 2)
301
+ }],
302
+ isError: true
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Delete nodes
309
+ */
310
+ export async function handleDeleteNodes(bridge, args) {
311
+ if (!bridge.isConnected()) {
312
+ return {
313
+ content: [{
314
+ type: 'text',
315
+ text: JSON.stringify({
316
+ error: {
317
+ code: 'NOT_CONNECTED',
318
+ message: 'Figma plugin is not connected.'
319
+ }
320
+ }, null, 2)
321
+ }],
322
+ isError: true
323
+ };
324
+ }
325
+
326
+ const { nodeIds } = args;
327
+
328
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
329
+ return {
330
+ content: [{
331
+ type: 'text',
332
+ text: JSON.stringify({
333
+ error: {
334
+ code: 'INVALID_PARAMS',
335
+ message: 'nodeIds must be a non-empty array'
336
+ }
337
+ }, null, 2)
338
+ }],
339
+ isError: true
340
+ };
341
+ }
342
+
343
+ try {
344
+ const result = await bridge.sendCommand('delete_nodes', { nodeIds });
345
+ return {
346
+ content: [{
347
+ type: 'text',
348
+ text: JSON.stringify(result, null, 2)
349
+ }]
350
+ };
351
+ } catch (error) {
352
+ return {
353
+ content: [{
354
+ type: 'text',
355
+ text: JSON.stringify({
356
+ error: {
357
+ code: error.code || 'UNKNOWN_ERROR',
358
+ message: error.message
359
+ }
360
+ }, null, 2)
361
+ }],
362
+ isError: true
363
+ };
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Move nodes
369
+ */
370
+ export async function handleMoveNodes(bridge, args) {
371
+ if (!bridge.isConnected()) {
372
+ return {
373
+ content: [{
374
+ type: 'text',
375
+ text: JSON.stringify({
376
+ error: {
377
+ code: 'NOT_CONNECTED',
378
+ message: 'Figma plugin is not connected.'
379
+ }
380
+ }, null, 2)
381
+ }],
382
+ isError: true
383
+ };
384
+ }
385
+
386
+ const { nodeIds, x, y, relative } = args;
387
+
388
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
389
+ return {
390
+ content: [{
391
+ type: 'text',
392
+ text: JSON.stringify({
393
+ error: {
394
+ code: 'INVALID_PARAMS',
395
+ message: 'nodeIds must be a non-empty array'
396
+ }
397
+ }, null, 2)
398
+ }],
399
+ isError: true
400
+ };
401
+ }
402
+
403
+ if (x === undefined && y === undefined) {
404
+ return {
405
+ content: [{
406
+ type: 'text',
407
+ text: JSON.stringify({
408
+ error: {
409
+ code: 'INVALID_PARAMS',
410
+ message: 'At least one of x or y must be provided'
411
+ }
412
+ }, null, 2)
413
+ }],
414
+ isError: true
415
+ };
416
+ }
417
+
418
+ try {
419
+ const result = await bridge.sendCommand('move_nodes', { nodeIds, x, y, relative });
420
+ return {
421
+ content: [{
422
+ type: 'text',
423
+ text: JSON.stringify(result, null, 2)
424
+ }]
425
+ };
426
+ } catch (error) {
427
+ return {
428
+ content: [{
429
+ type: 'text',
430
+ text: JSON.stringify({
431
+ error: {
432
+ code: error.code || 'UNKNOWN_ERROR',
433
+ message: error.message
434
+ }
435
+ }, null, 2)
436
+ }],
437
+ isError: true
438
+ };
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Resize nodes
444
+ */
445
+ export async function handleResizeNodes(bridge, args) {
446
+ if (!bridge.isConnected()) {
447
+ return {
448
+ content: [{
449
+ type: 'text',
450
+ text: JSON.stringify({
451
+ error: {
452
+ code: 'NOT_CONNECTED',
453
+ message: 'Figma plugin is not connected.'
454
+ }
455
+ }, null, 2)
456
+ }],
457
+ isError: true
458
+ };
459
+ }
460
+
461
+ const { nodeIds, width, height } = args;
462
+
463
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
464
+ return {
465
+ content: [{
466
+ type: 'text',
467
+ text: JSON.stringify({
468
+ error: {
469
+ code: 'INVALID_PARAMS',
470
+ message: 'nodeIds must be a non-empty array'
471
+ }
472
+ }, null, 2)
473
+ }],
474
+ isError: true
475
+ };
476
+ }
477
+
478
+ if (width === undefined && height === undefined) {
479
+ return {
480
+ content: [{
481
+ type: 'text',
482
+ text: JSON.stringify({
483
+ error: {
484
+ code: 'INVALID_PARAMS',
485
+ message: 'At least one of width or height must be provided'
486
+ }
487
+ }, null, 2)
488
+ }],
489
+ isError: true
490
+ };
491
+ }
492
+
493
+ try {
494
+ const result = await bridge.sendCommand('resize_nodes', { nodeIds, width, height });
495
+ return {
496
+ content: [{
497
+ type: 'text',
498
+ text: JSON.stringify(result, null, 2)
499
+ }],
500
+ // The plugin verifies width/height by readback. A resize that silently did
501
+ // nothing comes back as success: false with an errors array — surface that
502
+ // as an error instead of letting the caller record work that never happened.
503
+ isError: !!(result && result.success === false)
504
+ };
505
+ } catch (error) {
506
+ return {
507
+ content: [{
508
+ type: 'text',
509
+ text: JSON.stringify({
510
+ error: {
511
+ code: error.code || 'UNKNOWN_ERROR',
512
+ message: error.message
513
+ }
514
+ }, null, 2)
515
+ }],
516
+ isError: true
517
+ };
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Set opacity
523
+ */
524
+ export async function handleSetOpacity(bridge, args) {
525
+ if (!bridge.isConnected()) {
526
+ return {
527
+ content: [{
528
+ type: 'text',
529
+ text: JSON.stringify({
530
+ error: {
531
+ code: 'NOT_CONNECTED',
532
+ message: 'Figma plugin is not connected.'
533
+ }
534
+ }, null, 2)
535
+ }],
536
+ isError: true
537
+ };
538
+ }
539
+
540
+ const { nodeId, opacity } = args;
541
+
542
+ if (!nodeId) {
543
+ return {
544
+ content: [{
545
+ type: 'text',
546
+ text: JSON.stringify({
547
+ error: {
548
+ code: 'INVALID_PARAMS',
549
+ message: 'nodeId is required'
550
+ }
551
+ }, null, 2)
552
+ }],
553
+ isError: true
554
+ };
555
+ }
556
+
557
+ if (opacity === undefined || opacity < 0 || opacity > 1) {
558
+ return {
559
+ content: [{
560
+ type: 'text',
561
+ text: JSON.stringify({
562
+ error: {
563
+ code: 'INVALID_PARAMS',
564
+ message: 'opacity must be a number between 0 and 1'
565
+ }
566
+ }, null, 2)
567
+ }],
568
+ isError: true
569
+ };
570
+ }
571
+
572
+ try {
573
+ const result = await bridge.sendCommand('set_opacity', { nodeId, opacity });
574
+ return {
575
+ content: [{
576
+ type: 'text',
577
+ text: JSON.stringify(result, null, 2)
578
+ }]
579
+ };
580
+ } catch (error) {
581
+ return {
582
+ content: [{
583
+ type: 'text',
584
+ text: JSON.stringify({
585
+ error: {
586
+ code: error.code || 'UNKNOWN_ERROR',
587
+ message: error.message
588
+ }
589
+ }, null, 2)
590
+ }],
591
+ isError: true
592
+ };
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Set corner radius
598
+ */
599
+ export async function handleSetCornerRadius(bridge, args) {
600
+ if (!bridge.isConnected()) {
601
+ return {
602
+ content: [{
603
+ type: 'text',
604
+ text: JSON.stringify({
605
+ error: {
606
+ code: 'NOT_CONNECTED',
607
+ message: 'Figma plugin is not connected.'
608
+ }
609
+ }, null, 2)
610
+ }],
611
+ isError: true
612
+ };
613
+ }
614
+
615
+ const { nodeId, radius, topLeft, topRight, bottomLeft, bottomRight } = args;
616
+
617
+ if (!nodeId) {
618
+ return {
619
+ content: [{
620
+ type: 'text',
621
+ text: JSON.stringify({
622
+ error: {
623
+ code: 'INVALID_PARAMS',
624
+ message: 'nodeId is required'
625
+ }
626
+ }, null, 2)
627
+ }],
628
+ isError: true
629
+ };
630
+ }
631
+
632
+ if (radius === undefined && topLeft === undefined && topRight === undefined && bottomLeft === undefined && bottomRight === undefined) {
633
+ return {
634
+ content: [{
635
+ type: 'text',
636
+ text: JSON.stringify({
637
+ error: {
638
+ code: 'INVALID_PARAMS',
639
+ message: 'At least one radius value must be provided'
640
+ }
641
+ }, null, 2)
642
+ }],
643
+ isError: true
644
+ };
645
+ }
646
+
647
+ try {
648
+ const result = await bridge.sendCommand('set_corner_radius', { nodeId, radius, topLeft, topRight, bottomLeft, bottomRight });
649
+ return {
650
+ content: [{
651
+ type: 'text',
652
+ text: JSON.stringify(result, null, 2)
653
+ }]
654
+ };
655
+ } catch (error) {
656
+ return {
657
+ content: [{
658
+ type: 'text',
659
+ text: JSON.stringify({
660
+ error: {
661
+ code: error.code || 'UNKNOWN_ERROR',
662
+ message: error.message
663
+ }
664
+ }, null, 2)
665
+ }],
666
+ isError: true
667
+ };
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Group nodes
673
+ */
674
+ export async function handleGroupNodes(bridge, args) {
675
+ if (!bridge.isConnected()) {
676
+ return {
677
+ content: [{
678
+ type: 'text',
679
+ text: JSON.stringify({
680
+ error: {
681
+ code: 'NOT_CONNECTED',
682
+ message: 'Figma plugin is not connected.'
683
+ }
684
+ }, null, 2)
685
+ }],
686
+ isError: true
687
+ };
688
+ }
689
+
690
+ const { nodeIds, name } = args;
691
+
692
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
693
+ return {
694
+ content: [{
695
+ type: 'text',
696
+ text: JSON.stringify({
697
+ error: {
698
+ code: 'INVALID_PARAMS',
699
+ message: 'nodeIds must be a non-empty array'
700
+ }
701
+ }, null, 2)
702
+ }],
703
+ isError: true
704
+ };
705
+ }
706
+
707
+ try {
708
+ const result = await bridge.sendCommand('group_nodes', { nodeIds, name });
709
+ return {
710
+ content: [{
711
+ type: 'text',
712
+ text: JSON.stringify(result, null, 2)
713
+ }]
714
+ };
715
+ } catch (error) {
716
+ return {
717
+ content: [{
718
+ type: 'text',
719
+ text: JSON.stringify({
720
+ error: {
721
+ code: error.code || 'UNKNOWN_ERROR',
722
+ message: error.message
723
+ }
724
+ }, null, 2)
725
+ }],
726
+ isError: true
727
+ };
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Ungroup nodes
733
+ */
734
+ export async function handleUngroupNodes(bridge, args) {
735
+ if (!bridge.isConnected()) {
736
+ return {
737
+ content: [{
738
+ type: 'text',
739
+ text: JSON.stringify({
740
+ error: {
741
+ code: 'NOT_CONNECTED',
742
+ message: 'Figma plugin is not connected.'
743
+ }
744
+ }, null, 2)
745
+ }],
746
+ isError: true
747
+ };
748
+ }
749
+
750
+ const { nodeIds } = args;
751
+
752
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
753
+ return {
754
+ content: [{
755
+ type: 'text',
756
+ text: JSON.stringify({
757
+ error: {
758
+ code: 'INVALID_PARAMS',
759
+ message: 'nodeIds must be a non-empty array'
760
+ }
761
+ }, null, 2)
762
+ }],
763
+ isError: true
764
+ };
765
+ }
766
+
767
+ try {
768
+ const result = await bridge.sendCommand('ungroup_nodes', { nodeIds });
769
+ return {
770
+ content: [{
771
+ type: 'text',
772
+ text: JSON.stringify(result, null, 2)
773
+ }]
774
+ };
775
+ } catch (error) {
776
+ return {
777
+ content: [{
778
+ type: 'text',
779
+ text: JSON.stringify({
780
+ error: {
781
+ code: error.code || 'UNKNOWN_ERROR',
782
+ message: error.message
783
+ }
784
+ }, null, 2)
785
+ }],
786
+ isError: true
787
+ };
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Create a frame
793
+ */
794
+ export async function handleCreateFrame(bridge, args) {
795
+ if (!bridge.isConnected()) {
796
+ return {
797
+ content: [{
798
+ type: 'text',
799
+ text: JSON.stringify({
800
+ error: {
801
+ code: 'NOT_CONNECTED',
802
+ message: 'Figma plugin is not connected.'
803
+ }
804
+ }, null, 2)
805
+ }],
806
+ isError: true
807
+ };
808
+ }
809
+
810
+ try {
811
+ const result = await bridge.sendCommand('create_frame', args);
812
+ return {
813
+ content: [{
814
+ type: 'text',
815
+ text: JSON.stringify(result, null, 2)
816
+ }]
817
+ };
818
+ } catch (error) {
819
+ return {
820
+ content: [{
821
+ type: 'text',
822
+ text: JSON.stringify({
823
+ error: {
824
+ code: error.code || 'UNKNOWN_ERROR',
825
+ message: error.message
826
+ }
827
+ }, null, 2)
828
+ }],
829
+ isError: true
830
+ };
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Create a text node
836
+ */
837
+ export async function handleCreateText(bridge, args) {
838
+ if (!bridge.isConnected()) {
839
+ return {
840
+ content: [{
841
+ type: 'text',
842
+ text: JSON.stringify({
843
+ error: {
844
+ code: 'NOT_CONNECTED',
845
+ message: 'Figma plugin is not connected.'
846
+ }
847
+ }, null, 2)
848
+ }],
849
+ isError: true
850
+ };
851
+ }
852
+
853
+ try {
854
+ const result = await bridge.sendCommand('create_text', args);
855
+ return {
856
+ content: [{
857
+ type: 'text',
858
+ text: JSON.stringify(result, null, 2)
859
+ }]
860
+ };
861
+ } catch (error) {
862
+ return {
863
+ content: [{
864
+ type: 'text',
865
+ text: JSON.stringify({
866
+ error: {
867
+ code: error.code || 'UNKNOWN_ERROR',
868
+ message: error.message
869
+ }
870
+ }, null, 2)
871
+ }],
872
+ isError: true
873
+ };
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Set selection
879
+ */
880
+ export async function handleSetSelection(bridge, args) {
881
+ if (!bridge.isConnected()) {
882
+ return {
883
+ content: [{
884
+ type: 'text',
885
+ text: JSON.stringify({
886
+ error: {
887
+ code: 'NOT_CONNECTED',
888
+ message: 'Figma plugin is not connected.'
889
+ }
890
+ }, null, 2)
891
+ }],
892
+ isError: true
893
+ };
894
+ }
895
+
896
+ const { nodeIds } = args;
897
+
898
+ if (!nodeIds || !Array.isArray(nodeIds)) {
899
+ return {
900
+ content: [{
901
+ type: 'text',
902
+ text: JSON.stringify({
903
+ error: {
904
+ code: 'INVALID_PARAMS',
905
+ message: 'nodeIds must be an array'
906
+ }
907
+ }, null, 2)
908
+ }],
909
+ isError: true
910
+ };
911
+ }
912
+
913
+ try {
914
+ const result = await bridge.sendCommand('set_selection', { nodeIds });
915
+ return {
916
+ content: [{
917
+ type: 'text',
918
+ text: JSON.stringify(result, null, 2)
919
+ }]
920
+ };
921
+ } catch (error) {
922
+ return {
923
+ content: [{
924
+ type: 'text',
925
+ text: JSON.stringify({
926
+ error: {
927
+ code: error.code || 'UNKNOWN_ERROR',
928
+ message: error.message
929
+ }
930
+ }, null, 2)
931
+ }],
932
+ isError: true
933
+ };
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Set current page
939
+ */
940
+ export async function handleSetCurrentPage(bridge, args) {
941
+ if (!bridge.isConnected()) {
942
+ return {
943
+ content: [{
944
+ type: 'text',
945
+ text: JSON.stringify({
946
+ error: {
947
+ code: 'NOT_CONNECTED',
948
+ message: 'Figma plugin is not connected.'
949
+ }
950
+ }, null, 2)
951
+ }],
952
+ isError: true
953
+ };
954
+ }
955
+
956
+ const { pageId } = args;
957
+
958
+ if (!pageId) {
959
+ return {
960
+ content: [{
961
+ type: 'text',
962
+ text: JSON.stringify({
963
+ error: {
964
+ code: 'INVALID_PARAMS',
965
+ message: 'pageId is required'
966
+ }
967
+ }, null, 2)
968
+ }],
969
+ isError: true
970
+ };
971
+ }
972
+
973
+ try {
974
+ const result = await bridge.sendCommand('set_current_page', { pageId });
975
+ return {
976
+ content: [{
977
+ type: 'text',
978
+ text: JSON.stringify(result, null, 2)
979
+ }]
980
+ };
981
+ } catch (error) {
982
+ return {
983
+ content: [{
984
+ type: 'text',
985
+ text: JSON.stringify({
986
+ error: {
987
+ code: error.code || 'UNKNOWN_ERROR',
988
+ message: error.message
989
+ }
990
+ }, null, 2)
991
+ }],
992
+ isError: true
993
+ };
994
+ }
995
+ }
996
+
997
+ /**
998
+ * Export node as image
999
+ */
1000
+ export async function handleExportNode(bridge, args) {
1001
+ if (!bridge.isConnected()) {
1002
+ return {
1003
+ content: [{
1004
+ type: 'text',
1005
+ text: JSON.stringify({
1006
+ error: {
1007
+ code: 'NOT_CONNECTED',
1008
+ message: 'Figma plugin is not connected.'
1009
+ }
1010
+ }, null, 2)
1011
+ }],
1012
+ isError: true
1013
+ };
1014
+ }
1015
+
1016
+ const { nodeId, format, scale, outputPath, returnBase64 } = args;
1017
+
1018
+ if (!nodeId) {
1019
+ return {
1020
+ content: [{
1021
+ type: 'text',
1022
+ text: JSON.stringify({
1023
+ error: {
1024
+ code: 'INVALID_PARAMS',
1025
+ message: 'nodeId is required'
1026
+ }
1027
+ }, null, 2)
1028
+ }],
1029
+ isError: true
1030
+ };
1031
+ }
1032
+
1033
+ if (outputPath !== undefined && outputPath !== null && !path.isAbsolute(outputPath)) {
1034
+ return {
1035
+ content: [{
1036
+ type: 'text',
1037
+ text: JSON.stringify({
1038
+ error: {
1039
+ code: 'INVALID_PARAMS',
1040
+ message: `outputPath must be an absolute file path. Got: ${outputPath}`
1041
+ }
1042
+ }, null, 2)
1043
+ }],
1044
+ isError: true
1045
+ };
1046
+ }
1047
+
1048
+ try {
1049
+ const result = await bridge.sendCommand('export_node', { nodeId, format, scale });
1050
+
1051
+ // Escape hatch: callers that genuinely want the bytes inline.
1052
+ if (returnBase64) {
1053
+ return {
1054
+ content: [{
1055
+ type: 'text',
1056
+ text: JSON.stringify(result, null, 2)
1057
+ }]
1058
+ };
1059
+ }
1060
+
1061
+ if (!result || typeof result.data !== 'string' || result.data.length === 0) {
1062
+ return {
1063
+ content: [{
1064
+ type: 'text',
1065
+ text: JSON.stringify({
1066
+ error: {
1067
+ code: 'EXPORT_NO_DATA',
1068
+ message: `Figma returned no image data for node ${nodeId}. Nothing was written to disk.`
1069
+ }
1070
+ }, null, 2)
1071
+ }],
1072
+ isError: true
1073
+ };
1074
+ }
1075
+
1076
+ // File-first: decode and write, return the path. Images cannot be viewed
1077
+ // inline as base64, so a path the caller can Read is the useful result.
1078
+ const resolvedFormat = String(format || result.format || 'PNG').toUpperCase();
1079
+ const targetPath = outputPath || defaultExportPath(result.nodeId || nodeId, resolvedFormat);
1080
+ const buffer = Buffer.from(result.data, 'base64');
1081
+
1082
+ try {
1083
+ const dir = path.dirname(targetPath);
1084
+ fs.mkdirSync(dir, { recursive: true });
1085
+ fs.writeFileSync(targetPath, buffer);
1086
+ } catch (writeError) {
1087
+ return {
1088
+ content: [{
1089
+ type: 'text',
1090
+ text: JSON.stringify({
1091
+ error: {
1092
+ code: 'EXPORT_WRITE_FAILED',
1093
+ message: `Exported node ${nodeId} but could not write the image to ${targetPath}: ${writeError.message}. ` +
1094
+ 'Pass a writable absolute outputPath, or returnBase64: true to get the data inline instead.',
1095
+ path: targetPath
1096
+ }
1097
+ }, null, 2)
1098
+ }],
1099
+ isError: true
1100
+ };
1101
+ }
1102
+
1103
+ return {
1104
+ content: [{
1105
+ type: 'text',
1106
+ text: JSON.stringify({
1107
+ success: true,
1108
+ nodeId: result.nodeId || nodeId,
1109
+ path: targetPath,
1110
+ format: resolvedFormat,
1111
+ scale: result.scale !== undefined ? result.scale : scale,
1112
+ bytes: buffer.length,
1113
+ message: 'Image written to disk. Read the file at `path` to view it.'
1114
+ }, null, 2)
1115
+ }]
1116
+ };
1117
+ } catch (error) {
1118
+ return {
1119
+ content: [{
1120
+ type: 'text',
1121
+ text: JSON.stringify({
1122
+ error: {
1123
+ code: error.code || 'UNKNOWN_ERROR',
1124
+ message: error.message
1125
+ }
1126
+ }, null, 2)
1127
+ }],
1128
+ isError: true
1129
+ };
1130
+ }
1131
+ }
1132
+
1133
+ // ============================================================
1134
+ // New Commands (Phase 2)
1135
+ // ============================================================
1136
+
1137
+ /**
1138
+ * Create an ellipse
1139
+ */
1140
+ export async function handleCreateEllipse(bridge, args) {
1141
+ if (!bridge.isConnected()) {
1142
+ return {
1143
+ content: [{
1144
+ type: 'text',
1145
+ text: JSON.stringify({
1146
+ error: {
1147
+ code: 'NOT_CONNECTED',
1148
+ message: 'Figma plugin is not connected.'
1149
+ }
1150
+ }, null, 2)
1151
+ }],
1152
+ isError: true
1153
+ };
1154
+ }
1155
+
1156
+ try {
1157
+ const result = await bridge.sendCommand('create_ellipse', args);
1158
+ return {
1159
+ content: [{
1160
+ type: 'text',
1161
+ text: JSON.stringify(result, null, 2)
1162
+ }]
1163
+ };
1164
+ } catch (error) {
1165
+ return {
1166
+ content: [{
1167
+ type: 'text',
1168
+ text: JSON.stringify({
1169
+ error: {
1170
+ code: error.code || 'UNKNOWN_ERROR',
1171
+ message: error.message
1172
+ }
1173
+ }, null, 2)
1174
+ }],
1175
+ isError: true
1176
+ };
1177
+ }
1178
+ }
1179
+
1180
+ /**
1181
+ * Set effects on a node
1182
+ */
1183
+ export async function handleSetEffects(bridge, args) {
1184
+ if (!bridge.isConnected()) {
1185
+ return {
1186
+ content: [{
1187
+ type: 'text',
1188
+ text: JSON.stringify({
1189
+ error: {
1190
+ code: 'NOT_CONNECTED',
1191
+ message: 'Figma plugin is not connected.'
1192
+ }
1193
+ }, null, 2)
1194
+ }],
1195
+ isError: true
1196
+ };
1197
+ }
1198
+
1199
+ const { nodeId, effects } = args;
1200
+
1201
+ if (!nodeId) {
1202
+ return {
1203
+ content: [{
1204
+ type: 'text',
1205
+ text: JSON.stringify({
1206
+ error: {
1207
+ code: 'INVALID_PARAMS',
1208
+ message: 'nodeId is required'
1209
+ }
1210
+ }, null, 2)
1211
+ }],
1212
+ isError: true
1213
+ };
1214
+ }
1215
+
1216
+ if (!effects || !Array.isArray(effects)) {
1217
+ return {
1218
+ content: [{
1219
+ type: 'text',
1220
+ text: JSON.stringify({
1221
+ error: {
1222
+ code: 'INVALID_PARAMS',
1223
+ message: 'effects must be an array'
1224
+ }
1225
+ }, null, 2)
1226
+ }],
1227
+ isError: true
1228
+ };
1229
+ }
1230
+
1231
+ try {
1232
+ const result = await bridge.sendCommand('set_effects', { nodeId, effects });
1233
+ return {
1234
+ content: [{
1235
+ type: 'text',
1236
+ text: JSON.stringify(result, null, 2)
1237
+ }]
1238
+ };
1239
+ } catch (error) {
1240
+ return {
1241
+ content: [{
1242
+ type: 'text',
1243
+ text: JSON.stringify({
1244
+ error: {
1245
+ code: error.code || 'UNKNOWN_ERROR',
1246
+ message: error.message
1247
+ }
1248
+ }, null, 2)
1249
+ }],
1250
+ isError: true
1251
+ };
1252
+ }
1253
+ }
1254
+
1255
+ /**
1256
+ * Set auto-layout on a frame
1257
+ */
1258
+ export async function handleSetAutoLayout(bridge, args) {
1259
+ if (!bridge.isConnected()) {
1260
+ return {
1261
+ content: [{
1262
+ type: 'text',
1263
+ text: JSON.stringify({
1264
+ error: {
1265
+ code: 'NOT_CONNECTED',
1266
+ message: 'Figma plugin is not connected.'
1267
+ }
1268
+ }, null, 2)
1269
+ }],
1270
+ isError: true
1271
+ };
1272
+ }
1273
+
1274
+ const { nodeId } = args;
1275
+
1276
+ if (!nodeId) {
1277
+ return {
1278
+ content: [{
1279
+ type: 'text',
1280
+ text: JSON.stringify({
1281
+ error: {
1282
+ code: 'INVALID_PARAMS',
1283
+ message: 'nodeId is required'
1284
+ }
1285
+ }, null, 2)
1286
+ }],
1287
+ isError: true
1288
+ };
1289
+ }
1290
+
1291
+ try {
1292
+ const result = await bridge.sendCommand('set_auto_layout', args);
1293
+ return {
1294
+ content: [{
1295
+ type: 'text',
1296
+ text: JSON.stringify(result, null, 2)
1297
+ }]
1298
+ };
1299
+ } catch (error) {
1300
+ return {
1301
+ content: [{
1302
+ type: 'text',
1303
+ text: JSON.stringify({
1304
+ error: {
1305
+ code: error.code || 'UNKNOWN_ERROR',
1306
+ message: error.message
1307
+ }
1308
+ }, null, 2)
1309
+ }],
1310
+ isError: true
1311
+ };
1312
+ }
1313
+ }
1314
+
1315
+ /**
1316
+ * Get local styles
1317
+ */
1318
+ export async function handleGetLocalStyles(bridge, args) {
1319
+ if (!bridge.isConnected()) {
1320
+ return {
1321
+ content: [{
1322
+ type: 'text',
1323
+ text: JSON.stringify({
1324
+ error: {
1325
+ code: 'NOT_CONNECTED',
1326
+ message: 'Figma plugin is not connected.'
1327
+ }
1328
+ }, null, 2)
1329
+ }],
1330
+ isError: true
1331
+ };
1332
+ }
1333
+
1334
+ try {
1335
+ const result = await bridge.sendCommand('get_local_styles', args);
1336
+ return {
1337
+ content: [{
1338
+ type: 'text',
1339
+ text: JSON.stringify(result, null, 2)
1340
+ }]
1341
+ };
1342
+ } catch (error) {
1343
+ return {
1344
+ content: [{
1345
+ type: 'text',
1346
+ text: JSON.stringify({
1347
+ error: {
1348
+ code: error.code || 'UNKNOWN_ERROR',
1349
+ message: error.message
1350
+ }
1351
+ }, null, 2)
1352
+ }],
1353
+ isError: true
1354
+ };
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Apply a style to a node
1360
+ */
1361
+ export async function handleApplyStyle(bridge, args) {
1362
+ if (!bridge.isConnected()) {
1363
+ return {
1364
+ content: [{
1365
+ type: 'text',
1366
+ text: JSON.stringify({
1367
+ error: {
1368
+ code: 'NOT_CONNECTED',
1369
+ message: 'Figma plugin is not connected.'
1370
+ }
1371
+ }, null, 2)
1372
+ }],
1373
+ isError: true
1374
+ };
1375
+ }
1376
+
1377
+ const { nodeId, styleId, property } = args;
1378
+
1379
+ if (!nodeId || !styleId || !property) {
1380
+ return {
1381
+ content: [{
1382
+ type: 'text',
1383
+ text: JSON.stringify({
1384
+ error: {
1385
+ code: 'INVALID_PARAMS',
1386
+ message: 'nodeId, styleId, and property are required'
1387
+ }
1388
+ }, null, 2)
1389
+ }],
1390
+ isError: true
1391
+ };
1392
+ }
1393
+
1394
+ try {
1395
+ const result = await bridge.sendCommand('apply_style', { nodeId, styleId, property });
1396
+ return {
1397
+ content: [{
1398
+ type: 'text',
1399
+ text: JSON.stringify(result, null, 2)
1400
+ }]
1401
+ };
1402
+ } catch (error) {
1403
+ return {
1404
+ content: [{
1405
+ type: 'text',
1406
+ text: JSON.stringify({
1407
+ error: {
1408
+ code: error.code || 'UNKNOWN_ERROR',
1409
+ message: error.message
1410
+ }
1411
+ }, null, 2)
1412
+ }],
1413
+ isError: true
1414
+ };
1415
+ }
1416
+ }
1417
+
1418
+ /**
1419
+ * Create a component
1420
+ */
1421
+ export async function handleCreateComponent(bridge, args) {
1422
+ if (!bridge.isConnected()) {
1423
+ return {
1424
+ content: [{
1425
+ type: 'text',
1426
+ text: JSON.stringify({
1427
+ error: {
1428
+ code: 'NOT_CONNECTED',
1429
+ message: 'Figma plugin is not connected.'
1430
+ }
1431
+ }, null, 2)
1432
+ }],
1433
+ isError: true
1434
+ };
1435
+ }
1436
+
1437
+ try {
1438
+ const result = await bridge.sendCommand('create_component', args);
1439
+ return {
1440
+ content: [{
1441
+ type: 'text',
1442
+ text: JSON.stringify(result, null, 2)
1443
+ }]
1444
+ };
1445
+ } catch (error) {
1446
+ return {
1447
+ content: [{
1448
+ type: 'text',
1449
+ text: JSON.stringify({
1450
+ error: {
1451
+ code: error.code || 'UNKNOWN_ERROR',
1452
+ message: error.message
1453
+ }
1454
+ }, null, 2)
1455
+ }],
1456
+ isError: true
1457
+ };
1458
+ }
1459
+ }
1460
+
1461
+ /**
1462
+ * Create an instance of a component
1463
+ */
1464
+ export async function handleCreateInstance(bridge, args) {
1465
+ if (!bridge.isConnected()) {
1466
+ return {
1467
+ content: [{
1468
+ type: 'text',
1469
+ text: JSON.stringify({
1470
+ error: {
1471
+ code: 'NOT_CONNECTED',
1472
+ message: 'Figma plugin is not connected.'
1473
+ }
1474
+ }, null, 2)
1475
+ }],
1476
+ isError: true
1477
+ };
1478
+ }
1479
+
1480
+ const { componentId } = args;
1481
+
1482
+ if (!componentId) {
1483
+ return {
1484
+ content: [{
1485
+ type: 'text',
1486
+ text: JSON.stringify({
1487
+ error: {
1488
+ code: 'INVALID_PARAMS',
1489
+ message: 'componentId is required'
1490
+ }
1491
+ }, null, 2)
1492
+ }],
1493
+ isError: true
1494
+ };
1495
+ }
1496
+
1497
+ try {
1498
+ const result = await bridge.sendCommand('create_instance', args);
1499
+ return {
1500
+ content: [{
1501
+ type: 'text',
1502
+ text: JSON.stringify(result, null, 2)
1503
+ }]
1504
+ };
1505
+ } catch (error) {
1506
+ return {
1507
+ content: [{
1508
+ type: 'text',
1509
+ text: JSON.stringify({
1510
+ error: {
1511
+ code: error.code || 'UNKNOWN_ERROR',
1512
+ message: error.message
1513
+ }
1514
+ }, null, 2)
1515
+ }],
1516
+ isError: true
1517
+ };
1518
+ }
1519
+ }
1520
+
1521
+ // ============================================================
1522
+ // Phase 3 Commands: Variables, Lines, Constraints
1523
+ // ============================================================
1524
+
1525
+ /**
1526
+ * Get local variables and variable collections
1527
+ */
1528
+ export async function handleGetLocalVariables(bridge, args) {
1529
+ if (!bridge.isConnected()) {
1530
+ return {
1531
+ content: [{
1532
+ type: 'text',
1533
+ text: JSON.stringify({
1534
+ error: {
1535
+ code: 'NOT_CONNECTED',
1536
+ message: 'Figma plugin is not connected.'
1537
+ }
1538
+ }, null, 2)
1539
+ }],
1540
+ isError: true
1541
+ };
1542
+ }
1543
+
1544
+ try {
1545
+ const result = await bridge.sendCommand('get_local_variables', args);
1546
+ return {
1547
+ content: [{
1548
+ type: 'text',
1549
+ text: JSON.stringify(result, null, 2)
1550
+ }]
1551
+ };
1552
+ } catch (error) {
1553
+ return {
1554
+ content: [{
1555
+ type: 'text',
1556
+ text: JSON.stringify({
1557
+ error: {
1558
+ code: error.code || 'UNKNOWN_ERROR',
1559
+ message: error.message
1560
+ }
1561
+ }, null, 2)
1562
+ }],
1563
+ isError: true
1564
+ };
1565
+ }
1566
+ }
1567
+
1568
+ /**
1569
+ * Search variables with filtering (optimized for reduced token usage)
1570
+ */
1571
+ export async function handleSearchVariables(bridge, args) {
1572
+ if (!bridge.isConnected()) {
1573
+ return {
1574
+ content: [{
1575
+ type: 'text',
1576
+ text: JSON.stringify({
1577
+ error: {
1578
+ code: 'NOT_CONNECTED',
1579
+ message: 'Figma plugin is not connected.'
1580
+ }
1581
+ }, null, 2)
1582
+ }],
1583
+ isError: true
1584
+ };
1585
+ }
1586
+
1587
+ try {
1588
+ const result = await bridge.sendCommand('search_variables', args);
1589
+ return {
1590
+ content: [{
1591
+ type: 'text',
1592
+ text: JSON.stringify(result, null, 2)
1593
+ }]
1594
+ };
1595
+ } catch (error) {
1596
+ return {
1597
+ content: [{
1598
+ type: 'text',
1599
+ text: JSON.stringify({
1600
+ error: {
1601
+ code: error.code || 'UNKNOWN_ERROR',
1602
+ message: error.message
1603
+ }
1604
+ }, null, 2)
1605
+ }],
1606
+ isError: true
1607
+ };
1608
+ }
1609
+ }
1610
+
1611
+ /**
1612
+ * Set variable value, or bind variable to a node or style property
1613
+ */
1614
+ export async function handleSetVariable(bridge, args) {
1615
+ if (!bridge.isConnected()) {
1616
+ return {
1617
+ content: [{
1618
+ type: 'text',
1619
+ text: JSON.stringify({
1620
+ error: {
1621
+ code: 'NOT_CONNECTED',
1622
+ message: 'Figma plugin is not connected.'
1623
+ }
1624
+ }, null, 2)
1625
+ }],
1626
+ isError: true
1627
+ };
1628
+ }
1629
+
1630
+ const { variableId, modeId, value, nodeId, styleId, field } = args;
1631
+
1632
+ if (!variableId) {
1633
+ return {
1634
+ content: [{
1635
+ type: 'text',
1636
+ text: JSON.stringify({
1637
+ error: {
1638
+ code: 'INVALID_PARAMS',
1639
+ message: 'variableId is required'
1640
+ }
1641
+ }, null, 2)
1642
+ }],
1643
+ isError: true
1644
+ };
1645
+ }
1646
+
1647
+ // Validate: either (modeId + value) for setting, or (nodeId + field) for binding
1648
+ if (value !== undefined && !modeId) {
1649
+ return {
1650
+ content: [{
1651
+ type: 'text',
1652
+ text: JSON.stringify({
1653
+ error: {
1654
+ code: 'INVALID_PARAMS',
1655
+ message: 'modeId is required when setting a value'
1656
+ }
1657
+ }, null, 2)
1658
+ }],
1659
+ isError: true
1660
+ };
1661
+ }
1662
+
1663
+ if ((nodeId || styleId) && !field) {
1664
+ return {
1665
+ content: [{
1666
+ type: 'text',
1667
+ text: JSON.stringify({
1668
+ error: {
1669
+ code: 'INVALID_PARAMS',
1670
+ message: 'field is required when binding to a node or style'
1671
+ }
1672
+ }, null, 2)
1673
+ }],
1674
+ isError: true
1675
+ };
1676
+ }
1677
+
1678
+ try {
1679
+ const result = await bridge.sendCommand('set_variable', args);
1680
+ return {
1681
+ content: [{
1682
+ type: 'text',
1683
+ text: JSON.stringify(result, null, 2)
1684
+ }]
1685
+ };
1686
+ } catch (error) {
1687
+ return {
1688
+ content: [{
1689
+ type: 'text',
1690
+ text: JSON.stringify({
1691
+ error: {
1692
+ code: error.code || 'UNKNOWN_ERROR',
1693
+ message: error.message
1694
+ }
1695
+ }, null, 2)
1696
+ }],
1697
+ isError: true
1698
+ };
1699
+ }
1700
+ }
1701
+
1702
+ /**
1703
+ * Create a line
1704
+ */
1705
+ export async function handleCreateLine(bridge, args) {
1706
+ if (!bridge.isConnected()) {
1707
+ return {
1708
+ content: [{
1709
+ type: 'text',
1710
+ text: JSON.stringify({
1711
+ error: {
1712
+ code: 'NOT_CONNECTED',
1713
+ message: 'Figma plugin is not connected.'
1714
+ }
1715
+ }, null, 2)
1716
+ }],
1717
+ isError: true
1718
+ };
1719
+ }
1720
+
1721
+ try {
1722
+ const result = await bridge.sendCommand('create_line', args);
1723
+ return {
1724
+ content: [{
1725
+ type: 'text',
1726
+ text: JSON.stringify(result, null, 2)
1727
+ }]
1728
+ };
1729
+ } catch (error) {
1730
+ return {
1731
+ content: [{
1732
+ type: 'text',
1733
+ text: JSON.stringify({
1734
+ error: {
1735
+ code: error.code || 'UNKNOWN_ERROR',
1736
+ message: error.message
1737
+ }
1738
+ }, null, 2)
1739
+ }],
1740
+ isError: true
1741
+ };
1742
+ }
1743
+ }
1744
+
1745
+ /**
1746
+ * Set constraints on a node
1747
+ */
1748
+ export async function handleSetConstraints(bridge, args) {
1749
+ if (!bridge.isConnected()) {
1750
+ return {
1751
+ content: [{
1752
+ type: 'text',
1753
+ text: JSON.stringify({
1754
+ error: {
1755
+ code: 'NOT_CONNECTED',
1756
+ message: 'Figma plugin is not connected.'
1757
+ }
1758
+ }, null, 2)
1759
+ }],
1760
+ isError: true
1761
+ };
1762
+ }
1763
+
1764
+ const { nodeId, horizontal, vertical } = args;
1765
+
1766
+ if (!nodeId) {
1767
+ return {
1768
+ content: [{
1769
+ type: 'text',
1770
+ text: JSON.stringify({
1771
+ error: {
1772
+ code: 'INVALID_PARAMS',
1773
+ message: 'nodeId is required'
1774
+ }
1775
+ }, null, 2)
1776
+ }],
1777
+ isError: true
1778
+ };
1779
+ }
1780
+
1781
+ if (horizontal === undefined && vertical === undefined) {
1782
+ return {
1783
+ content: [{
1784
+ type: 'text',
1785
+ text: JSON.stringify({
1786
+ error: {
1787
+ code: 'INVALID_PARAMS',
1788
+ message: 'At least one of horizontal or vertical must be provided'
1789
+ }
1790
+ }, null, 2)
1791
+ }],
1792
+ isError: true
1793
+ };
1794
+ }
1795
+
1796
+ try {
1797
+ const result = await bridge.sendCommand('set_constraints', { nodeId, horizontal, vertical });
1798
+ return {
1799
+ content: [{
1800
+ type: 'text',
1801
+ text: JSON.stringify(result, null, 2)
1802
+ }]
1803
+ };
1804
+ } catch (error) {
1805
+ return {
1806
+ content: [{
1807
+ type: 'text',
1808
+ text: JSON.stringify({
1809
+ error: {
1810
+ code: error.code || 'UNKNOWN_ERROR',
1811
+ message: error.message
1812
+ }
1813
+ }, null, 2)
1814
+ }],
1815
+ isError: true
1816
+ };
1817
+ }
1818
+ }
1819
+
1820
+ // ============================================================
1821
+ // Phase 4 Commands: Polygons, Boolean Operations, Viewport, Blend Mode, Detach
1822
+ // ============================================================
1823
+
1824
+ /**
1825
+ * Create a polygon or star
1826
+ */
1827
+ export async function handleCreatePolygon(bridge, args) {
1828
+ if (!bridge.isConnected()) {
1829
+ return {
1830
+ content: [{
1831
+ type: 'text',
1832
+ text: JSON.stringify({
1833
+ error: {
1834
+ code: 'NOT_CONNECTED',
1835
+ message: 'Figma plugin is not connected.'
1836
+ }
1837
+ }, null, 2)
1838
+ }],
1839
+ isError: true
1840
+ };
1841
+ }
1842
+
1843
+ try {
1844
+ const result = await bridge.sendCommand('create_polygon', args);
1845
+ return {
1846
+ content: [{
1847
+ type: 'text',
1848
+ text: JSON.stringify(result, null, 2)
1849
+ }]
1850
+ };
1851
+ } catch (error) {
1852
+ return {
1853
+ content: [{
1854
+ type: 'text',
1855
+ text: JSON.stringify({
1856
+ error: {
1857
+ code: error.code || 'UNKNOWN_ERROR',
1858
+ message: error.message
1859
+ }
1860
+ }, null, 2)
1861
+ }],
1862
+ isError: true
1863
+ };
1864
+ }
1865
+ }
1866
+
1867
+ /**
1868
+ * Perform boolean operation on nodes
1869
+ */
1870
+ export async function handleBooleanOperation(bridge, args) {
1871
+ if (!bridge.isConnected()) {
1872
+ return {
1873
+ content: [{
1874
+ type: 'text',
1875
+ text: JSON.stringify({
1876
+ error: {
1877
+ code: 'NOT_CONNECTED',
1878
+ message: 'Figma plugin is not connected.'
1879
+ }
1880
+ }, null, 2)
1881
+ }],
1882
+ isError: true
1883
+ };
1884
+ }
1885
+
1886
+ const { operation, nodeIds } = args;
1887
+
1888
+ if (!operation) {
1889
+ return {
1890
+ content: [{
1891
+ type: 'text',
1892
+ text: JSON.stringify({
1893
+ error: {
1894
+ code: 'INVALID_PARAMS',
1895
+ message: 'operation is required'
1896
+ }
1897
+ }, null, 2)
1898
+ }],
1899
+ isError: true
1900
+ };
1901
+ }
1902
+
1903
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length < 2) {
1904
+ return {
1905
+ content: [{
1906
+ type: 'text',
1907
+ text: JSON.stringify({
1908
+ error: {
1909
+ code: 'INVALID_PARAMS',
1910
+ message: 'nodeIds must be an array with at least 2 nodes'
1911
+ }
1912
+ }, null, 2)
1913
+ }],
1914
+ isError: true
1915
+ };
1916
+ }
1917
+
1918
+ try {
1919
+ const result = await bridge.sendCommand('boolean_operation', args);
1920
+ return {
1921
+ content: [{
1922
+ type: 'text',
1923
+ text: JSON.stringify(result, null, 2)
1924
+ }]
1925
+ };
1926
+ } catch (error) {
1927
+ return {
1928
+ content: [{
1929
+ type: 'text',
1930
+ text: JSON.stringify({
1931
+ error: {
1932
+ code: error.code || 'UNKNOWN_ERROR',
1933
+ message: error.message
1934
+ }
1935
+ }, null, 2)
1936
+ }],
1937
+ isError: true
1938
+ };
1939
+ }
1940
+ }
1941
+
1942
+ /**
1943
+ * Zoom viewport to node(s)
1944
+ */
1945
+ export async function handleZoomToNode(bridge, args) {
1946
+ if (!bridge.isConnected()) {
1947
+ return {
1948
+ content: [{
1949
+ type: 'text',
1950
+ text: JSON.stringify({
1951
+ error: {
1952
+ code: 'NOT_CONNECTED',
1953
+ message: 'Figma plugin is not connected.'
1954
+ }
1955
+ }, null, 2)
1956
+ }],
1957
+ isError: true
1958
+ };
1959
+ }
1960
+
1961
+ const { nodeIds } = args;
1962
+
1963
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
1964
+ return {
1965
+ content: [{
1966
+ type: 'text',
1967
+ text: JSON.stringify({
1968
+ error: {
1969
+ code: 'INVALID_PARAMS',
1970
+ message: 'nodeIds must be a non-empty array'
1971
+ }
1972
+ }, null, 2)
1973
+ }],
1974
+ isError: true
1975
+ };
1976
+ }
1977
+
1978
+ try {
1979
+ const result = await bridge.sendCommand('zoom_to_node', { nodeIds });
1980
+ return {
1981
+ content: [{
1982
+ type: 'text',
1983
+ text: JSON.stringify(result, null, 2)
1984
+ }]
1985
+ };
1986
+ } catch (error) {
1987
+ return {
1988
+ content: [{
1989
+ type: 'text',
1990
+ text: JSON.stringify({
1991
+ error: {
1992
+ code: error.code || 'UNKNOWN_ERROR',
1993
+ message: error.message
1994
+ }
1995
+ }, null, 2)
1996
+ }],
1997
+ isError: true
1998
+ };
1999
+ }
2000
+ }
2001
+
2002
+ /**
2003
+ * Set blend mode on a node
2004
+ */
2005
+ export async function handleSetBlendMode(bridge, args) {
2006
+ if (!bridge.isConnected()) {
2007
+ return {
2008
+ content: [{
2009
+ type: 'text',
2010
+ text: JSON.stringify({
2011
+ error: {
2012
+ code: 'NOT_CONNECTED',
2013
+ message: 'Figma plugin is not connected.'
2014
+ }
2015
+ }, null, 2)
2016
+ }],
2017
+ isError: true
2018
+ };
2019
+ }
2020
+
2021
+ const { nodeId, blendMode } = args;
2022
+
2023
+ if (!nodeId) {
2024
+ return {
2025
+ content: [{
2026
+ type: 'text',
2027
+ text: JSON.stringify({
2028
+ error: {
2029
+ code: 'INVALID_PARAMS',
2030
+ message: 'nodeId is required'
2031
+ }
2032
+ }, null, 2)
2033
+ }],
2034
+ isError: true
2035
+ };
2036
+ }
2037
+
2038
+ if (!blendMode) {
2039
+ return {
2040
+ content: [{
2041
+ type: 'text',
2042
+ text: JSON.stringify({
2043
+ error: {
2044
+ code: 'INVALID_PARAMS',
2045
+ message: 'blendMode is required'
2046
+ }
2047
+ }, null, 2)
2048
+ }],
2049
+ isError: true
2050
+ };
2051
+ }
2052
+
2053
+ try {
2054
+ const result = await bridge.sendCommand('set_blend_mode', { nodeId, blendMode });
2055
+ return {
2056
+ content: [{
2057
+ type: 'text',
2058
+ text: JSON.stringify(result, null, 2)
2059
+ }]
2060
+ };
2061
+ } catch (error) {
2062
+ return {
2063
+ content: [{
2064
+ type: 'text',
2065
+ text: JSON.stringify({
2066
+ error: {
2067
+ code: error.code || 'UNKNOWN_ERROR',
2068
+ message: error.message
2069
+ }
2070
+ }, null, 2)
2071
+ }],
2072
+ isError: true
2073
+ };
2074
+ }
2075
+ }
2076
+
2077
+ /**
2078
+ * Detach instance from component
2079
+ */
2080
+ export async function handleDetachInstance(bridge, args) {
2081
+ if (!bridge.isConnected()) {
2082
+ return {
2083
+ content: [{
2084
+ type: 'text',
2085
+ text: JSON.stringify({
2086
+ error: {
2087
+ code: 'NOT_CONNECTED',
2088
+ message: 'Figma plugin is not connected.'
2089
+ }
2090
+ }, null, 2)
2091
+ }],
2092
+ isError: true
2093
+ };
2094
+ }
2095
+
2096
+ const { nodeId } = args;
2097
+
2098
+ if (!nodeId) {
2099
+ return {
2100
+ content: [{
2101
+ type: 'text',
2102
+ text: JSON.stringify({
2103
+ error: {
2104
+ code: 'INVALID_PARAMS',
2105
+ message: 'nodeId is required'
2106
+ }
2107
+ }, null, 2)
2108
+ }],
2109
+ isError: true
2110
+ };
2111
+ }
2112
+
2113
+ try {
2114
+ const result = await bridge.sendCommand('detach_instance', { nodeId });
2115
+ return {
2116
+ content: [{
2117
+ type: 'text',
2118
+ text: JSON.stringify(result, null, 2)
2119
+ }]
2120
+ };
2121
+ } catch (error) {
2122
+ return {
2123
+ content: [{
2124
+ type: 'text',
2125
+ text: JSON.stringify({
2126
+ error: {
2127
+ code: error.code || 'UNKNOWN_ERROR',
2128
+ message: error.message
2129
+ }
2130
+ }, null, 2)
2131
+ }],
2132
+ isError: true
2133
+ };
2134
+ }
2135
+ }
2136
+
2137
+ // ============================================================
2138
+ // Phase 5 Commands: Layout Align, Vector, Rename, Reorder
2139
+ // ============================================================
2140
+
2141
+ /**
2142
+ * Set layout align properties on a node (for auto-layout children)
2143
+ */
2144
+ export async function handleSetLayoutAlign(bridge, args) {
2145
+ if (!bridge.isConnected()) {
2146
+ return {
2147
+ content: [{
2148
+ type: 'text',
2149
+ text: JSON.stringify({
2150
+ error: {
2151
+ code: 'NOT_CONNECTED',
2152
+ message: 'Figma plugin is not connected.'
2153
+ }
2154
+ }, null, 2)
2155
+ }],
2156
+ isError: true
2157
+ };
2158
+ }
2159
+
2160
+ const { nodeId } = args;
2161
+
2162
+ if (!nodeId) {
2163
+ return {
2164
+ content: [{
2165
+ type: 'text',
2166
+ text: JSON.stringify({
2167
+ error: {
2168
+ code: 'INVALID_PARAMS',
2169
+ message: 'nodeId is required'
2170
+ }
2171
+ }, null, 2)
2172
+ }],
2173
+ isError: true
2174
+ };
2175
+ }
2176
+
2177
+ try {
2178
+ const result = await bridge.sendCommand('set_layout_align', args);
2179
+ return {
2180
+ content: [{
2181
+ type: 'text',
2182
+ text: JSON.stringify(result, null, 2)
2183
+ }]
2184
+ };
2185
+ } catch (error) {
2186
+ return {
2187
+ content: [{
2188
+ type: 'text',
2189
+ text: JSON.stringify({
2190
+ error: {
2191
+ code: error.code || 'UNKNOWN_ERROR',
2192
+ message: error.message
2193
+ }
2194
+ }, null, 2)
2195
+ }],
2196
+ isError: true
2197
+ };
2198
+ }
2199
+ }
2200
+
2201
+ /**
2202
+ * Create a vector with custom path data
2203
+ */
2204
+ export async function handleCreateVector(bridge, args) {
2205
+ if (!bridge.isConnected()) {
2206
+ return {
2207
+ content: [{
2208
+ type: 'text',
2209
+ text: JSON.stringify({
2210
+ error: {
2211
+ code: 'NOT_CONNECTED',
2212
+ message: 'Figma plugin is not connected.'
2213
+ }
2214
+ }, null, 2)
2215
+ }],
2216
+ isError: true
2217
+ };
2218
+ }
2219
+
2220
+ const { data } = args;
2221
+
2222
+ if (!data) {
2223
+ return {
2224
+ content: [{
2225
+ type: 'text',
2226
+ text: JSON.stringify({
2227
+ error: {
2228
+ code: 'INVALID_PARAMS',
2229
+ message: 'data (SVG path string) is required'
2230
+ }
2231
+ }, null, 2)
2232
+ }],
2233
+ isError: true
2234
+ };
2235
+ }
2236
+
2237
+ try {
2238
+ const result = await bridge.sendCommand('create_vector', args);
2239
+ return {
2240
+ content: [{
2241
+ type: 'text',
2242
+ text: JSON.stringify(result, null, 2)
2243
+ }]
2244
+ };
2245
+ } catch (error) {
2246
+ return {
2247
+ content: [{
2248
+ type: 'text',
2249
+ text: JSON.stringify({
2250
+ error: {
2251
+ code: error.code || 'UNKNOWN_ERROR',
2252
+ message: error.message
2253
+ }
2254
+ }, null, 2)
2255
+ }],
2256
+ isError: true
2257
+ };
2258
+ }
2259
+ }
2260
+
2261
+ /**
2262
+ * Rename one or more nodes
2263
+ */
2264
+ export async function handleRenameNode(bridge, args) {
2265
+ if (!bridge.isConnected()) {
2266
+ return {
2267
+ content: [{
2268
+ type: 'text',
2269
+ text: JSON.stringify({
2270
+ error: {
2271
+ code: 'NOT_CONNECTED',
2272
+ message: 'Figma plugin is not connected.'
2273
+ }
2274
+ }, null, 2)
2275
+ }],
2276
+ isError: true
2277
+ };
2278
+ }
2279
+
2280
+ const { nodeId, nodeIds, name } = args;
2281
+
2282
+ if (!nodeId && (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0)) {
2283
+ return {
2284
+ content: [{
2285
+ type: 'text',
2286
+ text: JSON.stringify({
2287
+ error: {
2288
+ code: 'INVALID_PARAMS',
2289
+ message: 'nodeId or nodeIds is required'
2290
+ }
2291
+ }, null, 2)
2292
+ }],
2293
+ isError: true
2294
+ };
2295
+ }
2296
+
2297
+ if (!name) {
2298
+ return {
2299
+ content: [{
2300
+ type: 'text',
2301
+ text: JSON.stringify({
2302
+ error: {
2303
+ code: 'INVALID_PARAMS',
2304
+ message: 'name is required'
2305
+ }
2306
+ }, null, 2)
2307
+ }],
2308
+ isError: true
2309
+ };
2310
+ }
2311
+
2312
+ try {
2313
+ const result = await bridge.sendCommand('rename_node', args);
2314
+ return {
2315
+ content: [{
2316
+ type: 'text',
2317
+ text: JSON.stringify(result, null, 2)
2318
+ }]
2319
+ };
2320
+ } catch (error) {
2321
+ return {
2322
+ content: [{
2323
+ type: 'text',
2324
+ text: JSON.stringify({
2325
+ error: {
2326
+ code: error.code || 'UNKNOWN_ERROR',
2327
+ message: error.message
2328
+ }
2329
+ }, null, 2)
2330
+ }],
2331
+ isError: true
2332
+ };
2333
+ }
2334
+ }
2335
+
2336
+ /**
2337
+ * Reorder a node (change z-order)
2338
+ */
2339
+ export async function handleReorderNode(bridge, args) {
2340
+ if (!bridge.isConnected()) {
2341
+ return {
2342
+ content: [{
2343
+ type: 'text',
2344
+ text: JSON.stringify({
2345
+ error: {
2346
+ code: 'NOT_CONNECTED',
2347
+ message: 'Figma plugin is not connected.'
2348
+ }
2349
+ }, null, 2)
2350
+ }],
2351
+ isError: true
2352
+ };
2353
+ }
2354
+
2355
+ const { nodeId, position } = args;
2356
+
2357
+ if (!nodeId) {
2358
+ return {
2359
+ content: [{
2360
+ type: 'text',
2361
+ text: JSON.stringify({
2362
+ error: {
2363
+ code: 'INVALID_PARAMS',
2364
+ message: 'nodeId is required'
2365
+ }
2366
+ }, null, 2)
2367
+ }],
2368
+ isError: true
2369
+ };
2370
+ }
2371
+
2372
+ if (position === undefined) {
2373
+ return {
2374
+ content: [{
2375
+ type: 'text',
2376
+ text: JSON.stringify({
2377
+ error: {
2378
+ code: 'INVALID_PARAMS',
2379
+ message: 'position is required (front, back, or index number)'
2380
+ }
2381
+ }, null, 2)
2382
+ }],
2383
+ isError: true
2384
+ };
2385
+ }
2386
+
2387
+ try {
2388
+ const result = await bridge.sendCommand('reorder_node', args);
2389
+ return {
2390
+ content: [{
2391
+ type: 'text',
2392
+ text: JSON.stringify(result, null, 2)
2393
+ }]
2394
+ };
2395
+ } catch (error) {
2396
+ return {
2397
+ content: [{
2398
+ type: 'text',
2399
+ text: JSON.stringify({
2400
+ error: {
2401
+ code: error.code || 'UNKNOWN_ERROR',
2402
+ message: error.message
2403
+ }
2404
+ }, null, 2)
2405
+ }],
2406
+ isError: true
2407
+ };
2408
+ }
2409
+ }
2410
+
2411
+ // ============================================================
2412
+ // Smart Query Commands (token-efficient search)
2413
+ // ============================================================
2414
+
2415
+ /**
2416
+ * Search nodes by name within a scope
2417
+ */
2418
+ export async function handleSearchNodes(bridge, args) {
2419
+ if (!bridge.isConnected()) {
2420
+ return {
2421
+ content: [{
2422
+ type: 'text',
2423
+ text: JSON.stringify({
2424
+ error: {
2425
+ code: 'NOT_CONNECTED',
2426
+ message: 'Figma plugin is not connected.'
2427
+ }
2428
+ }, null, 2)
2429
+ }],
2430
+ isError: true
2431
+ };
2432
+ }
2433
+
2434
+ const { parentId } = args;
2435
+
2436
+ if (!parentId) {
2437
+ return {
2438
+ content: [{
2439
+ type: 'text',
2440
+ text: JSON.stringify({
2441
+ error: {
2442
+ code: 'INVALID_PARAMS',
2443
+ message: 'parentId is required to scope the search'
2444
+ }
2445
+ }, null, 2)
2446
+ }],
2447
+ isError: true
2448
+ };
2449
+ }
2450
+
2451
+ try {
2452
+ const result = await bridge.sendCommand('search_nodes', args);
2453
+ return {
2454
+ content: [{
2455
+ type: 'text',
2456
+ text: JSON.stringify(result, null, 2)
2457
+ }]
2458
+ };
2459
+ } catch (error) {
2460
+ return {
2461
+ content: [{
2462
+ type: 'text',
2463
+ text: JSON.stringify({
2464
+ error: {
2465
+ code: error.code || 'UNKNOWN_ERROR',
2466
+ message: error.message
2467
+ }
2468
+ }, null, 2)
2469
+ }],
2470
+ isError: true
2471
+ };
2472
+ }
2473
+ }
2474
+
2475
+ /**
2476
+ * Search local components by name
2477
+ */
2478
+ export async function handleSearchComponents(bridge, args) {
2479
+ if (!bridge.isConnected()) {
2480
+ return {
2481
+ content: [{
2482
+ type: 'text',
2483
+ text: JSON.stringify({
2484
+ error: {
2485
+ code: 'NOT_CONNECTED',
2486
+ message: 'Figma plugin is not connected.'
2487
+ }
2488
+ }, null, 2)
2489
+ }],
2490
+ isError: true
2491
+ };
2492
+ }
2493
+
2494
+ try {
2495
+ const result = await bridge.sendCommand('search_components', args);
2496
+ return {
2497
+ content: [{
2498
+ type: 'text',
2499
+ text: JSON.stringify(result, null, 2)
2500
+ }]
2501
+ };
2502
+ } catch (error) {
2503
+ return {
2504
+ content: [{
2505
+ type: 'text',
2506
+ text: JSON.stringify({
2507
+ error: {
2508
+ code: error.code || 'UNKNOWN_ERROR',
2509
+ message: error.message
2510
+ }
2511
+ }, null, 2)
2512
+ }],
2513
+ isError: true
2514
+ };
2515
+ }
2516
+ }
2517
+
2518
+ /**
2519
+ * Search local styles by name
2520
+ */
2521
+ export async function handleSearchStyles(bridge, args) {
2522
+ if (!bridge.isConnected()) {
2523
+ return {
2524
+ content: [{
2525
+ type: 'text',
2526
+ text: JSON.stringify({
2527
+ error: {
2528
+ code: 'NOT_CONNECTED',
2529
+ message: 'Figma plugin is not connected.'
2530
+ }
2531
+ }, null, 2)
2532
+ }],
2533
+ isError: true
2534
+ };
2535
+ }
2536
+
2537
+ try {
2538
+ const result = await bridge.sendCommand('search_styles', args);
2539
+ return {
2540
+ content: [{
2541
+ type: 'text',
2542
+ text: JSON.stringify(result, null, 2)
2543
+ }]
2544
+ };
2545
+ } catch (error) {
2546
+ return {
2547
+ content: [{
2548
+ type: 'text',
2549
+ text: JSON.stringify({
2550
+ error: {
2551
+ code: error.code || 'UNKNOWN_ERROR',
2552
+ message: error.message
2553
+ }
2554
+ }, null, 2)
2555
+ }],
2556
+ isError: true
2557
+ };
2558
+ }
2559
+ }
2560
+
2561
+ /**
2562
+ * Get immediate children of a node
2563
+ */
2564
+ export async function handleGetChildren(bridge, args) {
2565
+ if (!bridge.isConnected()) {
2566
+ return {
2567
+ content: [{
2568
+ type: 'text',
2569
+ text: JSON.stringify({
2570
+ error: {
2571
+ code: 'NOT_CONNECTED',
2572
+ message: 'Figma plugin is not connected.'
2573
+ }
2574
+ }, null, 2)
2575
+ }],
2576
+ isError: true
2577
+ };
2578
+ }
2579
+
2580
+ const { parentId } = args;
2581
+
2582
+ if (!parentId) {
2583
+ return {
2584
+ content: [{
2585
+ type: 'text',
2586
+ text: JSON.stringify({
2587
+ error: {
2588
+ code: 'INVALID_PARAMS',
2589
+ message: 'parentId is required'
2590
+ }
2591
+ }, null, 2)
2592
+ }],
2593
+ isError: true
2594
+ };
2595
+ }
2596
+
2597
+ try {
2598
+ const result = await bridge.sendCommand('get_children', args);
2599
+ return {
2600
+ content: [{
2601
+ type: 'text',
2602
+ text: JSON.stringify(result, null, 2)
2603
+ }]
2604
+ };
2605
+ } catch (error) {
2606
+ return {
2607
+ content: [{
2608
+ type: 'text',
2609
+ text: JSON.stringify({
2610
+ error: {
2611
+ code: error.code || 'UNKNOWN_ERROR',
2612
+ message: error.message
2613
+ }
2614
+ }, null, 2)
2615
+ }],
2616
+ isError: true
2617
+ };
2618
+ }
2619
+ }
2620
+
2621
+ /**
2622
+ * Set text style properties on an existing text node
2623
+ */
2624
+ export async function handleSetTextStyle(bridge, args) {
2625
+ if (!bridge.isConnected()) {
2626
+ return {
2627
+ content: [{
2628
+ type: 'text',
2629
+ text: JSON.stringify({
2630
+ error: {
2631
+ code: 'NOT_CONNECTED',
2632
+ message: 'Figma plugin is not connected.'
2633
+ }
2634
+ }, null, 2)
2635
+ }],
2636
+ isError: true
2637
+ };
2638
+ }
2639
+
2640
+ const { nodeId } = args;
2641
+
2642
+ if (!nodeId) {
2643
+ return {
2644
+ content: [{
2645
+ type: 'text',
2646
+ text: JSON.stringify({
2647
+ error: {
2648
+ code: 'INVALID_PARAMS',
2649
+ message: 'nodeId is required'
2650
+ }
2651
+ }, null, 2)
2652
+ }],
2653
+ isError: true
2654
+ };
2655
+ }
2656
+
2657
+ try {
2658
+ const result = await bridge.sendCommand('set_text_style', args);
2659
+ return {
2660
+ content: [{
2661
+ type: 'text',
2662
+ text: JSON.stringify(result, null, 2)
2663
+ }]
2664
+ };
2665
+ } catch (error) {
2666
+ return {
2667
+ content: [{
2668
+ type: 'text',
2669
+ text: JSON.stringify({
2670
+ error: {
2671
+ code: error.code || 'UNKNOWN_ERROR',
2672
+ message: error.message
2673
+ }
2674
+ }, null, 2)
2675
+ }],
2676
+ isError: true
2677
+ };
2678
+ }
2679
+ }
2680
+
2681
+ /**
2682
+ * Create a new local paint style
2683
+ */
2684
+ export async function handleCreatePaintStyle(bridge, args) {
2685
+ if (!bridge.isConnected()) {
2686
+ return {
2687
+ content: [{
2688
+ type: 'text',
2689
+ text: JSON.stringify({
2690
+ error: {
2691
+ code: 'NOT_CONNECTED',
2692
+ message: 'Figma plugin is not connected.'
2693
+ }
2694
+ }, null, 2)
2695
+ }],
2696
+ isError: true
2697
+ };
2698
+ }
2699
+
2700
+ const { name, fills } = args;
2701
+
2702
+ if (!name || !fills) {
2703
+ return {
2704
+ content: [{
2705
+ type: 'text',
2706
+ text: JSON.stringify({
2707
+ error: {
2708
+ code: 'INVALID_PARAMS',
2709
+ message: 'name and fills are required'
2710
+ }
2711
+ }, null, 2)
2712
+ }],
2713
+ isError: true
2714
+ };
2715
+ }
2716
+
2717
+ try {
2718
+ const result = await bridge.sendCommand('create_paint_style', args);
2719
+ return {
2720
+ content: [{
2721
+ type: 'text',
2722
+ text: JSON.stringify(result, null, 2)
2723
+ }]
2724
+ };
2725
+ } catch (error) {
2726
+ return {
2727
+ content: [{
2728
+ type: 'text',
2729
+ text: JSON.stringify({
2730
+ error: {
2731
+ code: error.code || 'UNKNOWN_ERROR',
2732
+ message: error.message
2733
+ }
2734
+ }, null, 2)
2735
+ }],
2736
+ isError: true
2737
+ };
2738
+ }
2739
+ }
2740
+
2741
+ /**
2742
+ * Create a new local text style
2743
+ */
2744
+ export async function handleCreateTextStyle(bridge, args) {
2745
+ if (!bridge.isConnected()) {
2746
+ return {
2747
+ content: [{
2748
+ type: 'text',
2749
+ text: JSON.stringify({
2750
+ error: {
2751
+ code: 'NOT_CONNECTED',
2752
+ message: 'Figma plugin is not connected.'
2753
+ }
2754
+ }, null, 2)
2755
+ }],
2756
+ isError: true
2757
+ };
2758
+ }
2759
+
2760
+ const { name } = args;
2761
+
2762
+ if (!name) {
2763
+ return {
2764
+ content: [{
2765
+ type: 'text',
2766
+ text: JSON.stringify({
2767
+ error: {
2768
+ code: 'INVALID_PARAMS',
2769
+ message: 'name is required'
2770
+ }
2771
+ }, null, 2)
2772
+ }],
2773
+ isError: true
2774
+ };
2775
+ }
2776
+
2777
+ try {
2778
+ const result = await bridge.sendCommand('create_text_style', args);
2779
+ return {
2780
+ content: [{
2781
+ type: 'text',
2782
+ text: JSON.stringify(result, null, 2)
2783
+ }]
2784
+ };
2785
+ } catch (error) {
2786
+ return {
2787
+ content: [{
2788
+ type: 'text',
2789
+ text: JSON.stringify({
2790
+ error: {
2791
+ code: error.code || 'UNKNOWN_ERROR',
2792
+ message: error.message
2793
+ }
2794
+ }, null, 2)
2795
+ }],
2796
+ isError: true
2797
+ };
2798
+ }
2799
+ }
2800
+
2801
+ /**
2802
+ * Create a new variable collection
2803
+ */
2804
+ export async function handleCreateVariableCollection(bridge, args) {
2805
+ if (!bridge.isConnected()) {
2806
+ return {
2807
+ content: [{
2808
+ type: 'text',
2809
+ text: JSON.stringify({
2810
+ error: {
2811
+ code: 'NOT_CONNECTED',
2812
+ message: 'Figma plugin is not connected.'
2813
+ }
2814
+ }, null, 2)
2815
+ }],
2816
+ isError: true
2817
+ };
2818
+ }
2819
+
2820
+ const { name } = args;
2821
+
2822
+ if (!name) {
2823
+ return {
2824
+ content: [{
2825
+ type: 'text',
2826
+ text: JSON.stringify({
2827
+ error: {
2828
+ code: 'INVALID_PARAMS',
2829
+ message: 'name is required'
2830
+ }
2831
+ }, null, 2)
2832
+ }],
2833
+ isError: true
2834
+ };
2835
+ }
2836
+
2837
+ try {
2838
+ const result = await bridge.sendCommand('create_variable_collection', args);
2839
+ return {
2840
+ content: [{
2841
+ type: 'text',
2842
+ text: JSON.stringify(result, null, 2)
2843
+ }]
2844
+ };
2845
+ } catch (error) {
2846
+ return {
2847
+ content: [{
2848
+ type: 'text',
2849
+ text: JSON.stringify({
2850
+ error: {
2851
+ code: error.code || 'UNKNOWN_ERROR',
2852
+ message: error.message
2853
+ }
2854
+ }, null, 2)
2855
+ }],
2856
+ isError: true
2857
+ };
2858
+ }
2859
+ }
2860
+
2861
+ /**
2862
+ * Create a new variable in a collection
2863
+ */
2864
+ export async function handleCreateVariable(bridge, args) {
2865
+ if (!bridge.isConnected()) {
2866
+ return {
2867
+ content: [{
2868
+ type: 'text',
2869
+ text: JSON.stringify({
2870
+ error: {
2871
+ code: 'NOT_CONNECTED',
2872
+ message: 'Figma plugin is not connected.'
2873
+ }
2874
+ }, null, 2)
2875
+ }],
2876
+ isError: true
2877
+ };
2878
+ }
2879
+
2880
+ const { collectionId, name, type } = args;
2881
+
2882
+ if (!collectionId || !name || !type) {
2883
+ return {
2884
+ content: [{
2885
+ type: 'text',
2886
+ text: JSON.stringify({
2887
+ error: {
2888
+ code: 'INVALID_PARAMS',
2889
+ message: 'collectionId, name, and type are required'
2890
+ }
2891
+ }, null, 2)
2892
+ }],
2893
+ isError: true
2894
+ };
2895
+ }
2896
+
2897
+ try {
2898
+ const result = await bridge.sendCommand('create_variable', args);
2899
+ return {
2900
+ content: [{
2901
+ type: 'text',
2902
+ text: JSON.stringify(result, null, 2)
2903
+ }]
2904
+ };
2905
+ } catch (error) {
2906
+ return {
2907
+ content: [{
2908
+ type: 'text',
2909
+ text: JSON.stringify({
2910
+ error: {
2911
+ code: error.code || 'UNKNOWN_ERROR',
2912
+ message: error.message
2913
+ }
2914
+ }, null, 2)
2915
+ }],
2916
+ isError: true
2917
+ };
2918
+ }
2919
+ }
2920
+
2921
+ /**
2922
+ * Rename an existing variable
2923
+ */
2924
+ export async function handleRenameVariable(bridge, args) {
2925
+ if (!bridge.isConnected()) {
2926
+ return {
2927
+ content: [{
2928
+ type: 'text',
2929
+ text: JSON.stringify({
2930
+ error: {
2931
+ code: 'NOT_CONNECTED',
2932
+ message: 'Figma plugin is not connected.'
2933
+ }
2934
+ }, null, 2)
2935
+ }],
2936
+ isError: true
2937
+ };
2938
+ }
2939
+
2940
+ const { variableId, name } = args;
2941
+
2942
+ if (!variableId || !name) {
2943
+ return {
2944
+ content: [{
2945
+ type: 'text',
2946
+ text: JSON.stringify({
2947
+ error: {
2948
+ code: 'INVALID_PARAMS',
2949
+ message: 'variableId and name are required'
2950
+ }
2951
+ }, null, 2)
2952
+ }],
2953
+ isError: true
2954
+ };
2955
+ }
2956
+
2957
+ try {
2958
+ const result = await bridge.sendCommand('rename_variable', args);
2959
+ return {
2960
+ content: [{
2961
+ type: 'text',
2962
+ text: JSON.stringify(result, null, 2)
2963
+ }]
2964
+ };
2965
+ } catch (error) {
2966
+ return {
2967
+ content: [{
2968
+ type: 'text',
2969
+ text: JSON.stringify({
2970
+ error: {
2971
+ code: error.code || 'UNKNOWN_ERROR',
2972
+ message: error.message
2973
+ }
2974
+ }, null, 2)
2975
+ }],
2976
+ isError: true
2977
+ };
2978
+ }
2979
+ }
2980
+
2981
+ /**
2982
+ * Delete one or more variables
2983
+ */
2984
+ export async function handleDeleteVariables(bridge, args) {
2985
+ if (!bridge.isConnected()) {
2986
+ return {
2987
+ content: [{
2988
+ type: 'text',
2989
+ text: JSON.stringify({
2990
+ error: {
2991
+ code: 'NOT_CONNECTED',
2992
+ message: 'Figma plugin is not connected.'
2993
+ }
2994
+ }, null, 2)
2995
+ }],
2996
+ isError: true
2997
+ };
2998
+ }
2999
+
3000
+ const { variableIds } = args;
3001
+
3002
+ if (!variableIds || !Array.isArray(variableIds) || variableIds.length === 0) {
3003
+ return {
3004
+ content: [{
3005
+ type: 'text',
3006
+ text: JSON.stringify({
3007
+ error: {
3008
+ code: 'INVALID_PARAMS',
3009
+ message: 'variableIds array is required'
3010
+ }
3011
+ }, null, 2)
3012
+ }],
3013
+ isError: true
3014
+ };
3015
+ }
3016
+
3017
+ try {
3018
+ const result = await bridge.sendCommand('delete_variables', args);
3019
+ return {
3020
+ content: [{
3021
+ type: 'text',
3022
+ text: JSON.stringify(result, null, 2)
3023
+ }]
3024
+ };
3025
+ } catch (error) {
3026
+ return {
3027
+ content: [{
3028
+ type: 'text',
3029
+ text: JSON.stringify({
3030
+ error: {
3031
+ code: error.code || 'UNKNOWN_ERROR',
3032
+ message: error.message
3033
+ }
3034
+ }, null, 2)
3035
+ }],
3036
+ isError: true
3037
+ };
3038
+ }
3039
+ }
3040
+
3041
+ /**
3042
+ * Delete a variable collection
3043
+ */
3044
+ export async function handleDeleteVariableCollection(bridge, args) {
3045
+ if (!bridge.isConnected()) {
3046
+ return {
3047
+ content: [{
3048
+ type: 'text',
3049
+ text: JSON.stringify({
3050
+ error: {
3051
+ code: 'NOT_CONNECTED',
3052
+ message: 'Figma plugin is not connected.'
3053
+ }
3054
+ }, null, 2)
3055
+ }],
3056
+ isError: true
3057
+ };
3058
+ }
3059
+
3060
+ const { collectionId } = args;
3061
+
3062
+ if (!collectionId) {
3063
+ return {
3064
+ content: [{
3065
+ type: 'text',
3066
+ text: JSON.stringify({
3067
+ error: {
3068
+ code: 'INVALID_PARAMS',
3069
+ message: 'collectionId is required'
3070
+ }
3071
+ }, null, 2)
3072
+ }],
3073
+ isError: true
3074
+ };
3075
+ }
3076
+
3077
+ try {
3078
+ const result = await bridge.sendCommand('delete_variable_collection', args);
3079
+ return {
3080
+ content: [{
3081
+ type: 'text',
3082
+ text: JSON.stringify(result, null, 2)
3083
+ }]
3084
+ };
3085
+ } catch (error) {
3086
+ return {
3087
+ content: [{
3088
+ type: 'text',
3089
+ text: JSON.stringify({
3090
+ error: {
3091
+ code: error.code || 'UNKNOWN_ERROR',
3092
+ message: error.message
3093
+ }
3094
+ }, null, 2)
3095
+ }],
3096
+ isError: true
3097
+ };
3098
+ }
3099
+ }
3100
+
3101
+ /**
3102
+ * Rename a variable collection
3103
+ */
3104
+ export async function handleRenameVariableCollection(bridge, args) {
3105
+ if (!bridge.isConnected()) {
3106
+ return {
3107
+ content: [{
3108
+ type: 'text',
3109
+ text: JSON.stringify({
3110
+ error: {
3111
+ code: 'NOT_CONNECTED',
3112
+ message: 'Figma plugin is not connected.'
3113
+ }
3114
+ }, null, 2)
3115
+ }],
3116
+ isError: true
3117
+ };
3118
+ }
3119
+
3120
+ const { collectionId, name } = args;
3121
+
3122
+ if (!collectionId || !name) {
3123
+ return {
3124
+ content: [{
3125
+ type: 'text',
3126
+ text: JSON.stringify({
3127
+ error: {
3128
+ code: 'INVALID_PARAMS',
3129
+ message: 'collectionId and name are required'
3130
+ }
3131
+ }, null, 2)
3132
+ }],
3133
+ isError: true
3134
+ };
3135
+ }
3136
+
3137
+ try {
3138
+ const result = await bridge.sendCommand('rename_variable_collection', args);
3139
+ return {
3140
+ content: [{
3141
+ type: 'text',
3142
+ text: JSON.stringify(result, null, 2)
3143
+ }]
3144
+ };
3145
+ } catch (error) {
3146
+ return {
3147
+ content: [{
3148
+ type: 'text',
3149
+ text: JSON.stringify({
3150
+ error: {
3151
+ code: error.code || 'UNKNOWN_ERROR',
3152
+ message: error.message
3153
+ }
3154
+ }, null, 2)
3155
+ }],
3156
+ isError: true
3157
+ };
3158
+ }
3159
+ }
3160
+
3161
+ /**
3162
+ * Rename a mode in a variable collection
3163
+ */
3164
+ export async function handleRenameMode(bridge, args) {
3165
+ if (!bridge.isConnected()) {
3166
+ return {
3167
+ content: [{
3168
+ type: 'text',
3169
+ text: JSON.stringify({
3170
+ error: {
3171
+ code: 'NOT_CONNECTED',
3172
+ message: 'Figma plugin is not connected.'
3173
+ }
3174
+ }, null, 2)
3175
+ }],
3176
+ isError: true
3177
+ };
3178
+ }
3179
+
3180
+ const { collectionId, modeId, name } = args;
3181
+
3182
+ if (!collectionId || !modeId || !name) {
3183
+ return {
3184
+ content: [{
3185
+ type: 'text',
3186
+ text: JSON.stringify({
3187
+ error: {
3188
+ code: 'INVALID_PARAMS',
3189
+ message: 'collectionId, modeId, and name are required'
3190
+ }
3191
+ }, null, 2)
3192
+ }],
3193
+ isError: true
3194
+ };
3195
+ }
3196
+
3197
+ try {
3198
+ const result = await bridge.sendCommand('rename_mode', args);
3199
+ return {
3200
+ content: [{
3201
+ type: 'text',
3202
+ text: JSON.stringify(result, null, 2)
3203
+ }]
3204
+ };
3205
+ } catch (error) {
3206
+ return {
3207
+ content: [{
3208
+ type: 'text',
3209
+ text: JSON.stringify({
3210
+ error: {
3211
+ code: error.code || 'UNKNOWN_ERROR',
3212
+ message: error.message
3213
+ }
3214
+ }, null, 2)
3215
+ }],
3216
+ isError: true
3217
+ };
3218
+ }
3219
+ }
3220
+
3221
+ /**
3222
+ * Add a mode to a variable collection
3223
+ */
3224
+ export async function handleAddMode(bridge, args) {
3225
+ if (!bridge.isConnected()) {
3226
+ return {
3227
+ content: [{
3228
+ type: 'text',
3229
+ text: JSON.stringify({
3230
+ error: {
3231
+ code: 'NOT_CONNECTED',
3232
+ message: 'Figma plugin is not connected.'
3233
+ }
3234
+ }, null, 2)
3235
+ }],
3236
+ isError: true
3237
+ };
3238
+ }
3239
+
3240
+ const { collectionId, name } = args;
3241
+
3242
+ if (!collectionId || !name) {
3243
+ return {
3244
+ content: [{
3245
+ type: 'text',
3246
+ text: JSON.stringify({
3247
+ error: {
3248
+ code: 'INVALID_PARAMS',
3249
+ message: 'collectionId and name are required'
3250
+ }
3251
+ }, null, 2)
3252
+ }],
3253
+ isError: true
3254
+ };
3255
+ }
3256
+
3257
+ try {
3258
+ const result = await bridge.sendCommand('add_mode', args);
3259
+ return {
3260
+ content: [{
3261
+ type: 'text',
3262
+ text: JSON.stringify(result, null, 2)
3263
+ }]
3264
+ };
3265
+ } catch (error) {
3266
+ return {
3267
+ content: [{
3268
+ type: 'text',
3269
+ text: JSON.stringify({
3270
+ error: {
3271
+ code: error.code || 'UNKNOWN_ERROR',
3272
+ message: error.message
3273
+ }
3274
+ }, null, 2)
3275
+ }],
3276
+ isError: true
3277
+ };
3278
+ }
3279
+ }
3280
+
3281
+ /**
3282
+ * Delete a mode from a variable collection
3283
+ */
3284
+ export async function handleDeleteMode(bridge, args) {
3285
+ if (!bridge.isConnected()) {
3286
+ return {
3287
+ content: [{
3288
+ type: 'text',
3289
+ text: JSON.stringify({
3290
+ error: {
3291
+ code: 'NOT_CONNECTED',
3292
+ message: 'Figma plugin is not connected.'
3293
+ }
3294
+ }, null, 2)
3295
+ }],
3296
+ isError: true
3297
+ };
3298
+ }
3299
+
3300
+ const { collectionId, modeId } = args;
3301
+
3302
+ if (!collectionId || !modeId) {
3303
+ return {
3304
+ content: [{
3305
+ type: 'text',
3306
+ text: JSON.stringify({
3307
+ error: {
3308
+ code: 'INVALID_PARAMS',
3309
+ message: 'collectionId and modeId are required'
3310
+ }
3311
+ }, null, 2)
3312
+ }],
3313
+ isError: true
3314
+ };
3315
+ }
3316
+
3317
+ try {
3318
+ const result = await bridge.sendCommand('delete_mode', args);
3319
+ return {
3320
+ content: [{
3321
+ type: 'text',
3322
+ text: JSON.stringify(result, null, 2)
3323
+ }]
3324
+ };
3325
+ } catch (error) {
3326
+ return {
3327
+ content: [{
3328
+ type: 'text',
3329
+ text: JSON.stringify({
3330
+ error: {
3331
+ code: error.code || 'UNKNOWN_ERROR',
3332
+ message: error.message
3333
+ }
3334
+ }, null, 2)
3335
+ }],
3336
+ isError: true
3337
+ };
3338
+ }
3339
+ }
3340
+
3341
+ /**
3342
+ * Unbind a variable from a node property
3343
+ */
3344
+ export async function handleUnbindVariable(bridge, args) {
3345
+ if (!bridge.isConnected()) {
3346
+ return {
3347
+ content: [{
3348
+ type: 'text',
3349
+ text: JSON.stringify({
3350
+ error: {
3351
+ code: 'NOT_CONNECTED',
3352
+ message: 'Figma plugin is not connected.'
3353
+ }
3354
+ }, null, 2)
3355
+ }],
3356
+ isError: true
3357
+ };
3358
+ }
3359
+
3360
+ const { nodeId, field } = args;
3361
+
3362
+ if (!nodeId || !field) {
3363
+ return {
3364
+ content: [{
3365
+ type: 'text',
3366
+ text: JSON.stringify({
3367
+ error: {
3368
+ code: 'INVALID_PARAMS',
3369
+ message: 'nodeId and field are required'
3370
+ }
3371
+ }, null, 2)
3372
+ }],
3373
+ isError: true
3374
+ };
3375
+ }
3376
+
3377
+ try {
3378
+ const result = await bridge.sendCommand('unbind_variable', args);
3379
+ return {
3380
+ content: [{
3381
+ type: 'text',
3382
+ text: JSON.stringify(result, null, 2)
3383
+ }]
3384
+ };
3385
+ } catch (error) {
3386
+ return {
3387
+ content: [{
3388
+ type: 'text',
3389
+ text: JSON.stringify({
3390
+ error: {
3391
+ code: error.code || 'UNKNOWN_ERROR',
3392
+ message: error.message
3393
+ }
3394
+ }, null, 2)
3395
+ }],
3396
+ isError: true
3397
+ };
3398
+ }
3399
+ }
3400
+
3401
+ // ============================================================
3402
+ // Page Management Commands
3403
+ // ============================================================
3404
+
3405
+ /**
3406
+ * Create a new page
3407
+ */
3408
+ export async function handleCreatePage(bridge, args) {
3409
+ if (!bridge.isConnected()) {
3410
+ return {
3411
+ content: [{
3412
+ type: 'text',
3413
+ text: JSON.stringify({
3414
+ error: {
3415
+ code: 'NOT_CONNECTED',
3416
+ message: 'Figma plugin is not connected.'
3417
+ }
3418
+ }, null, 2)
3419
+ }],
3420
+ isError: true
3421
+ };
3422
+ }
3423
+
3424
+ const { name } = args;
3425
+
3426
+ if (!name) {
3427
+ return {
3428
+ content: [{
3429
+ type: 'text',
3430
+ text: JSON.stringify({
3431
+ error: {
3432
+ code: 'INVALID_PARAMS',
3433
+ message: 'name is required'
3434
+ }
3435
+ }, null, 2)
3436
+ }],
3437
+ isError: true
3438
+ };
3439
+ }
3440
+
3441
+ try {
3442
+ const result = await bridge.sendCommand('create_page', args);
3443
+ return {
3444
+ content: [{
3445
+ type: 'text',
3446
+ text: JSON.stringify(result, null, 2)
3447
+ }]
3448
+ };
3449
+ } catch (error) {
3450
+ return {
3451
+ content: [{
3452
+ type: 'text',
3453
+ text: JSON.stringify({
3454
+ error: {
3455
+ code: error.code || 'UNKNOWN_ERROR',
3456
+ message: error.message
3457
+ }
3458
+ }, null, 2)
3459
+ }],
3460
+ isError: true
3461
+ };
3462
+ }
3463
+ }
3464
+
3465
+ /**
3466
+ * Rename a page
3467
+ */
3468
+ export async function handleRenamePage(bridge, args) {
3469
+ if (!bridge.isConnected()) {
3470
+ return {
3471
+ content: [{
3472
+ type: 'text',
3473
+ text: JSON.stringify({
3474
+ error: {
3475
+ code: 'NOT_CONNECTED',
3476
+ message: 'Figma plugin is not connected.'
3477
+ }
3478
+ }, null, 2)
3479
+ }],
3480
+ isError: true
3481
+ };
3482
+ }
3483
+
3484
+ const { pageId, name } = args;
3485
+
3486
+ if (!pageId || !name) {
3487
+ return {
3488
+ content: [{
3489
+ type: 'text',
3490
+ text: JSON.stringify({
3491
+ error: {
3492
+ code: 'INVALID_PARAMS',
3493
+ message: 'pageId and name are required'
3494
+ }
3495
+ }, null, 2)
3496
+ }],
3497
+ isError: true
3498
+ };
3499
+ }
3500
+
3501
+ try {
3502
+ const result = await bridge.sendCommand('rename_page', args);
3503
+ return {
3504
+ content: [{
3505
+ type: 'text',
3506
+ text: JSON.stringify(result, null, 2)
3507
+ }]
3508
+ };
3509
+ } catch (error) {
3510
+ return {
3511
+ content: [{
3512
+ type: 'text',
3513
+ text: JSON.stringify({
3514
+ error: {
3515
+ code: error.code || 'UNKNOWN_ERROR',
3516
+ message: error.message
3517
+ }
3518
+ }, null, 2)
3519
+ }],
3520
+ isError: true
3521
+ };
3522
+ }
3523
+ }
3524
+
3525
+ /**
3526
+ * Delete a page
3527
+ */
3528
+ export async function handleDeletePage(bridge, args) {
3529
+ if (!bridge.isConnected()) {
3530
+ return {
3531
+ content: [{
3532
+ type: 'text',
3533
+ text: JSON.stringify({
3534
+ error: {
3535
+ code: 'NOT_CONNECTED',
3536
+ message: 'Figma plugin is not connected.'
3537
+ }
3538
+ }, null, 2)
3539
+ }],
3540
+ isError: true
3541
+ };
3542
+ }
3543
+
3544
+ const { pageId } = args;
3545
+
3546
+ if (!pageId) {
3547
+ return {
3548
+ content: [{
3549
+ type: 'text',
3550
+ text: JSON.stringify({
3551
+ error: {
3552
+ code: 'INVALID_PARAMS',
3553
+ message: 'pageId is required'
3554
+ }
3555
+ }, null, 2)
3556
+ }],
3557
+ isError: true
3558
+ };
3559
+ }
3560
+
3561
+ try {
3562
+ const result = await bridge.sendCommand('delete_page', args);
3563
+ return {
3564
+ content: [{
3565
+ type: 'text',
3566
+ text: JSON.stringify(result, null, 2)
3567
+ }]
3568
+ };
3569
+ } catch (error) {
3570
+ return {
3571
+ content: [{
3572
+ type: 'text',
3573
+ text: JSON.stringify({
3574
+ error: {
3575
+ code: error.code || 'UNKNOWN_ERROR',
3576
+ message: error.message
3577
+ }
3578
+ }, null, 2)
3579
+ }],
3580
+ isError: true
3581
+ };
3582
+ }
3583
+ }
3584
+
3585
+ /**
3586
+ * Reorder a page (change its position in the page list)
3587
+ */
3588
+ export async function handleReorderPage(bridge, args) {
3589
+ if (!bridge.isConnected()) {
3590
+ return {
3591
+ content: [{
3592
+ type: 'text',
3593
+ text: JSON.stringify({
3594
+ error: {
3595
+ code: 'NOT_CONNECTED',
3596
+ message: 'Figma plugin is not connected.'
3597
+ }
3598
+ }, null, 2)
3599
+ }],
3600
+ isError: true
3601
+ };
3602
+ }
3603
+
3604
+ const { pageId, index } = args;
3605
+
3606
+ if (!pageId || index === undefined) {
3607
+ return {
3608
+ content: [{
3609
+ type: 'text',
3610
+ text: JSON.stringify({
3611
+ error: {
3612
+ code: 'INVALID_PARAMS',
3613
+ message: 'pageId and index are required'
3614
+ }
3615
+ }, null, 2)
3616
+ }],
3617
+ isError: true
3618
+ };
3619
+ }
3620
+
3621
+ try {
3622
+ const result = await bridge.sendCommand('reorder_page', args);
3623
+ return {
3624
+ content: [{
3625
+ type: 'text',
3626
+ text: JSON.stringify(result, null, 2)
3627
+ }]
3628
+ };
3629
+ } catch (error) {
3630
+ return {
3631
+ content: [{
3632
+ type: 'text',
3633
+ text: JSON.stringify({
3634
+ error: {
3635
+ code: error.code || 'UNKNOWN_ERROR',
3636
+ message: error.message
3637
+ }
3638
+ }, null, 2)
3639
+ }],
3640
+ isError: true
3641
+ };
3642
+ }
3643
+ }
3644
+
3645
+ // ============================================================
3646
+ // Node Structure Commands
3647
+ // ============================================================
3648
+
3649
+ /**
3650
+ * Reparent nodes - move nodes to a different parent
3651
+ */
3652
+ export async function handleReparentNodes(bridge, args) {
3653
+ if (!bridge.isConnected()) {
3654
+ return {
3655
+ content: [{
3656
+ type: 'text',
3657
+ text: JSON.stringify({
3658
+ error: {
3659
+ code: 'NOT_CONNECTED',
3660
+ message: 'Figma plugin is not connected.'
3661
+ }
3662
+ }, null, 2)
3663
+ }],
3664
+ isError: true
3665
+ };
3666
+ }
3667
+
3668
+ const { nodeIds, newParentId } = args;
3669
+
3670
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
3671
+ return {
3672
+ content: [{
3673
+ type: 'text',
3674
+ text: JSON.stringify({
3675
+ error: {
3676
+ code: 'INVALID_PARAMS',
3677
+ message: 'nodeIds must be a non-empty array'
3678
+ }
3679
+ }, null, 2)
3680
+ }],
3681
+ isError: true
3682
+ };
3683
+ }
3684
+
3685
+ if (!newParentId) {
3686
+ return {
3687
+ content: [{
3688
+ type: 'text',
3689
+ text: JSON.stringify({
3690
+ error: {
3691
+ code: 'INVALID_PARAMS',
3692
+ message: 'newParentId is required'
3693
+ }
3694
+ }, null, 2)
3695
+ }],
3696
+ isError: true
3697
+ };
3698
+ }
3699
+
3700
+ try {
3701
+ const result = await bridge.sendCommand('reparent_nodes', args);
3702
+ return {
3703
+ content: [{
3704
+ type: 'text',
3705
+ text: JSON.stringify(result, null, 2)
3706
+ }]
3707
+ };
3708
+ } catch (error) {
3709
+ return {
3710
+ content: [{
3711
+ type: 'text',
3712
+ text: JSON.stringify({
3713
+ error: {
3714
+ code: error.code || 'UNKNOWN_ERROR',
3715
+ message: error.message
3716
+ }
3717
+ }, null, 2)
3718
+ }],
3719
+ isError: true
3720
+ };
3721
+ }
3722
+ }
3723
+
3724
+ /**
3725
+ * Move nodes to a different page
3726
+ */
3727
+ export async function handleMoveToPage(bridge, args) {
3728
+ if (!bridge.isConnected()) {
3729
+ return {
3730
+ content: [{
3731
+ type: 'text',
3732
+ text: JSON.stringify({
3733
+ error: {
3734
+ code: 'NOT_CONNECTED',
3735
+ message: 'Figma plugin is not connected.'
3736
+ }
3737
+ }, null, 2)
3738
+ }],
3739
+ isError: true
3740
+ };
3741
+ }
3742
+
3743
+ const { nodeIds, targetPageId } = args;
3744
+
3745
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
3746
+ return {
3747
+ content: [{
3748
+ type: 'text',
3749
+ text: JSON.stringify({
3750
+ error: {
3751
+ code: 'INVALID_PARAMS',
3752
+ message: 'nodeIds must be a non-empty array'
3753
+ }
3754
+ }, null, 2)
3755
+ }],
3756
+ isError: true
3757
+ };
3758
+ }
3759
+
3760
+ if (!targetPageId) {
3761
+ return {
3762
+ content: [{
3763
+ type: 'text',
3764
+ text: JSON.stringify({
3765
+ error: {
3766
+ code: 'INVALID_PARAMS',
3767
+ message: 'targetPageId is required'
3768
+ }
3769
+ }, null, 2)
3770
+ }],
3771
+ isError: true
3772
+ };
3773
+ }
3774
+
3775
+ try {
3776
+ const result = await bridge.sendCommand('move_to_page', args);
3777
+ return {
3778
+ content: [{
3779
+ type: 'text',
3780
+ text: JSON.stringify(result, null, 2)
3781
+ }]
3782
+ };
3783
+ } catch (error) {
3784
+ return {
3785
+ content: [{
3786
+ type: 'text',
3787
+ text: JSON.stringify({
3788
+ error: {
3789
+ code: error.code || 'UNKNOWN_ERROR',
3790
+ message: error.message
3791
+ }
3792
+ }, null, 2)
3793
+ }],
3794
+ isError: true
3795
+ };
3796
+ }
3797
+ }
3798
+
3799
+ // ============================================================
3800
+ // Component Instance Commands
3801
+ // ============================================================
3802
+
3803
+ /**
3804
+ * Swap an instance to use a different component
3805
+ */
3806
+ export async function handleSwapInstance(bridge, args) {
3807
+ if (!bridge.isConnected()) {
3808
+ return {
3809
+ content: [{
3810
+ type: 'text',
3811
+ text: JSON.stringify({
3812
+ error: {
3813
+ code: 'NOT_CONNECTED',
3814
+ message: 'Figma plugin is not connected.'
3815
+ }
3816
+ }, null, 2)
3817
+ }],
3818
+ isError: true
3819
+ };
3820
+ }
3821
+
3822
+ const { instanceId, newComponentId } = args;
3823
+
3824
+ if (!instanceId) {
3825
+ return {
3826
+ content: [{
3827
+ type: 'text',
3828
+ text: JSON.stringify({
3829
+ error: {
3830
+ code: 'INVALID_PARAMS',
3831
+ message: 'instanceId is required'
3832
+ }
3833
+ }, null, 2)
3834
+ }],
3835
+ isError: true
3836
+ };
3837
+ }
3838
+
3839
+ if (!newComponentId) {
3840
+ return {
3841
+ content: [{
3842
+ type: 'text',
3843
+ text: JSON.stringify({
3844
+ error: {
3845
+ code: 'INVALID_PARAMS',
3846
+ message: 'newComponentId is required'
3847
+ }
3848
+ }, null, 2)
3849
+ }],
3850
+ isError: true
3851
+ };
3852
+ }
3853
+
3854
+ try {
3855
+ const result = await bridge.sendCommand('swap_instance', args);
3856
+ return {
3857
+ content: [{
3858
+ type: 'text',
3859
+ text: JSON.stringify(result, null, 2)
3860
+ }]
3861
+ };
3862
+ } catch (error) {
3863
+ return {
3864
+ content: [{
3865
+ type: 'text',
3866
+ text: JSON.stringify({
3867
+ error: {
3868
+ code: error.code || 'UNKNOWN_ERROR',
3869
+ message: error.message
3870
+ }
3871
+ }, null, 2)
3872
+ }],
3873
+ isError: true
3874
+ };
3875
+ }
3876
+ }
3877
+
3878
+ // ============================================================
3879
+ // Additional Commands
3880
+ // ============================================================
3881
+
3882
+ /**
3883
+ * Duplicate a page - clone entire page with all contents
3884
+ */
3885
+ export async function handleDuplicatePage(bridge, args) {
3886
+ if (!bridge.isConnected()) {
3887
+ return {
3888
+ content: [{
3889
+ type: 'text',
3890
+ text: JSON.stringify({
3891
+ error: {
3892
+ code: 'NOT_CONNECTED',
3893
+ message: 'Figma plugin is not connected.'
3894
+ }
3895
+ }, null, 2)
3896
+ }],
3897
+ isError: true
3898
+ };
3899
+ }
3900
+
3901
+ const { pageId } = args;
3902
+
3903
+ if (!pageId) {
3904
+ return {
3905
+ content: [{
3906
+ type: 'text',
3907
+ text: JSON.stringify({
3908
+ error: {
3909
+ code: 'INVALID_PARAMS',
3910
+ message: 'pageId is required'
3911
+ }
3912
+ }, null, 2)
3913
+ }],
3914
+ isError: true
3915
+ };
3916
+ }
3917
+
3918
+ try {
3919
+ const result = await bridge.sendCommand('duplicate_page', args);
3920
+ return {
3921
+ content: [{
3922
+ type: 'text',
3923
+ text: JSON.stringify(result, null, 2)
3924
+ }]
3925
+ };
3926
+ } catch (error) {
3927
+ return {
3928
+ content: [{
3929
+ type: 'text',
3930
+ text: JSON.stringify({
3931
+ error: {
3932
+ code: error.code || 'UNKNOWN_ERROR',
3933
+ message: error.message
3934
+ }
3935
+ }, null, 2)
3936
+ }],
3937
+ isError: true
3938
+ };
3939
+ }
3940
+ }
3941
+
3942
+ /**
3943
+ * Set rotation on nodes
3944
+ */
3945
+ export async function handleSetRotation(bridge, args) {
3946
+ if (!bridge.isConnected()) {
3947
+ return {
3948
+ content: [{
3949
+ type: 'text',
3950
+ text: JSON.stringify({
3951
+ error: {
3952
+ code: 'NOT_CONNECTED',
3953
+ message: 'Figma plugin is not connected.'
3954
+ }
3955
+ }, null, 2)
3956
+ }],
3957
+ isError: true
3958
+ };
3959
+ }
3960
+
3961
+ const { nodeIds, rotation, pivot } = args;
3962
+
3963
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
3964
+ return {
3965
+ content: [{
3966
+ type: 'text',
3967
+ text: JSON.stringify({
3968
+ error: {
3969
+ code: 'INVALID_PARAMS',
3970
+ message: 'nodeIds must be a non-empty array'
3971
+ }
3972
+ }, null, 2)
3973
+ }],
3974
+ isError: true
3975
+ };
3976
+ }
3977
+
3978
+ if (pivot !== undefined && pivot !== 'center' && pivot !== 'top-left') {
3979
+ return invalidParams(`pivot must be "center" or "top-left", got "${pivot}"`);
3980
+ }
3981
+
3982
+ if (rotation === undefined) {
3983
+ return {
3984
+ content: [{
3985
+ type: 'text',
3986
+ text: JSON.stringify({
3987
+ error: {
3988
+ code: 'INVALID_PARAMS',
3989
+ message: 'rotation is required'
3990
+ }
3991
+ }, null, 2)
3992
+ }],
3993
+ isError: true
3994
+ };
3995
+ }
3996
+
3997
+ try {
3998
+ const result = await bridge.sendCommand('set_rotation', args);
3999
+ return {
4000
+ content: [{
4001
+ type: 'text',
4002
+ text: JSON.stringify(result, null, 2)
4003
+ }]
4004
+ };
4005
+ } catch (error) {
4006
+ return {
4007
+ content: [{
4008
+ type: 'text',
4009
+ text: JSON.stringify({
4010
+ error: {
4011
+ code: error.code || 'UNKNOWN_ERROR',
4012
+ message: error.message
4013
+ }
4014
+ }, null, 2)
4015
+ }],
4016
+ isError: true
4017
+ };
4018
+ }
4019
+ }
4020
+
4021
+ /**
4022
+ * Set layout grids on a frame
4023
+ */
4024
+ export async function handleSetLayoutGrids(bridge, args) {
4025
+ if (!bridge.isConnected()) {
4026
+ return {
4027
+ content: [{
4028
+ type: 'text',
4029
+ text: JSON.stringify({
4030
+ error: {
4031
+ code: 'NOT_CONNECTED',
4032
+ message: 'Figma plugin is not connected.'
4033
+ }
4034
+ }, null, 2)
4035
+ }],
4036
+ isError: true
4037
+ };
4038
+ }
4039
+
4040
+ const { nodeId, layoutGrids } = args;
4041
+
4042
+ if (!nodeId) {
4043
+ return {
4044
+ content: [{
4045
+ type: 'text',
4046
+ text: JSON.stringify({
4047
+ error: {
4048
+ code: 'INVALID_PARAMS',
4049
+ message: 'nodeId is required'
4050
+ }
4051
+ }, null, 2)
4052
+ }],
4053
+ isError: true
4054
+ };
4055
+ }
4056
+
4057
+ if (!layoutGrids || !Array.isArray(layoutGrids)) {
4058
+ return {
4059
+ content: [{
4060
+ type: 'text',
4061
+ text: JSON.stringify({
4062
+ error: {
4063
+ code: 'INVALID_PARAMS',
4064
+ message: 'layoutGrids must be an array'
4065
+ }
4066
+ }, null, 2)
4067
+ }],
4068
+ isError: true
4069
+ };
4070
+ }
4071
+
4072
+ try {
4073
+ const result = await bridge.sendCommand('set_layout_grids', args);
4074
+ return {
4075
+ content: [{
4076
+ type: 'text',
4077
+ text: JSON.stringify(result, null, 2)
4078
+ }]
4079
+ };
4080
+ } catch (error) {
4081
+ return {
4082
+ content: [{
4083
+ type: 'text',
4084
+ text: JSON.stringify({
4085
+ error: {
4086
+ code: error.code || 'UNKNOWN_ERROR',
4087
+ message: error.message
4088
+ }
4089
+ }, null, 2)
4090
+ }],
4091
+ isError: true
4092
+ };
4093
+ }
4094
+ }
4095
+
4096
+ // ============================================================
4097
+ // FigJam handlers
4098
+ // ============================================================
4099
+
4100
+ /**
4101
+ * Standard MCP-shaped wrapper: connection guard + sendCommand + error handling.
4102
+ * Thin handlers that have no extra param validation use this directly.
4103
+ */
4104
+ async function runCommand(bridge, command, args) {
4105
+ if (!bridge.isConnected()) {
4106
+ return {
4107
+ content: [{
4108
+ type: 'text',
4109
+ text: JSON.stringify({
4110
+ error: { code: 'NOT_CONNECTED', message: 'Figma plugin is not connected.' }
4111
+ }, null, 2)
4112
+ }],
4113
+ isError: true
4114
+ };
4115
+ }
4116
+ try {
4117
+ const result = await bridge.sendCommand(command, args);
4118
+ return {
4119
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
4120
+ };
4121
+ } catch (error) {
4122
+ return {
4123
+ content: [{
4124
+ type: 'text',
4125
+ text: JSON.stringify({
4126
+ error: { code: error.code || 'UNKNOWN_ERROR', message: error.message }
4127
+ }, null, 2)
4128
+ }],
4129
+ isError: true
4130
+ };
4131
+ }
4132
+ }
4133
+
4134
+ export async function handleCreateSticky(bridge, args) {
4135
+ return runCommand(bridge, 'create_sticky', args);
4136
+ }
4137
+
4138
+ export async function handleSetSticky(bridge, args) {
4139
+ return runCommand(bridge, 'set_sticky', args);
4140
+ }
4141
+
4142
+ export async function handleCreateShapeWithText(bridge, args) {
4143
+ return runCommand(bridge, 'create_shape_with_text', args);
4144
+ }
4145
+
4146
+ export async function handleSetShapeType(bridge, args) {
4147
+ return runCommand(bridge, 'set_shape_type', args);
4148
+ }
4149
+
4150
+ export async function handleCreateConnector(bridge, args) {
4151
+ return runCommand(bridge, 'create_connector', args);
4152
+ }
4153
+
4154
+ export async function handleSetConnector(bridge, args) {
4155
+ return runCommand(bridge, 'set_connector', args);
4156
+ }
4157
+
4158
+ export async function handleCreateSection(bridge, args) {
4159
+ return runCommand(bridge, 'create_section', args);
4160
+ }
4161
+
4162
+ export async function handleSetSection(bridge, args) {
4163
+ return runCommand(bridge, 'set_section', args);
4164
+ }
4165
+
4166
+ export async function handleCreateTable(bridge, args) {
4167
+ return runCommand(bridge, 'create_table', args);
4168
+ }
4169
+
4170
+ export async function handleSetTableCell(bridge, args) {
4171
+ return runCommand(bridge, 'set_table_cell', args);
4172
+ }
4173
+
4174
+ export async function handleInsertTableRow(bridge, args) {
4175
+ return runCommand(bridge, 'insert_table_row', args);
4176
+ }
4177
+
4178
+ export async function handleInsertTableColumn(bridge, args) {
4179
+ return runCommand(bridge, 'insert_table_column', args);
4180
+ }
4181
+
4182
+ export async function handleRemoveTableRow(bridge, args) {
4183
+ return runCommand(bridge, 'remove_table_row', args);
4184
+ }
4185
+
4186
+ export async function handleRemoveTableColumn(bridge, args) {
4187
+ return runCommand(bridge, 'remove_table_column', args);
4188
+ }
4189
+
4190
+ export async function handleResizeTableRow(bridge, args) {
4191
+ return runCommand(bridge, 'resize_table_row', args);
4192
+ }
4193
+
4194
+ export async function handleResizeTableColumn(bridge, args) {
4195
+ return runCommand(bridge, 'resize_table_column', args);
4196
+ }
4197
+
4198
+ export async function handleMoveTableRow(bridge, args) {
4199
+ return runCommand(bridge, 'move_table_row', args);
4200
+ }
4201
+
4202
+ export async function handleMoveTableColumn(bridge, args) {
4203
+ return runCommand(bridge, 'move_table_column', args);
4204
+ }
4205
+
4206
+ export async function handleCreateCodeBlock(bridge, args) {
4207
+ return runCommand(bridge, 'create_code_block', args);
4208
+ }
4209
+
4210
+ export async function handleSetCodeBlock(bridge, args) {
4211
+ return runCommand(bridge, 'set_code_block', args);
4212
+ }
4213
+
4214
+ export async function handleCreateLinkPreview(bridge, args) {
4215
+ return runCommand(bridge, 'create_link_preview', args);
4216
+ }
4217
+
4218
+ /**
4219
+ * Combine components as variants into a component set
4220
+ */
4221
+ export async function handleCombineAsVariants(bridge, args) {
4222
+ if (!bridge.isConnected()) {
4223
+ return {
4224
+ content: [{
4225
+ type: 'text',
4226
+ text: JSON.stringify({
4227
+ error: {
4228
+ code: 'NOT_CONNECTED',
4229
+ message: 'Figma plugin is not connected.'
4230
+ }
4231
+ }, null, 2)
4232
+ }],
4233
+ isError: true
4234
+ };
4235
+ }
4236
+
4237
+ const { componentIds } = args;
4238
+
4239
+ if (!componentIds || !Array.isArray(componentIds) || componentIds.length < 2) {
4240
+ return {
4241
+ content: [{
4242
+ type: 'text',
4243
+ text: JSON.stringify({
4244
+ error: {
4245
+ code: 'INVALID_PARAMS',
4246
+ message: 'componentIds must be an array with at least 2 component IDs'
4247
+ }
4248
+ }, null, 2)
4249
+ }],
4250
+ isError: true
4251
+ };
4252
+ }
4253
+
4254
+ try {
4255
+ const result = await bridge.sendCommand('combine_as_variants', args);
4256
+ return {
4257
+ content: [{
4258
+ type: 'text',
4259
+ text: JSON.stringify(result, null, 2)
4260
+ }]
4261
+ };
4262
+ } catch (error) {
4263
+ return {
4264
+ content: [{
4265
+ type: 'text',
4266
+ text: JSON.stringify({
4267
+ error: {
4268
+ code: error.code || 'UNKNOWN_ERROR',
4269
+ message: error.message
4270
+ }
4271
+ }, null, 2)
4272
+ }],
4273
+ isError: true
4274
+ };
4275
+ }
4276
+ }
4277
+
4278
+ // ============================================================
4279
+ // Prototype tools
4280
+ // ============================================================
4281
+
4282
+ export async function handleGetReactions(bridge, args) {
4283
+ return runCommand(bridge, 'get_reactions', args);
4284
+ }
4285
+
4286
+ export async function handleAddReaction(bridge, args) {
4287
+ return runCommand(bridge, 'add_reaction', args);
4288
+ }
4289
+
4290
+ export async function handleRemoveReaction(bridge, args) {
4291
+ return runCommand(bridge, 'remove_reaction', args);
4292
+ }
4293
+
4294
+ export async function handleSetFlowStartingPoint(bridge, args) {
4295
+ return runCommand(bridge, 'set_flow_starting_point', args);
4296
+ }
4297
+
4298
+ // ============================================================
4299
+ // Visibility, clipping, style deletion, variable modes
4300
+ // ============================================================
4301
+
4302
+ /**
4303
+ * Invalid-params response in the standard MCP shape
4304
+ */
4305
+ function invalidParams(message) {
4306
+ return {
4307
+ content: [{
4308
+ type: 'text',
4309
+ text: JSON.stringify({
4310
+ error: { code: 'INVALID_PARAMS', message }
4311
+ }, null, 2)
4312
+ }],
4313
+ isError: true
4314
+ };
4315
+ }
4316
+
4317
+ /**
4318
+ * Show or hide nodes
4319
+ */
4320
+ export async function handleSetVisible(bridge, args) {
4321
+ const { nodeIds } = args;
4322
+
4323
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
4324
+ return invalidParams('nodeIds must be a non-empty array of node IDs');
4325
+ }
4326
+
4327
+ return runCommand(bridge, 'set_visible', args);
4328
+ }
4329
+
4330
+ /**
4331
+ * Set clipsContent on frame-like nodes
4332
+ */
4333
+ export async function handleSetClipsContent(bridge, args) {
4334
+ const { nodeIds } = args;
4335
+
4336
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
4337
+ return invalidParams('nodeIds must be a non-empty array of node IDs');
4338
+ }
4339
+
4340
+ return runCommand(bridge, 'set_clips_content', args);
4341
+ }
4342
+
4343
+ /**
4344
+ * Set or clear min/max size limits on nodes.
4345
+ *
4346
+ * `null` is a meaningful value here (it clears the limit), so the presence check
4347
+ * uses hasOwnProperty rather than a truthiness test.
4348
+ */
4349
+ export async function handleSetSizeLimits(bridge, args) {
4350
+ const { nodeIds } = args;
4351
+
4352
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
4353
+ return invalidParams('nodeIds must be a non-empty array of node IDs');
4354
+ }
4355
+
4356
+ const limitFields = ['minWidth', 'maxWidth', 'minHeight', 'maxHeight'];
4357
+ const provided = limitFields.filter(
4358
+ (field) => Object.prototype.hasOwnProperty.call(args, field) && args[field] !== undefined
4359
+ );
4360
+
4361
+ if (provided.length === 0) {
4362
+ return invalidParams(
4363
+ 'At least one of minWidth, maxWidth, minHeight, maxHeight must be provided. ' +
4364
+ 'Pass a number to set a limit, or null to clear it.'
4365
+ );
4366
+ }
4367
+
4368
+ for (const field of provided) {
4369
+ const value = args[field];
4370
+ if (value !== null && (typeof value !== 'number' || !(value > 0))) {
4371
+ return invalidParams(`${field} must be a positive number, or null to clear it (got ${JSON.stringify(value)})`);
4372
+ }
4373
+ }
4374
+
4375
+ const result = await runCommand(bridge, 'set_size_limits', args);
4376
+ if (result.isError) return result;
4377
+
4378
+ // The plugin verifies every write by readback; a limit that did not land comes
4379
+ // back as success: false with an errors array.
4380
+ try {
4381
+ const parsed = JSON.parse(result.content[0].text);
4382
+ if (parsed && parsed.success === false) {
4383
+ return Object.assign({}, result, { isError: true });
4384
+ }
4385
+ } catch (_) {
4386
+ // Non-JSON payload — leave the response as-is.
4387
+ }
4388
+ return result;
4389
+ }
4390
+
4391
+ /**
4392
+ * Delete a local style
4393
+ */
4394
+ export async function handleDeleteStyle(bridge, args) {
4395
+ const { styleId } = args;
4396
+
4397
+ if (!styleId) {
4398
+ return invalidParams('styleId is required');
4399
+ }
4400
+
4401
+ return runCommand(bridge, 'delete_style', args);
4402
+ }
4403
+
4404
+ /**
4405
+ * Pin or unpin an explicit variable mode on nodes/pages
4406
+ */
4407
+ export async function handleSetVariableMode(bridge, args) {
4408
+ const { nodeIds, collectionId, modeId, clear } = args;
4409
+
4410
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
4411
+ return invalidParams('nodeIds must be a non-empty array of node or page IDs');
4412
+ }
4413
+
4414
+ if (!collectionId) {
4415
+ return invalidParams('collectionId is required');
4416
+ }
4417
+
4418
+ if (!clear && !modeId) {
4419
+ return invalidParams('modeId is required unless clear is true');
4420
+ }
4421
+
4422
+ return runCommand(bridge, 'set_variable_mode', args);
4423
+ }