conductor-figma 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/conductor.js CHANGED
@@ -10,17 +10,18 @@ if (args.includes('--help') || args.includes('-h')) {
10
10
  ⊞ CONDUCTOR — Design-intelligent MCP server for Figma
11
11
 
12
12
  Usage:
13
- conductor-figma Start MCP server + Figma relay
13
+ conductor-figma Start MCP server + orchestrator
14
14
  conductor-figma --port 9800 Set WebSocket port (default: 9800)
15
15
  conductor-figma --list List all ${TOOLS.length} tools
16
16
  conductor-figma --categories Show tool categories
17
17
  conductor-figma --help Show this help
18
18
 
19
19
  How it works:
20
- 1. Cursor sends tool calls via MCP (stdio)
21
- 2. Design tools (color, type, spacing) resolve locally
22
- 3. Figma tools forward through WebSocket to the plugin
23
- 4. Plugin executes on canvas, results flow back
20
+ 1. You say "create a pricing page" in Cursor
21
+ 2. CONDUCTOR generates a blueprint (30-50 commands)
22
+ 3. Each command executes sequentially on your Figma canvas
23
+ 4. Frames, text, cards, buttons appear in real-time
24
+ 5. All auto-layout. All grid-aligned. All design-intelligent.
24
25
 
25
26
  Setup:
26
27
  ~/.cursor/mcp.json:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-figma",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Design-intelligent MCP server for Figma. 61 tools across 10 categories. 8px grid, type scale ratios, auto-layout, component reuse, accessibility — real design intelligence, not shape proxying.",
5
5
  "author": "0xDragoon",
6
6
  "license": "MIT",
@@ -0,0 +1,656 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — Blueprints
3
+ // ═══════════════════════════════════════════
4
+ // Each blueprint generates a complete command sequence.
5
+ // Commands reference previous results via '$N.id' tokens.
6
+ // All values are grid-aligned and design-intelligent.
7
+
8
+ import { snapToGrid, generateTypeScale, generateSemanticColors, generatePalette } from './design/intelligence.js';
9
+
10
+ // ─── Helpers ───
11
+
12
+ function frame(name, opts) {
13
+ return {
14
+ type: 'create_frame',
15
+ data: Object.assign({
16
+ name: name,
17
+ direction: 'VERTICAL',
18
+ padding: 0,
19
+ gap: 0,
20
+ fill: '#0f0f1a',
21
+ cornerRadius: 0,
22
+ }, opts),
23
+ };
24
+ }
25
+
26
+ function hframe(name, opts) {
27
+ return frame(name, Object.assign({ direction: 'HORIZONTAL' }, opts));
28
+ }
29
+
30
+ function text(content, opts) {
31
+ return {
32
+ type: 'create_text',
33
+ data: Object.assign({
34
+ text: content,
35
+ fontSize: 16,
36
+ color: '#ffffff',
37
+ fontName: { family: 'Inter', style: 'Regular' },
38
+ }, opts),
39
+ };
40
+ }
41
+
42
+ function rect(name, opts) {
43
+ return {
44
+ type: 'create_rect',
45
+ data: Object.assign({ name: name, width: 100, height: 100 }, opts),
46
+ };
47
+ }
48
+
49
+ function appendTo(parentRef, cmd) {
50
+ cmd.data.parentId = parentRef;
51
+ return cmd;
52
+ }
53
+
54
+ // ═══════════════════════════════════════════
55
+ // PAGE BLUEPRINTS
56
+ // ═══════════════════════════════════════════
57
+
58
+ export function buildLandingPage(args) {
59
+ var brandColor = args.brandColor || '#6366f1';
60
+ var colors = generateSemanticColors(brandColor);
61
+ var pageWidth = args.width || 1440;
62
+ var dark = args.darkMode !== false;
63
+ var bg = dark ? '#0f0f1a' : '#ffffff';
64
+ var textColor = dark ? '#ffffff' : '#111111';
65
+ var mutedColor = dark ? '#888899' : '#666677';
66
+ var cardBg = dark ? '#16162e' : '#f5f5f7';
67
+ var title = args.title || 'Ship faster with less overhead';
68
+ var subtitle = args.subtitle || 'The modern platform for teams that move fast. Everything you need to build, deploy, and scale.';
69
+ var ctaText = args.ctaText || 'Get started free';
70
+ var navItems = args.navItems || ['Features', 'Pricing', 'Docs', 'Blog'];
71
+ var features = args.features || [
72
+ { icon: '⚡', title: 'Instant Deploy', desc: 'Push to deploy in seconds. Zero config. Automatic HTTPS and global CDN.' },
73
+ { icon: '📈', title: 'Auto Scale', desc: 'Scales to millions automatically. Pay only for what you use.' },
74
+ { icon: '🔒', title: 'Enterprise Security', desc: 'SOC 2 compliant. End-to-end encryption. SSO and RBAC built in.' },
75
+ ];
76
+ var stats = args.stats || [
77
+ { value: '10,000+', label: 'Teams worldwide' },
78
+ { value: '99.9%', label: 'Uptime SLA' },
79
+ { value: '< 50ms', label: 'Global latency' },
80
+ ];
81
+
82
+ var cmds = [];
83
+
84
+ // 0: Page root
85
+ cmds.push(frame('Landing Page', { width: pageWidth, height: 2000, fill: bg, gap: 0 }));
86
+
87
+ // 1: Nav
88
+ cmds.push(appendTo('$0.id', hframe('Navigation', {
89
+ width: pageWidth, height: 64, padding: 24, gap: 16, fill: bg,
90
+ counterAxisAlignItems: 'CENTER',
91
+ })));
92
+
93
+ // 2: Logo
94
+ cmds.push(appendTo('$1.id', text(args.brand || 'acme', {
95
+ fontSize: 18, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
96
+ })));
97
+
98
+ // 3: Nav spacer
99
+ cmds.push(appendTo('$1.id', frame('Spacer', {
100
+ width: 1, height: 1, fill: bg,
101
+ primaryAxisSizingMode: 'FILL',
102
+ })));
103
+
104
+ // 4-N: Nav links
105
+ for (var i = 0; i < navItems.length; i++) {
106
+ cmds.push(appendTo('$1.id', text(navItems[i], {
107
+ fontSize: 14, color: mutedColor, fontName: { family: 'Inter', style: 'Medium' },
108
+ })));
109
+ }
110
+
111
+ // Nav CTA button frame
112
+ var navBtnIdx = cmds.length;
113
+ cmds.push(appendTo('$1.id', hframe('Nav CTA', {
114
+ width: 120, height: 36, padding: 12, gap: 0,
115
+ fill: brandColor, cornerRadius: 8,
116
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
117
+ })));
118
+ cmds.push(appendTo('$' + navBtnIdx + '.id', text('Sign up', {
119
+ fontSize: 13, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
120
+ })));
121
+
122
+ // Hero section
123
+ var heroIdx = cmds.length;
124
+ cmds.push(appendTo('$0.id', frame('Hero Section', {
125
+ width: pageWidth, height: 520, padding: 80, gap: 24, fill: bg,
126
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
127
+ })));
128
+
129
+ // Hero overline
130
+ cmds.push(appendTo('$' + heroIdx + '.id', text('INTRODUCING ' + (args.brand || 'ACME').toUpperCase(), {
131
+ fontSize: 12, color: brandColor, fontName: { family: 'Inter', style: 'Semi Bold' },
132
+ })));
133
+
134
+ // Hero heading
135
+ cmds.push(appendTo('$' + heroIdx + '.id', text(title, {
136
+ fontSize: 56, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
137
+ textAlignHorizontal: 'CENTER',
138
+ })));
139
+
140
+ // Hero subtitle
141
+ cmds.push(appendTo('$' + heroIdx + '.id', text(subtitle, {
142
+ fontSize: 18, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
143
+ textAlignHorizontal: 'CENTER',
144
+ })));
145
+
146
+ // Hero button row
147
+ var btnRowIdx = cmds.length;
148
+ cmds.push(appendTo('$' + heroIdx + '.id', hframe('Hero Buttons', {
149
+ width: 340, height: 48, gap: 12, fill: bg,
150
+ primaryAxisAlignItems: 'CENTER',
151
+ })));
152
+
153
+ // Primary CTA
154
+ var primaryBtnIdx = cmds.length;
155
+ cmds.push(appendTo('$' + btnRowIdx + '.id', hframe('Primary CTA', {
156
+ width: 160, height: 48, padding: 16, fill: brandColor, cornerRadius: 10,
157
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
158
+ })));
159
+ cmds.push(appendTo('$' + primaryBtnIdx + '.id', text(ctaText, {
160
+ fontSize: 15, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
161
+ })));
162
+
163
+ // Secondary CTA
164
+ var secBtnIdx = cmds.length;
165
+ cmds.push(appendTo('$' + btnRowIdx + '.id', hframe('Secondary CTA', {
166
+ width: 160, height: 48, padding: 16, fill: dark ? '#1a1a2e' : '#eeeeee', cornerRadius: 10,
167
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
168
+ })));
169
+ cmds.push(appendTo('$' + secBtnIdx + '.id', text('View demo →', {
170
+ fontSize: 15, color: dark ? '#ccccdd' : '#333333', fontName: { family: 'Inter', style: 'Medium' },
171
+ })));
172
+
173
+ // Stats bar
174
+ var statsIdx = cmds.length;
175
+ cmds.push(appendTo('$0.id', hframe('Stats Bar', {
176
+ width: pageWidth, height: 120, padding: 48, gap: 80, fill: dark ? '#13132a' : '#f8f8fa',
177
+ counterAxisAlignItems: 'CENTER',
178
+ })));
179
+
180
+ for (var s = 0; s < stats.length; s++) {
181
+ var statIdx = cmds.length;
182
+ cmds.push(appendTo('$' + statsIdx + '.id', frame('Stat ' + (s + 1), {
183
+ width: 200, height: 64, gap: 4, fill: dark ? '#13132a' : '#f8f8fa',
184
+ counterAxisAlignItems: 'CENTER',
185
+ })));
186
+ cmds.push(appendTo('$' + statIdx + '.id', text(stats[s].value, {
187
+ fontSize: 32, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
188
+ })));
189
+ cmds.push(appendTo('$' + statIdx + '.id', text(stats[s].label, {
190
+ fontSize: 13, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
191
+ })));
192
+ }
193
+
194
+ // Features section
195
+ var featSectionIdx = cmds.length;
196
+ cmds.push(appendTo('$0.id', frame('Features Section', {
197
+ width: pageWidth, padding: 64, gap: 32, fill: bg,
198
+ counterAxisAlignItems: 'CENTER',
199
+ })));
200
+
201
+ cmds.push(appendTo('$' + featSectionIdx + '.id', text('Everything you need', {
202
+ fontSize: 36, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
203
+ textAlignHorizontal: 'CENTER',
204
+ })));
205
+
206
+ cmds.push(appendTo('$' + featSectionIdx + '.id', text('Powerful features to help your team ship faster and with more confidence.', {
207
+ fontSize: 16, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
208
+ textAlignHorizontal: 'CENTER',
209
+ })));
210
+
211
+ // Feature card row
212
+ var cardRowIdx = cmds.length;
213
+ cmds.push(appendTo('$' + featSectionIdx + '.id', hframe('Feature Cards', {
214
+ width: pageWidth - 128, gap: 20, fill: bg,
215
+ })));
216
+
217
+ for (var f = 0; f < features.length; f++) {
218
+ var cardIdx = cmds.length;
219
+ var cardWidth = Math.floor((pageWidth - 128 - (features.length - 1) * 20) / features.length);
220
+ cmds.push(appendTo('$' + cardRowIdx + '.id', frame(features[f].title, {
221
+ width: cardWidth, height: 200, padding: 24, gap: 12, fill: cardBg, cornerRadius: 14,
222
+ })));
223
+ cmds.push(appendTo('$' + cardIdx + '.id', text(features[f].icon + ' ' + features[f].title, {
224
+ fontSize: 18, color: textColor, fontName: { family: 'Inter', style: 'Semi Bold' },
225
+ })));
226
+ cmds.push(appendTo('$' + cardIdx + '.id', text(features[f].desc, {
227
+ fontSize: 14, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
228
+ })));
229
+ }
230
+
231
+ // CTA section
232
+ var ctaSectionIdx = cmds.length;
233
+ cmds.push(appendTo('$0.id', frame('CTA Section', {
234
+ width: pageWidth, padding: 80, gap: 24, fill: dark ? '#0a0a14' : '#f0f0f2',
235
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
236
+ })));
237
+
238
+ cmds.push(appendTo('$' + ctaSectionIdx + '.id', text('Ready to get started?', {
239
+ fontSize: 40, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
240
+ textAlignHorizontal: 'CENTER',
241
+ })));
242
+
243
+ cmds.push(appendTo('$' + ctaSectionIdx + '.id', text('Join thousands of teams already shipping faster.', {
244
+ fontSize: 16, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
245
+ textAlignHorizontal: 'CENTER',
246
+ })));
247
+
248
+ var ctaBtnIdx = cmds.length;
249
+ cmds.push(appendTo('$' + ctaSectionIdx + '.id', hframe('CTA Button', {
250
+ width: 200, height: 52, padding: 16, fill: brandColor, cornerRadius: 12,
251
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
252
+ })));
253
+ cmds.push(appendTo('$' + ctaBtnIdx + '.id', text(ctaText, {
254
+ fontSize: 16, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
255
+ })));
256
+
257
+ // Footer
258
+ var footerIdx = cmds.length;
259
+ cmds.push(appendTo('$0.id', hframe('Footer', {
260
+ width: pageWidth, height: 80, padding: 24, gap: 16, fill: dark ? '#080812' : '#f5f5f7',
261
+ counterAxisAlignItems: 'CENTER',
262
+ })));
263
+
264
+ cmds.push(appendTo('$' + footerIdx + '.id', text('© 2025 ' + (args.brand || 'Acme') + '. All rights reserved.', {
265
+ fontSize: 13, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
266
+ })));
267
+
268
+ return {
269
+ commands: cmds,
270
+ description: 'Landing page with nav, hero, stats, features (' + features.length + ' cards), CTA, and footer. ' + cmds.length + ' elements, all auto-layout, ' + (dark ? 'dark' : 'light') + ' theme.',
271
+ };
272
+ }
273
+
274
+ // ═══════════════════════════════════════════
275
+ // PRICING PAGE
276
+ // ═══════════════════════════════════════════
277
+
278
+ export function buildPricingPage(args) {
279
+ var brandColor = args.brandColor || '#6366f1';
280
+ var pageWidth = args.width || 1440;
281
+ var bg = '#0f0f1a';
282
+ var textColor = '#ffffff';
283
+ var mutedColor = '#888899';
284
+ var cardBg = '#16162e';
285
+ var tiers = args.tiers || [
286
+ { name: 'Starter', price: '$0', period: '/mo', desc: 'For individuals and small projects', features: ['1 project', '100 API calls/day', 'Community support', 'Basic analytics'], cta: 'Start free', highlighted: false },
287
+ { name: 'Pro', price: '$29', period: '/mo', desc: 'For growing teams that need more', features: ['Unlimited projects', '10,000 API calls/day', 'Priority support', 'Advanced analytics', 'Team collaboration', 'Custom domains'], cta: 'Start free trial', highlighted: true },
288
+ { name: 'Enterprise', price: 'Custom', period: '', desc: 'For large organizations', features: ['Everything in Pro', 'Unlimited API calls', 'Dedicated support', 'SSO & SAML', 'SLA guarantee', 'Custom integrations', 'On-premise option'], cta: 'Contact sales', highlighted: false },
289
+ ];
290
+
291
+ var cmds = [];
292
+
293
+ // 0: Page
294
+ cmds.push(frame('Pricing Page', { width: pageWidth, height: 1400, fill: bg, gap: 0 }));
295
+
296
+ // 1: Header
297
+ var headerIdx = cmds.length;
298
+ cmds.push(appendTo('$0.id', frame('Pricing Header', {
299
+ width: pageWidth, padding: 80, gap: 16, fill: bg,
300
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
301
+ })));
302
+
303
+ cmds.push(appendTo('$' + headerIdx + '.id', text('Simple, transparent pricing', {
304
+ fontSize: 48, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
305
+ textAlignHorizontal: 'CENTER',
306
+ })));
307
+
308
+ cmds.push(appendTo('$' + headerIdx + '.id', text('No hidden fees. No surprises. Cancel anytime.', {
309
+ fontSize: 18, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
310
+ textAlignHorizontal: 'CENTER',
311
+ })));
312
+
313
+ // Tier cards row
314
+ var tierRowIdx = cmds.length;
315
+ cmds.push(appendTo('$0.id', hframe('Pricing Tiers', {
316
+ width: pageWidth, padding: 48, gap: 20, fill: bg,
317
+ primaryAxisAlignItems: 'CENTER',
318
+ })));
319
+
320
+ for (var t = 0; t < tiers.length; t++) {
321
+ var tier = tiers[t];
322
+ var cardW = Math.min(380, Math.floor((pageWidth - 96 - (tiers.length - 1) * 20) / tiers.length));
323
+ var isHighlighted = tier.highlighted;
324
+
325
+ var tierIdx = cmds.length;
326
+ cmds.push(appendTo('$' + tierRowIdx + '.id', frame(tier.name + ' Tier', {
327
+ width: cardW, padding: 32, gap: 16, cornerRadius: 16,
328
+ fill: isHighlighted ? '#1e1e40' : cardBg,
329
+ })));
330
+
331
+ if (isHighlighted) {
332
+ var badgeIdx = cmds.length;
333
+ cmds.push(appendTo('$' + tierIdx + '.id', hframe('Popular Badge', {
334
+ width: 100, height: 24, padding: 6, fill: brandColor, cornerRadius: 6,
335
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
336
+ })));
337
+ cmds.push(appendTo('$' + badgeIdx + '.id', text('POPULAR', {
338
+ fontSize: 10, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' },
339
+ })));
340
+ }
341
+
342
+ cmds.push(appendTo('$' + tierIdx + '.id', text(tier.name, {
343
+ fontSize: 20, color: textColor, fontName: { family: 'Inter', style: 'Semi Bold' },
344
+ })));
345
+
346
+ // Price row
347
+ var priceRowIdx = cmds.length;
348
+ cmds.push(appendTo('$' + tierIdx + '.id', hframe('Price', {
349
+ gap: 2, fill: isHighlighted ? '#1e1e40' : cardBg,
350
+ counterAxisAlignItems: 'BASELINE',
351
+ })));
352
+ cmds.push(appendTo('$' + priceRowIdx + '.id', text(tier.price, {
353
+ fontSize: 40, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
354
+ })));
355
+ if (tier.period) {
356
+ cmds.push(appendTo('$' + priceRowIdx + '.id', text(tier.period, {
357
+ fontSize: 16, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
358
+ })));
359
+ }
360
+
361
+ cmds.push(appendTo('$' + tierIdx + '.id', text(tier.desc, {
362
+ fontSize: 14, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
363
+ })));
364
+
365
+ // Divider
366
+ cmds.push(appendTo('$' + tierIdx + '.id', rect('Divider', {
367
+ width: cardW - 64, height: 1, fill: '#252540',
368
+ })));
369
+
370
+ // Features
371
+ for (var fi = 0; fi < tier.features.length; fi++) {
372
+ cmds.push(appendTo('$' + tierIdx + '.id', text('✓ ' + tier.features[fi], {
373
+ fontSize: 13, color: mutedColor, fontName: { family: 'Inter', style: 'Regular' },
374
+ })));
375
+ }
376
+
377
+ // CTA button
378
+ var tierBtnIdx = cmds.length;
379
+ cmds.push(appendTo('$' + tierIdx + '.id', hframe(tier.cta, {
380
+ height: 44, padding: 12,
381
+ fill: isHighlighted ? brandColor : 'transparent',
382
+ cornerRadius: 8,
383
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
384
+ primaryAxisSizingMode: 'FILL',
385
+ })));
386
+ cmds.push(appendTo('$' + tierBtnIdx + '.id', text(tier.cta, {
387
+ fontSize: 14, color: isHighlighted ? '#ffffff' : brandColor,
388
+ fontName: { family: 'Inter', style: 'Semi Bold' },
389
+ })));
390
+ }
391
+
392
+ return {
393
+ commands: cmds,
394
+ description: 'Pricing page with ' + tiers.length + ' tiers (' + tiers.map(function(t) { return t.name; }).join(', ') + '). ' + cmds.length + ' elements, all auto-layout.',
395
+ };
396
+ }
397
+
398
+ // ═══════════════════════════════════════════
399
+ // DASHBOARD PAGE
400
+ // ═══════════════════════════════════════════
401
+
402
+ export function buildDashboardPage(args) {
403
+ var pageWidth = args.width || 1440;
404
+ var bg = '#0f0f1a';
405
+ var cardBg = '#16162e';
406
+ var textColor = '#ffffff';
407
+ var mutedColor = '#888899';
408
+ var brandColor = args.brandColor || '#6366f1';
409
+ var metrics = args.metrics || [
410
+ { label: 'Total Revenue', value: '$48,290', change: '+12.5%', positive: true },
411
+ { label: 'Active Users', value: '2,420', change: '+8.3%', positive: true },
412
+ { label: 'Conversion', value: '3.24%', change: '-0.8%', positive: false },
413
+ { label: 'Avg. Session', value: '4m 32s', change: '+15.2%', positive: true },
414
+ ];
415
+
416
+ var cmds = [];
417
+
418
+ // 0: Page
419
+ cmds.push(hframe('Dashboard', { width: pageWidth, height: 900, fill: bg, gap: 0 }));
420
+
421
+ // 1: Sidebar
422
+ var sidebarIdx = cmds.length;
423
+ cmds.push(appendTo('$0.id', frame('Sidebar', {
424
+ width: 240, height: 900, padding: 16, gap: 8, fill: '#0a0a14',
425
+ })));
426
+
427
+ // Sidebar logo
428
+ cmds.push(appendTo('$' + sidebarIdx + '.id', text('◆ Dashboard', {
429
+ fontSize: 14, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
430
+ })));
431
+
432
+ // Sidebar spacer
433
+ cmds.push(appendTo('$' + sidebarIdx + '.id', frame('Spacer', { width: 1, height: 16, fill: '#0a0a14' })));
434
+
435
+ // Sidebar nav items
436
+ var sideNavItems = ['Overview', 'Analytics', 'Customers', 'Products', 'Settings'];
437
+ for (var si = 0; si < sideNavItems.length; si++) {
438
+ var navItemIdx = cmds.length;
439
+ cmds.push(appendTo('$' + sidebarIdx + '.id', hframe(sideNavItems[si], {
440
+ height: 36, padding: 12, gap: 8, cornerRadius: 6,
441
+ fill: si === 0 ? '#1a1a30' : '#0a0a14',
442
+ primaryAxisSizingMode: 'FILL', counterAxisAlignItems: 'CENTER',
443
+ })));
444
+ cmds.push(appendTo('$' + navItemIdx + '.id', text(sideNavItems[si], {
445
+ fontSize: 13, color: si === 0 ? textColor : mutedColor,
446
+ fontName: { family: 'Inter', style: si === 0 ? 'Medium' : 'Regular' },
447
+ })));
448
+ }
449
+
450
+ // Main content area
451
+ var mainIdx = cmds.length;
452
+ cmds.push(appendTo('$0.id', frame('Main Content', {
453
+ width: pageWidth - 240, height: 900, padding: 32, gap: 24, fill: bg,
454
+ })));
455
+
456
+ // Header row
457
+ var headerIdx = cmds.length;
458
+ cmds.push(appendTo('$' + mainIdx + '.id', hframe('Header', {
459
+ gap: 16, fill: bg, counterAxisAlignItems: 'CENTER',
460
+ primaryAxisSizingMode: 'FILL',
461
+ })));
462
+ cmds.push(appendTo('$' + headerIdx + '.id', text('Overview', {
463
+ fontSize: 24, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
464
+ })));
465
+
466
+ // Metric cards row
467
+ var metricsRowIdx = cmds.length;
468
+ cmds.push(appendTo('$' + mainIdx + '.id', hframe('Metrics', {
469
+ gap: 16, fill: bg, primaryAxisSizingMode: 'FILL',
470
+ })));
471
+
472
+ for (var mi = 0; mi < metrics.length; mi++) {
473
+ var metricIdx = cmds.length;
474
+ var metricW = Math.floor((pageWidth - 240 - 64 - (metrics.length - 1) * 16) / metrics.length);
475
+ cmds.push(appendTo('$' + metricsRowIdx + '.id', frame(metrics[mi].label, {
476
+ width: metricW, padding: 20, gap: 8, fill: cardBg, cornerRadius: 12,
477
+ })));
478
+ cmds.push(appendTo('$' + metricIdx + '.id', text(metrics[mi].label, {
479
+ fontSize: 12, color: mutedColor, fontName: { family: 'Inter', style: 'Medium' },
480
+ })));
481
+ cmds.push(appendTo('$' + metricIdx + '.id', text(metrics[mi].value, {
482
+ fontSize: 28, color: textColor, fontName: { family: 'Inter', style: 'Bold' },
483
+ })));
484
+ cmds.push(appendTo('$' + metricIdx + '.id', text(metrics[mi].change, {
485
+ fontSize: 12, color: metrics[mi].positive ? '#4ade80' : '#f87171',
486
+ fontName: { family: 'Inter', style: 'Medium' },
487
+ })));
488
+ }
489
+
490
+ // Chart placeholder
491
+ var chartIdx = cmds.length;
492
+ cmds.push(appendTo('$' + mainIdx + '.id', frame('Chart Area', {
493
+ height: 300, padding: 24, gap: 12, fill: cardBg, cornerRadius: 12,
494
+ primaryAxisSizingMode: 'FILL',
495
+ })));
496
+ cmds.push(appendTo('$' + chartIdx + '.id', text('Revenue Over Time', {
497
+ fontSize: 16, color: textColor, fontName: { family: 'Inter', style: 'Semi Bold' },
498
+ })));
499
+ cmds.push(appendTo('$' + chartIdx + '.id', rect('Chart Placeholder', {
500
+ width: 900, height: 200, fill: '#1a1a30', cornerRadius: 8,
501
+ })));
502
+
503
+ return {
504
+ commands: cmds,
505
+ description: 'Dashboard with sidebar nav, header, ' + metrics.length + ' metric cards, and chart area. ' + cmds.length + ' elements, all auto-layout.',
506
+ };
507
+ }
508
+
509
+ // ═══════════════════════════════════════════
510
+ // SECTION BLUEPRINTS
511
+ // ═══════════════════════════════════════════
512
+
513
+ export function buildSection(sectionType, args) {
514
+ switch (sectionType) {
515
+ case 'hero': return buildHeroSection(args);
516
+ case 'features': return buildFeaturesSection(args);
517
+ case 'pricing': return buildPricingSection(args);
518
+ case 'cta': return buildCTASection(args);
519
+ case 'testimonials': return buildTestimonialsSection(args);
520
+ case 'faq': return buildFAQSection(args);
521
+ default: return buildHeroSection(args);
522
+ }
523
+ }
524
+
525
+ function buildHeroSection(args) {
526
+ var w = args.width || 1440;
527
+ var brandColor = args.brandColor || '#6366f1';
528
+ var cmds = [];
529
+
530
+ cmds.push(frame('Hero', { width: w, padding: 80, gap: 24, fill: '#0f0f1a', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' }));
531
+ cmds.push(appendTo('$0.id', text(args.heading || 'Build something great', { fontSize: 56, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
532
+ cmds.push(appendTo('$0.id', text(args.subheading || 'The platform for modern teams.', { fontSize: 18, color: '#888899', fontName: { family: 'Inter', style: 'Regular' }, textAlignHorizontal: 'CENTER' })));
533
+
534
+ var btnIdx = cmds.length;
535
+ cmds.push(appendTo('$0.id', hframe('CTA', { width: 180, height: 48, padding: 16, fill: brandColor, cornerRadius: 10, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' })));
536
+ cmds.push(appendTo('$' + btnIdx + '.id', text(args.ctaText || 'Get started', { fontSize: 15, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
537
+
538
+ return { commands: cmds, description: 'Hero section with heading, subtitle, and CTA.' };
539
+ }
540
+
541
+ function buildFeaturesSection(args) {
542
+ var w = args.width || 1440;
543
+ var features = args.features || [
544
+ { title: 'Fast', desc: 'Blazing fast performance.' },
545
+ { title: 'Secure', desc: 'Enterprise-grade security.' },
546
+ { title: 'Scalable', desc: 'Grows with your team.' },
547
+ ];
548
+ var cmds = [];
549
+
550
+ cmds.push(frame('Features', { width: w, padding: 64, gap: 32, fill: '#0f0f1a', counterAxisAlignItems: 'CENTER' }));
551
+ cmds.push(appendTo('$0.id', text(args.heading || 'Features', { fontSize: 36, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
552
+
553
+ var rowIdx = cmds.length;
554
+ cmds.push(appendTo('$0.id', hframe('Feature Cards', { width: w - 128, gap: 20, fill: '#0f0f1a' })));
555
+
556
+ for (var i = 0; i < features.length; i++) {
557
+ var cw = Math.floor((w - 128 - (features.length - 1) * 20) / features.length);
558
+ var ci = cmds.length;
559
+ cmds.push(appendTo('$' + rowIdx + '.id', frame(features[i].title, { width: cw, height: 160, padding: 24, gap: 10, fill: '#16162e', cornerRadius: 12 })));
560
+ cmds.push(appendTo('$' + ci + '.id', text(features[i].title, { fontSize: 18, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
561
+ cmds.push(appendTo('$' + ci + '.id', text(features[i].desc, { fontSize: 14, color: '#888899', fontName: { family: 'Inter', style: 'Regular' } })));
562
+ }
563
+
564
+ return { commands: cmds, description: 'Features section with ' + features.length + ' cards.' };
565
+ }
566
+
567
+ function buildPricingSection(args) {
568
+ return buildPricingPage(args);
569
+ }
570
+
571
+ function buildCTASection(args) {
572
+ var w = args.width || 1440;
573
+ var brandColor = args.brandColor || '#6366f1';
574
+ var cmds = [];
575
+
576
+ cmds.push(frame('CTA Section', { width: w, padding: 80, gap: 24, fill: '#0a0a14', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' }));
577
+ cmds.push(appendTo('$0.id', text(args.heading || 'Ready to start?', { fontSize: 40, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
578
+ cmds.push(appendTo('$0.id', text(args.subheading || 'Join thousands of teams.', { fontSize: 16, color: '#888899', fontName: { family: 'Inter', style: 'Regular' }, textAlignHorizontal: 'CENTER' })));
579
+
580
+ var btnIdx = cmds.length;
581
+ cmds.push(appendTo('$0.id', hframe('CTA Button', { width: 200, height: 52, padding: 16, fill: brandColor, cornerRadius: 12, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' })));
582
+ cmds.push(appendTo('$' + btnIdx + '.id', text(args.ctaText || 'Get started free', { fontSize: 16, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
583
+
584
+ return { commands: cmds, description: 'CTA section with heading, subtitle, and button.' };
585
+ }
586
+
587
+ function buildTestimonialsSection(args) {
588
+ var w = args.width || 1440;
589
+ var testimonials = args.testimonials || [
590
+ { quote: 'This product transformed how our team works. Absolutely incredible.', author: 'Sarah Chen', role: 'CTO, TechCorp' },
591
+ { quote: 'The best tool we have adopted this year. Our productivity doubled.', author: 'Marcus Rivera', role: 'VP Engineering, ScaleUp' },
592
+ ];
593
+ var cmds = [];
594
+
595
+ cmds.push(frame('Testimonials', { width: w, padding: 64, gap: 32, fill: '#0f0f1a', counterAxisAlignItems: 'CENTER' }));
596
+ cmds.push(appendTo('$0.id', text('What people say', { fontSize: 36, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
597
+
598
+ var rowIdx = cmds.length;
599
+ cmds.push(appendTo('$0.id', hframe('Testimonial Cards', { width: w - 128, gap: 20, fill: '#0f0f1a' })));
600
+
601
+ for (var i = 0; i < testimonials.length; i++) {
602
+ var tw = Math.floor((w - 128 - (testimonials.length - 1) * 20) / testimonials.length);
603
+ var ti = cmds.length;
604
+ cmds.push(appendTo('$' + rowIdx + '.id', frame('Testimonial ' + (i + 1), { width: tw, padding: 24, gap: 16, fill: '#16162e', cornerRadius: 14 })));
605
+ cmds.push(appendTo('$' + ti + '.id', text('"' + testimonials[i].quote + '"', { fontSize: 15, color: '#ccccdd', fontName: { family: 'Inter', style: 'Regular' } })));
606
+ cmds.push(appendTo('$' + ti + '.id', text(testimonials[i].author, { fontSize: 14, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
607
+ cmds.push(appendTo('$' + ti + '.id', text(testimonials[i].role, { fontSize: 12, color: '#888899', fontName: { family: 'Inter', style: 'Regular' } })));
608
+ }
609
+
610
+ return { commands: cmds, description: 'Testimonials with ' + testimonials.length + ' cards.' };
611
+ }
612
+
613
+ function buildFAQSection(args) {
614
+ var w = args.width || 1440;
615
+ var faqs = args.faqs || [
616
+ { q: 'How do I get started?', a: 'Sign up for free and follow our quick-start guide. Takes less than 5 minutes.' },
617
+ { q: 'Can I cancel anytime?', a: 'Yes. No contracts, no cancellation fees. Cancel with one click.' },
618
+ { q: 'Do you offer support?', a: 'All plans include email support. Pro and Enterprise get priority support.' },
619
+ ];
620
+ var cmds = [];
621
+
622
+ cmds.push(frame('FAQ', { width: w, padding: 64, gap: 32, fill: '#0f0f1a', counterAxisAlignItems: 'CENTER' }));
623
+ cmds.push(appendTo('$0.id', text('Frequently asked questions', { fontSize: 36, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
624
+
625
+ var listIdx = cmds.length;
626
+ cmds.push(appendTo('$0.id', frame('FAQ List', { width: Math.min(700, w - 128), gap: 12, fill: '#0f0f1a' })));
627
+
628
+ for (var i = 0; i < faqs.length; i++) {
629
+ var faqIdx = cmds.length;
630
+ cmds.push(appendTo('$' + listIdx + '.id', frame('FAQ ' + (i + 1), { padding: 20, gap: 8, fill: '#16162e', cornerRadius: 10, primaryAxisSizingMode: 'FILL' })));
631
+ cmds.push(appendTo('$' + faqIdx + '.id', text(faqs[i].q, { fontSize: 15, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
632
+ cmds.push(appendTo('$' + faqIdx + '.id', text(faqs[i].a, { fontSize: 13, color: '#888899', fontName: { family: 'Inter', style: 'Regular' } })));
633
+ }
634
+
635
+ return { commands: cmds, description: 'FAQ section with ' + faqs.length + ' items.' };
636
+ }
637
+
638
+ // ═══════════════════════════════════════════
639
+ // BLUEPRINT ROUTER
640
+ // ═══════════════════════════════════════════
641
+
642
+ export function getBlueprint(toolName, args) {
643
+ switch (toolName) {
644
+ case 'create_page':
645
+ switch (args.pageType) {
646
+ case 'landing': return buildLandingPage(args);
647
+ case 'pricing': return buildPricingPage(args);
648
+ case 'dashboard': return buildDashboardPage(args);
649
+ default: return buildLandingPage(args);
650
+ }
651
+ case 'create_section':
652
+ return buildSection(args.sectionType, args);
653
+ default:
654
+ return null;
655
+ }
656
+ }
package/src/index.js CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  export { startServer } from './server.js';
6
6
  export { Relay } from './relay.js';
7
+ export { executeSequence } from './orchestrator.js';
8
+ export { getBlueprint, buildLandingPage, buildPricingPage, buildDashboardPage, buildSection } from './blueprints.js';
7
9
  export { TOOLS, CATEGORIES, getToolByName, getToolsByCategory, getAllToolNames } from './tools/registry.js';
8
10
  export { handleTool } from './tools/handlers.js';
9
11
 
@@ -0,0 +1,100 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — Orchestrator
3
+ // ═══════════════════════════════════════════
4
+ // Takes a blueprint (sequence of commands) and executes them
5
+ // through the relay one at a time. Each command can reference
6
+ // results from previous commands via $ref tokens.
7
+ //
8
+ // Example: create a frame, then create text inside it.
9
+ // The text command references the frame's ID via $ref.
10
+ //
11
+ // { type: 'create_frame', name: 'Hero', ... } → returns { id: '5:2' }
12
+ // { type: 'create_text', parentId: '$0.id', text: ... } → parentId resolved to '5:2'
13
+
14
+ /**
15
+ * Execute a sequence of Figma commands through the relay.
16
+ * Supports $ref tokens: '$N.field' references result N's field.
17
+ *
18
+ * @param {Object} relay - The WebSocket relay instance
19
+ * @param {Array} commands - Array of { type, data } command objects
20
+ * @param {Function} onProgress - Optional callback(stepIndex, totalSteps, result)
21
+ * @returns {Promise<{ results: Array, errors: Array, success: boolean }>}
22
+ */
23
+ export async function executeSequence(relay, commands, onProgress) {
24
+ var results = [];
25
+ var errors = [];
26
+
27
+ for (var i = 0; i < commands.length; i++) {
28
+ var cmd = commands[i];
29
+ var resolvedData = resolveRefs(cmd.data || {}, results);
30
+
31
+ try {
32
+ var result = await relay.sendToPlugin(cmd.type, resolvedData, 10000);
33
+
34
+ if (result && result.error) {
35
+ errors.push({ step: i, command: cmd.type, error: result.error });
36
+ results.push(result);
37
+ // Don't stop on error — skip and continue
38
+ process.stderr.write('CONDUCTOR orchestrator: step ' + i + ' (' + cmd.type + ') error: ' + result.error + '\n');
39
+ } else {
40
+ results.push(result || {});
41
+ if (onProgress) onProgress(i, commands.length, result);
42
+ process.stderr.write('CONDUCTOR orchestrator: step ' + (i + 1) + '/' + commands.length + ' ' + cmd.type + ' -> ' + (result && (result.id || result.name) || 'ok') + '\n');
43
+ }
44
+ } catch (err) {
45
+ errors.push({ step: i, command: cmd.type, error: String(err) });
46
+ results.push({ error: String(err) });
47
+ process.stderr.write('CONDUCTOR orchestrator: step ' + i + ' (' + cmd.type + ') threw: ' + String(err) + '\n');
48
+ }
49
+
50
+ // Small delay between commands to let Figma process
51
+ await sleep(50);
52
+ }
53
+
54
+ return {
55
+ results: results,
56
+ errors: errors,
57
+ success: errors.length === 0,
58
+ totalSteps: commands.length,
59
+ completedSteps: commands.length - errors.length,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Resolve $ref tokens in command data.
65
+ * '$0.id' → results[0].id
66
+ * '$parent' → results[results.length-1].id (last result's id)
67
+ * '$2.name' → results[2].name
68
+ */
69
+ function resolveRefs(data, results) {
70
+ if (typeof data === 'string') {
71
+ // Check for $ref pattern
72
+ return data.replace(/\$(\d+)\.(\w+)/g, function(match, idx, field) {
73
+ var r = results[parseInt(idx)];
74
+ return (r && r[field] !== undefined) ? r[field] : match;
75
+ }).replace(/\$parent/g, function() {
76
+ var last = results[results.length - 1];
77
+ return (last && last.id) ? last.id : '';
78
+ });
79
+ }
80
+
81
+ if (Array.isArray(data)) {
82
+ return data.map(function(item) { return resolveRefs(item, results); });
83
+ }
84
+
85
+ if (data && typeof data === 'object') {
86
+ var resolved = {};
87
+ for (var key in data) {
88
+ if (data.hasOwnProperty(key)) {
89
+ resolved[key] = resolveRefs(data[key], results);
90
+ }
91
+ }
92
+ return resolved;
93
+ }
94
+
95
+ return data;
96
+ }
97
+
98
+ function sleep(ms) {
99
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
100
+ }
package/src/server.js CHANGED
@@ -1,20 +1,25 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — MCP Server + Relay Bridge
2
+ // CONDUCTOR — MCP Server + Orchestrator
3
3
  // ═══════════════════════════════════════════
4
- // MCP protocol over stdio (for Cursor/Claude Code).
5
- // WebSocket relay to Figma plugin (for canvas operations).
6
- //
7
- // Pure design tools (color_palette, type_scale, etc.) resolve locally.
8
- // Figma tools (create_frame, read_node, etc.) forward through WebSocket.
4
+ // When create_page or create_section is called:
5
+ // 1. Generates a blueprint (30-50 sequential commands)
6
+ // 2. Executes each one through the relay to the Figma plugin
7
+ // 3. Each command references results from previous commands ($ref)
8
+ // 4. Returns a summary of everything created
9
9
 
10
10
  import { TOOLS } from './tools/registry.js';
11
11
  import { handleTool } from './tools/handlers.js';
12
12
  import { Relay } from './relay.js';
13
+ import { getBlueprint } from './blueprints.js';
14
+ import { executeSequence } from './orchestrator.js';
13
15
 
14
- var SERVER_INFO = { name: 'conductor-figma', version: '0.2.0' };
16
+ var SERVER_INFO = { name: 'conductor-figma', version: '0.3.0' };
15
17
  var CAPABILITIES = { tools: {} };
16
18
  var relay = null;
17
19
 
20
+ // Tools that trigger blueprint orchestration (multi-command sequences)
21
+ var BLUEPRINT_TOOLS = new Set(['create_page', 'create_section']);
22
+
18
23
  export async function startServer(options) {
19
24
  options = options || {};
20
25
  var port = options.port || 9800;
@@ -23,9 +28,9 @@ export async function startServer(options) {
23
28
  var relayStarted = await relay.start();
24
29
 
25
30
  if (relayStarted) {
26
- process.stderr.write('CONDUCTOR: MCP + relay started (' + TOOLS.length + ' tools, ws://localhost:' + port + ')\n');
31
+ process.stderr.write('CONDUCTOR v0.3.0: MCP + orchestrator ready (' + TOOLS.length + ' tools, ws://localhost:' + port + ')\n');
27
32
  } else {
28
- process.stderr.write('CONDUCTOR: MCP started (' + TOOLS.length + ' tools, no relay — install ws for Figma bridge)\n');
33
+ process.stderr.write('CONDUCTOR v0.3.0: MCP ready (' + TOOLS.length + ' tools, no relay — install ws)\n');
29
34
  }
30
35
 
31
36
  var buffer = '';
@@ -89,7 +94,47 @@ async function handleMessage(msg) {
89
94
  }
90
95
 
91
96
  async function handleToolCall(id, toolName, toolArgs) {
92
- // If tool is a direct Figma command and plugin is connected — forward it
97
+
98
+ // ═══ ORCHESTRATED BLUEPRINTS ═══
99
+ // create_page and create_section generate 20-50 commands and execute them all
100
+ if (BLUEPRINT_TOOLS.has(toolName) && relay && relay.isConnected()) {
101
+ var blueprint = getBlueprint(toolName, toolArgs);
102
+
103
+ if (blueprint && blueprint.commands && blueprint.commands.length > 0) {
104
+ process.stderr.write('CONDUCTOR orchestrator: ' + toolName + ' -> ' + blueprint.commands.length + ' commands\n');
105
+ process.stderr.write('CONDUCTOR orchestrator: ' + blueprint.description + '\n');
106
+
107
+ var outcome = await executeSequence(relay, blueprint.commands);
108
+
109
+ var summary = {
110
+ tool: toolName,
111
+ description: blueprint.description,
112
+ totalCommands: outcome.totalSteps,
113
+ completed: outcome.completedSteps,
114
+ errors: outcome.errors.length,
115
+ success: outcome.success,
116
+ createdNodes: [],
117
+ };
118
+
119
+ // Collect all created node IDs and names
120
+ for (var r = 0; r < outcome.results.length; r++) {
121
+ var res = outcome.results[r];
122
+ if (res && res.id) {
123
+ summary.createdNodes.push({ id: res.id, name: res.name || '', type: res.type || '' });
124
+ }
125
+ }
126
+
127
+ if (outcome.errors.length > 0) {
128
+ summary.errorDetails = outcome.errors;
129
+ }
130
+
131
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] });
132
+ return;
133
+ }
134
+ }
135
+
136
+ // ═══ DIRECT FIGMA COMMANDS ═══
137
+ // Single commands forwarded directly to plugin
93
138
  if (relay && relay.isFigmaCommand(toolName) && relay.isConnected()) {
94
139
  process.stderr.write('CONDUCTOR: -> Figma: ' + toolName + '\n');
95
140
  try {
@@ -102,10 +147,10 @@ async function handleToolCall(id, toolName, toolArgs) {
102
147
  return;
103
148
  }
104
149
 
105
- // Run through design intelligence handler
150
+ // ═══ DESIGN INTELLIGENCE (local) ═══
106
151
  var result = handleTool(toolName, toolArgs, null);
107
152
 
108
- // Check if handler produced a Figma action we should forward
153
+ // Check if handler produced a Figma action to forward
109
154
  if (relay && relay.isConnected()) {
110
155
  try {
111
156
  var data = JSON.parse(result.content[0].text);
@@ -120,7 +165,25 @@ async function handleToolCall(id, toolName, toolArgs) {
120
165
  } catch (e) { /* not JSON or no action */ }
121
166
  }
122
167
 
123
- // If Figma command but plugin not connected — add note
168
+ // Blueprint tool but plugin not connected — return the blueprint spec
169
+ if (BLUEPRINT_TOOLS.has(toolName)) {
170
+ var bp = getBlueprint(toolName, toolArgs);
171
+ if (bp) {
172
+ var spec = {
173
+ tool: toolName,
174
+ description: bp.description,
175
+ commandCount: bp.commands.length,
176
+ _note: relay && !relay.isConnected()
177
+ ? 'Figma plugin not connected. Connect the CONDUCTOR plugin to execute these ' + bp.commands.length + ' commands on canvas.'
178
+ : 'WebSocket relay not available. Install ws package for Figma bridge.',
179
+ commands: bp.commands.map(function(c, i) { return { step: i, type: c.type, name: c.data.name || c.data.text || '' }; }),
180
+ };
181
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(spec, null, 2) }] });
182
+ return;
183
+ }
184
+ }
185
+
186
+ // Figma command but not connected — add note
124
187
  if (relay && relay.isFigmaCommand(toolName) && !relay.isConnected()) {
125
188
  try {
126
189
  var parsed = JSON.parse(result.content[0].text);