conductor-figma 0.2.0 → 0.3.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.
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.1",
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,775 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — Blueprints v2
3
+ // ═══════════════════════════════════════════
4
+ // Polished, production-grade command sequences.
5
+ // Every value is intentional. Every pixel is grid-aligned.
6
+
7
+ import { snapToGrid, generateTypeScale, generateSemanticColors, generatePalette } from './design/intelligence.js';
8
+
9
+ // ─── Helpers ───
10
+
11
+ function frame(name, opts) {
12
+ return { type: 'create_frame', data: Object.assign({ name: name, direction: 'VERTICAL', padding: 0, gap: 0, fill: '#0c0c18', cornerRadius: 0 }, opts) };
13
+ }
14
+
15
+ function hframe(name, opts) {
16
+ return frame(name, Object.assign({ direction: 'HORIZONTAL' }, opts));
17
+ }
18
+
19
+ function text(content, opts) {
20
+ return { type: 'create_text', data: Object.assign({ text: content, fontSize: 16, color: '#ffffff', fontName: { family: 'Inter', style: 'Regular' } }, opts) };
21
+ }
22
+
23
+ function rect(name, opts) {
24
+ return { type: 'create_rect', data: Object.assign({ name: name, width: 100, height: 100 }, opts) };
25
+ }
26
+
27
+ function ap(ref, cmd) { cmd.data.parentId = ref; return cmd; }
28
+
29
+ // ═══════════════════════════════════════════
30
+ // LANDING PAGE — Premium quality
31
+ // ═══════════════════════════════════════════
32
+
33
+ export function buildLandingPage(args) {
34
+ var brand = args.brandColor || '#6366f1';
35
+ var W = args.width || 1440;
36
+ var contentW = 1120;
37
+ var dark = args.darkMode !== false;
38
+
39
+ // Palette
40
+ var bg = dark ? '#09090f' : '#ffffff';
41
+ var bg2 = dark ? '#0f0f1c' : '#f9f9fb';
42
+ var bg3 = dark ? '#14142a' : '#f3f3f7';
43
+ var cardBg = dark ? '#12122a' : '#ffffff';
44
+ var cardBorder = dark ? '#1e1e3a' : '#e8e8ee';
45
+ var text1 = dark ? '#f0f0f8' : '#111118';
46
+ var text2 = dark ? '#a0a0b8' : '#55556a';
47
+ var text3 = dark ? '#686880' : '#88889a';
48
+ var divider = dark ? '#1a1a32' : '#e4e4ec';
49
+
50
+ var title = args.title || 'Ship faster with\nless overhead';
51
+ var subtitle = args.subtitle || 'The modern platform for teams that move fast.\nEverything you need to build, deploy, and scale.';
52
+ var ctaText = args.ctaText || 'Start for free';
53
+ var navItems = args.navItems || ['Features', 'Pricing', 'Docs', 'Blog'];
54
+ var features = args.features || [
55
+ { icon: '⚡', title: 'Instant Deploy', desc: 'Push to deploy in seconds. Zero config. Automatic HTTPS, global CDN, and instant rollbacks.' },
56
+ { icon: '📈', title: 'Auto Scale', desc: 'Scales to millions of requests automatically. Pay only for what you use. No capacity planning.' },
57
+ { icon: '🔒', title: 'Enterprise Security', desc: 'SOC 2 Type II compliant. End-to-end encryption. SSO, RBAC, and audit logs built in.' },
58
+ ];
59
+ var stats = args.stats || [
60
+ { value: '10,000+', label: 'Teams worldwide' },
61
+ { value: '99.9%', label: 'Uptime SLA' },
62
+ { value: '< 50ms', label: 'Global latency' },
63
+ { value: '4.9/5', label: 'Customer rating' },
64
+ ];
65
+
66
+ var cmds = [];
67
+
68
+ // ── 0: Page Root ──
69
+ cmds.push(frame('Landing Page', { width: W, fill: bg, gap: 0, primaryAxisSizingMode: 'HUG' }));
70
+
71
+ // ── 1: Navigation ──
72
+ var navIdx = cmds.length;
73
+ cmds.push(ap('$0.id', hframe('Navigation', {
74
+ width: W, height: 72, paddingLeft: 48, paddingRight: 48, paddingTop: 0, paddingBottom: 0, gap: 0, fill: bg,
75
+ counterAxisAlignItems: 'CENTER',
76
+ })));
77
+
78
+ // Nav logo
79
+ cmds.push(ap('$' + navIdx + '.id', text(args.brand || 'acme', {
80
+ fontSize: 20, color: text1, fontName: { family: 'Inter', style: 'Bold' },
81
+ })));
82
+
83
+ // Nav spacer (pushes links right)
84
+ var spacerIdx = cmds.length;
85
+ cmds.push(ap('$' + navIdx + '.id', frame('_spacer', {
86
+ width: 1, height: 1, fill: bg, primaryAxisSizingMode: 'FILL',
87
+ })));
88
+
89
+ // Nav links container
90
+ var navLinksIdx = cmds.length;
91
+ cmds.push(ap('$' + navIdx + '.id', hframe('Nav Links', {
92
+ gap: 32, fill: bg, counterAxisAlignItems: 'CENTER',
93
+ })));
94
+
95
+ for (var i = 0; i < navItems.length; i++) {
96
+ cmds.push(ap('$' + navLinksIdx + '.id', text(navItems[i], {
97
+ fontSize: 14, color: text2, fontName: { family: 'Inter', style: 'Medium' },
98
+ })));
99
+ }
100
+
101
+ // Nav CTA
102
+ var navBtnIdx = cmds.length;
103
+ cmds.push(ap('$' + navIdx + '.id', hframe('Nav CTA', {
104
+ height: 40, paddingLeft: 20, paddingRight: 20, paddingTop: 0, paddingBottom: 0,
105
+ fill: brand, cornerRadius: 8,
106
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
107
+ })));
108
+ cmds.push(ap('$' + navBtnIdx + '.id', text('Get started', {
109
+ fontSize: 14, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
110
+ })));
111
+
112
+ // ── Divider ──
113
+ cmds.push(ap('$0.id', rect('Nav Divider', { width: W, height: 1, fill: divider })));
114
+
115
+ // ── Hero Section ──
116
+ var heroIdx = cmds.length;
117
+ cmds.push(ap('$0.id', frame('Hero Section', {
118
+ width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48,
119
+ gap: 32, fill: bg,
120
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
121
+ })));
122
+
123
+ // Overline badge
124
+ var badgeIdx = cmds.length;
125
+ cmds.push(ap('$' + heroIdx + '.id', hframe('Badge', {
126
+ paddingLeft: 16, paddingRight: 16, paddingTop: 8, paddingBottom: 8,
127
+ fill: dark ? '#1a1a36' : '#f0f0ff', cornerRadius: 20,
128
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER', gap: 8,
129
+ })));
130
+ cmds.push(ap('$' + badgeIdx + '.id', text('✦', { fontSize: 10, color: brand })));
131
+ cmds.push(ap('$' + badgeIdx + '.id', text('Now available — v2.0 is here', {
132
+ fontSize: 12, color: dark ? '#b0b0d0' : '#5555aa', fontName: { family: 'Inter', style: 'Medium' },
133
+ })));
134
+
135
+ // Hero heading
136
+ cmds.push(ap('$' + heroIdx + '.id', text(title, {
137
+ fontSize: 64, color: text1, fontName: { family: 'Inter', style: 'Bold' },
138
+ textAlignHorizontal: 'CENTER',
139
+ })));
140
+
141
+ // Hero subtitle
142
+ cmds.push(ap('$' + heroIdx + '.id', text(subtitle, {
143
+ fontSize: 20, color: text2, fontName: { family: 'Inter', style: 'Regular' },
144
+ textAlignHorizontal: 'CENTER',
145
+ })));
146
+
147
+ // Hero button row
148
+ var btnRowIdx = cmds.length;
149
+ cmds.push(ap('$' + heroIdx + '.id', hframe('Hero Buttons', {
150
+ gap: 12, fill: bg,
151
+ primaryAxisAlignItems: 'CENTER',
152
+ })));
153
+
154
+ // Primary CTA
155
+ var pBtnIdx = cmds.length;
156
+ cmds.push(ap('$' + btnRowIdx + '.id', hframe('Primary CTA', {
157
+ height: 52, paddingLeft: 28, paddingRight: 28, paddingTop: 0, paddingBottom: 0,
158
+ fill: brand, cornerRadius: 12,
159
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
160
+ })));
161
+ cmds.push(ap('$' + pBtnIdx + '.id', text(ctaText, {
162
+ fontSize: 16, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
163
+ })));
164
+
165
+ // Secondary CTA
166
+ var sBtnIdx = cmds.length;
167
+ cmds.push(ap('$' + btnRowIdx + '.id', hframe('Secondary CTA', {
168
+ height: 52, paddingLeft: 28, paddingRight: 28, paddingTop: 0, paddingBottom: 0,
169
+ fill: 'transparent', cornerRadius: 12,
170
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
171
+ })));
172
+ cmds.push(ap('$' + sBtnIdx + '.id', text('See how it works →', {
173
+ fontSize: 16, color: text2, fontName: { family: 'Inter', style: 'Medium' },
174
+ })));
175
+
176
+ // ── Social proof line ──
177
+ var proofIdx = cmds.length;
178
+ cmds.push(ap('$' + heroIdx + '.id', hframe('Social Proof', {
179
+ gap: 8, fill: bg, counterAxisAlignItems: 'CENTER',
180
+ paddingTop: 16,
181
+ })));
182
+ cmds.push(ap('$' + proofIdx + '.id', text('★★★★★', {
183
+ fontSize: 14, color: '#fbbf24',
184
+ })));
185
+ cmds.push(ap('$' + proofIdx + '.id', text('Loved by 10,000+ teams', {
186
+ fontSize: 13, color: text3, fontName: { family: 'Inter', style: 'Medium' },
187
+ })));
188
+
189
+ // ── Stats Section ──
190
+ var statsIdx = cmds.length;
191
+ cmds.push(ap('$0.id', hframe('Stats Bar', {
192
+ width: W, paddingTop: 48, paddingBottom: 48, paddingLeft: 48, paddingRight: 48,
193
+ gap: 0, fill: bg2,
194
+ primaryAxisAlignItems: 'CENTER',
195
+ })));
196
+
197
+ // Stats inner container (centered, max-width)
198
+ var statsInnerIdx = cmds.length;
199
+ cmds.push(ap('$' + statsIdx + '.id', hframe('Stats Inner', {
200
+ width: contentW, gap: 0, fill: bg2,
201
+ })));
202
+
203
+ for (var s = 0; s < stats.length; s++) {
204
+ var statIdx = cmds.length;
205
+ var statW = Math.floor(contentW / stats.length);
206
+ cmds.push(ap('$' + statsInnerIdx + '.id', frame('Stat: ' + stats[s].label, {
207
+ width: statW, paddingTop: 24, paddingBottom: 24, gap: 4, fill: bg2,
208
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
209
+ })));
210
+ cmds.push(ap('$' + statIdx + '.id', text(stats[s].value, {
211
+ fontSize: 36, color: text1, fontName: { family: 'Inter', style: 'Bold' },
212
+ })));
213
+ cmds.push(ap('$' + statIdx + '.id', text(stats[s].label, {
214
+ fontSize: 14, color: text3, fontName: { family: 'Inter', style: 'Medium' },
215
+ })));
216
+ }
217
+
218
+ // ── Features Section ──
219
+ var featIdx = cmds.length;
220
+ cmds.push(ap('$0.id', frame('Features Section', {
221
+ width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48,
222
+ gap: 48, fill: bg,
223
+ counterAxisAlignItems: 'CENTER',
224
+ })));
225
+
226
+ // Features header
227
+ var featHeaderIdx = cmds.length;
228
+ cmds.push(ap('$' + featIdx + '.id', frame('Features Header', {
229
+ gap: 16, fill: bg, counterAxisAlignItems: 'CENTER',
230
+ })));
231
+ cmds.push(ap('$' + featHeaderIdx + '.id', text('Everything you need', {
232
+ fontSize: 40, color: text1, fontName: { family: 'Inter', style: 'Bold' },
233
+ textAlignHorizontal: 'CENTER',
234
+ })));
235
+ cmds.push(ap('$' + featHeaderIdx + '.id', text('Powerful features to help your team ship faster\nand with more confidence.', {
236
+ fontSize: 18, color: text2, fontName: { family: 'Inter', style: 'Regular' },
237
+ textAlignHorizontal: 'CENTER',
238
+ })));
239
+
240
+ // Feature card row
241
+ var cardRowIdx = cmds.length;
242
+ cmds.push(ap('$' + featIdx + '.id', hframe('Feature Cards', {
243
+ width: contentW, gap: 24, fill: bg,
244
+ })));
245
+
246
+ for (var f = 0; f < features.length; f++) {
247
+ var cardW = Math.floor((contentW - (features.length - 1) * 24) / features.length);
248
+ var cardIdx = cmds.length;
249
+ cmds.push(ap('$' + cardRowIdx + '.id', frame(features[f].title, {
250
+ width: cardW, paddingTop: 32, paddingBottom: 32, paddingLeft: 28, paddingRight: 28,
251
+ gap: 16, fill: cardBg, cornerRadius: 16,
252
+ })));
253
+
254
+ // Icon circle
255
+ var iconIdx = cmds.length;
256
+ cmds.push(ap('$' + cardIdx + '.id', hframe('Icon', {
257
+ width: 48, height: 48, fill: dark ? '#1c1c3a' : '#f0f0ff', cornerRadius: 12,
258
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
259
+ })));
260
+ cmds.push(ap('$' + iconIdx + '.id', text(features[f].icon, { fontSize: 20 })));
261
+
262
+ // Card title
263
+ cmds.push(ap('$' + cardIdx + '.id', text(features[f].title, {
264
+ fontSize: 20, color: text1, fontName: { family: 'Inter', style: 'Semi Bold' },
265
+ })));
266
+
267
+ // Card description
268
+ cmds.push(ap('$' + cardIdx + '.id', text(features[f].desc, {
269
+ fontSize: 15, color: text2, fontName: { family: 'Inter', style: 'Regular' },
270
+ })));
271
+
272
+ // Learn more link
273
+ cmds.push(ap('$' + cardIdx + '.id', text('Learn more →', {
274
+ fontSize: 14, color: brand, fontName: { family: 'Inter', style: 'Medium' },
275
+ })));
276
+ }
277
+
278
+ // ── CTA Section ──
279
+ var ctaSectionIdx = cmds.length;
280
+ cmds.push(ap('$0.id', frame('CTA Section', {
281
+ width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48,
282
+ gap: 0, fill: bg2,
283
+ counterAxisAlignItems: 'CENTER',
284
+ })));
285
+
286
+ // CTA card
287
+ var ctaCardIdx = cmds.length;
288
+ cmds.push(ap('$' + ctaSectionIdx + '.id', frame('CTA Card', {
289
+ width: contentW, paddingTop: 64, paddingBottom: 64, paddingLeft: 48, paddingRight: 48,
290
+ gap: 28, fill: dark ? '#14142e' : '#ffffff', cornerRadius: 20,
291
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
292
+ })));
293
+
294
+ cmds.push(ap('$' + ctaCardIdx + '.id', text('Ready to get started?', {
295
+ fontSize: 44, color: text1, fontName: { family: 'Inter', style: 'Bold' },
296
+ textAlignHorizontal: 'CENTER',
297
+ })));
298
+
299
+ cmds.push(ap('$' + ctaCardIdx + '.id', text('Join thousands of teams already shipping faster.\nNo credit card required.', {
300
+ fontSize: 18, color: text2, fontName: { family: 'Inter', style: 'Regular' },
301
+ textAlignHorizontal: 'CENTER',
302
+ })));
303
+
304
+ var ctaBtnRowIdx = cmds.length;
305
+ cmds.push(ap('$' + ctaCardIdx + '.id', hframe('CTA Buttons', {
306
+ gap: 12, fill: dark ? '#14142e' : '#ffffff',
307
+ })));
308
+
309
+ var ctaPrimaryIdx = cmds.length;
310
+ cmds.push(ap('$' + ctaBtnRowIdx + '.id', hframe('CTA Primary', {
311
+ height: 56, paddingLeft: 32, paddingRight: 32, paddingTop: 0, paddingBottom: 0,
312
+ fill: brand, cornerRadius: 12,
313
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
314
+ })));
315
+ cmds.push(ap('$' + ctaPrimaryIdx + '.id', text(ctaText, {
316
+ fontSize: 17, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' },
317
+ })));
318
+
319
+ var ctaSecIdx = cmds.length;
320
+ cmds.push(ap('$' + ctaBtnRowIdx + '.id', hframe('CTA Secondary', {
321
+ height: 56, paddingLeft: 32, paddingRight: 32, paddingTop: 0, paddingBottom: 0,
322
+ fill: 'transparent', cornerRadius: 12,
323
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
324
+ })));
325
+ cmds.push(ap('$' + ctaSecIdx + '.id', text('Talk to sales →', {
326
+ fontSize: 17, color: text2, fontName: { family: 'Inter', style: 'Medium' },
327
+ })));
328
+
329
+ // ── Footer ──
330
+ cmds.push(ap('$0.id', rect('Footer Divider', { width: W, height: 1, fill: divider })));
331
+
332
+ var footerIdx = cmds.length;
333
+ cmds.push(ap('$0.id', hframe('Footer', {
334
+ width: W, paddingTop: 48, paddingBottom: 48, paddingLeft: 48, paddingRight: 48,
335
+ gap: 0, fill: bg,
336
+ counterAxisAlignItems: 'CENTER',
337
+ })));
338
+
339
+ // Footer left
340
+ cmds.push(ap('$' + footerIdx + '.id', text(args.brand || 'acme', {
341
+ fontSize: 16, color: text3, fontName: { family: 'Inter', style: 'Semi Bold' },
342
+ })));
343
+
344
+ // Footer spacer
345
+ cmds.push(ap('$' + footerIdx + '.id', frame('_spacer', { width: 1, height: 1, fill: bg, primaryAxisSizingMode: 'FILL' })));
346
+
347
+ // Footer right
348
+ cmds.push(ap('$' + footerIdx + '.id', text('© 2025 ' + (args.brand || 'Acme') + ' Inc. All rights reserved.', {
349
+ fontSize: 13, color: text3, fontName: { family: 'Inter', style: 'Regular' },
350
+ })));
351
+
352
+ return {
353
+ commands: cmds,
354
+ description: 'Landing page: nav, hero with badge, ' + stats.length + ' stats, ' + features.length + ' feature cards with icons, CTA card, footer. ' + cmds.length + ' elements.',
355
+ };
356
+ }
357
+
358
+ // ═══════════════════════════════════════════
359
+ // PRICING PAGE
360
+ // ═══════════════════════════════════════════
361
+
362
+ export function buildPricingPage(args) {
363
+ var brand = args.brandColor || '#6366f1';
364
+ var W = args.width || 1440;
365
+ var contentW = 1080;
366
+ var bg = '#09090f';
367
+ var bg2 = '#0f0f1c';
368
+ var cardBg = '#12122a';
369
+ var text1 = '#f0f0f8';
370
+ var text2 = '#a0a0b8';
371
+ var text3 = '#686880';
372
+ var divider = '#1a1a32';
373
+
374
+ var tiers = args.tiers || [
375
+ { name: 'Starter', price: '$0', period: '/mo', desc: 'For side projects and experiments.', features: ['1 project', '1,000 API calls/day', 'Community support', 'Basic analytics'], cta: 'Start free', highlighted: false },
376
+ { name: 'Pro', price: '$29', period: '/mo', desc: 'For growing teams that need more power.', features: ['Unlimited projects', '100,000 API calls/day', 'Priority support', 'Advanced analytics', 'Team collaboration', 'Custom domains', 'Webhooks'], cta: 'Start free trial', highlighted: true },
377
+ { name: 'Enterprise', price: 'Custom', period: '', desc: 'For organizations with advanced needs.', features: ['Everything in Pro', 'Unlimited API calls', 'Dedicated support', 'SSO & SAML', '99.99% SLA', 'Custom integrations', 'On-premise option', 'Audit logs'], cta: 'Contact sales', highlighted: false },
378
+ ];
379
+
380
+ var cmds = [];
381
+
382
+ // 0: Root
383
+ cmds.push(frame('Pricing Page', { width: W, fill: bg, primaryAxisSizingMode: 'HUG' }));
384
+
385
+ // Header
386
+ var headerIdx = cmds.length;
387
+ cmds.push(ap('$0.id', frame('Pricing Header', {
388
+ width: W, paddingTop: 96, paddingBottom: 64, paddingLeft: 48, paddingRight: 48,
389
+ gap: 20, fill: bg,
390
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
391
+ })));
392
+
393
+ cmds.push(ap('$' + headerIdx + '.id', text('Simple, transparent pricing', {
394
+ fontSize: 52, color: text1, fontName: { family: 'Inter', style: 'Bold' },
395
+ textAlignHorizontal: 'CENTER',
396
+ })));
397
+
398
+ cmds.push(ap('$' + headerIdx + '.id', text('No hidden fees. No surprises. Cancel anytime.\nStart free and scale as you grow.', {
399
+ fontSize: 18, color: text2, fontName: { family: 'Inter', style: 'Regular' },
400
+ textAlignHorizontal: 'CENTER',
401
+ })));
402
+
403
+ // Tier row
404
+ var tierRowIdx = cmds.length;
405
+ cmds.push(ap('$0.id', hframe('Pricing Tiers', {
406
+ width: W, paddingLeft: Math.floor((W - contentW) / 2), paddingRight: Math.floor((W - contentW) / 2),
407
+ paddingTop: 0, paddingBottom: 96,
408
+ gap: 20, fill: bg, counterAxisAlignItems: 'STRETCH',
409
+ })));
410
+
411
+ for (var t = 0; t < tiers.length; t++) {
412
+ var tier = tiers[t];
413
+ var isHl = tier.highlighted;
414
+ var cardW = Math.floor((contentW - (tiers.length - 1) * 20) / tiers.length);
415
+
416
+ var tierIdx = cmds.length;
417
+ cmds.push(ap('$' + tierRowIdx + '.id', frame(tier.name, {
418
+ width: cardW, paddingTop: 36, paddingBottom: 36, paddingLeft: 32, paddingRight: 32,
419
+ gap: 20, cornerRadius: 16,
420
+ fill: isHl ? '#1a1a3e' : cardBg,
421
+ })));
422
+
423
+ // Popular badge
424
+ if (isHl) {
425
+ var bIdx = cmds.length;
426
+ cmds.push(ap('$' + tierIdx + '.id', hframe('Popular', {
427
+ paddingLeft: 12, paddingRight: 12, paddingTop: 4, paddingBottom: 4,
428
+ fill: brand, cornerRadius: 6,
429
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
430
+ })));
431
+ cmds.push(ap('$' + bIdx + '.id', text('MOST POPULAR', {
432
+ fontSize: 10, color: '#ffffff', fontName: { family: 'Inter', style: 'Bold' },
433
+ })));
434
+ }
435
+
436
+ // Tier name
437
+ cmds.push(ap('$' + tierIdx + '.id', text(tier.name, {
438
+ fontSize: 22, color: text1, fontName: { family: 'Inter', style: 'Semi Bold' },
439
+ })));
440
+
441
+ // Price row
442
+ var priceIdx = cmds.length;
443
+ cmds.push(ap('$' + tierIdx + '.id', hframe('Price', {
444
+ gap: 4, fill: isHl ? '#1a1a3e' : cardBg,
445
+ counterAxisAlignItems: 'BASELINE',
446
+ })));
447
+ cmds.push(ap('$' + priceIdx + '.id', text(tier.price, {
448
+ fontSize: 48, color: text1, fontName: { family: 'Inter', style: 'Bold' },
449
+ })));
450
+ if (tier.period) {
451
+ cmds.push(ap('$' + priceIdx + '.id', text(tier.period, {
452
+ fontSize: 16, color: text3, fontName: { family: 'Inter', style: 'Regular' },
453
+ })));
454
+ }
455
+
456
+ // Description
457
+ cmds.push(ap('$' + tierIdx + '.id', text(tier.desc, {
458
+ fontSize: 14, color: text2, fontName: { family: 'Inter', style: 'Regular' },
459
+ })));
460
+
461
+ // Divider
462
+ cmds.push(ap('$' + tierIdx + '.id', rect('Divider', {
463
+ width: cardW - 64, height: 1, fill: divider,
464
+ })));
465
+
466
+ // Features list
467
+ var featListIdx = cmds.length;
468
+ cmds.push(ap('$' + tierIdx + '.id', frame('Features', {
469
+ gap: 12, fill: isHl ? '#1a1a3e' : cardBg,
470
+ })));
471
+
472
+ for (var fi = 0; fi < tier.features.length; fi++) {
473
+ cmds.push(ap('$' + featListIdx + '.id', text('✓ ' + tier.features[fi], {
474
+ fontSize: 14, color: text2, fontName: { family: 'Inter', style: 'Regular' },
475
+ })));
476
+ }
477
+
478
+ // CTA button
479
+ var btnIdx = cmds.length;
480
+ cmds.push(ap('$' + tierIdx + '.id', hframe(tier.cta, {
481
+ height: 48, paddingLeft: 24, paddingRight: 24, paddingTop: 0, paddingBottom: 0,
482
+ fill: isHl ? brand : 'transparent',
483
+ cornerRadius: 10,
484
+ primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER',
485
+ primaryAxisSizingMode: 'FILL',
486
+ })));
487
+ cmds.push(ap('$' + btnIdx + '.id', text(tier.cta, {
488
+ fontSize: 15, color: isHl ? '#ffffff' : brand,
489
+ fontName: { family: 'Inter', style: 'Semi Bold' },
490
+ })));
491
+ }
492
+
493
+ return {
494
+ commands: cmds,
495
+ description: 'Pricing page with header and ' + tiers.length + ' tiers (' + tiers.map(function(t) { return t.name; }).join(', ') + '). ' + cmds.length + ' elements.',
496
+ };
497
+ }
498
+
499
+ // ═══════════════════════════════════════════
500
+ // DASHBOARD
501
+ // ═══════════════════════════════════════════
502
+
503
+ export function buildDashboardPage(args) {
504
+ var W = args.width || 1440;
505
+ var brand = args.brandColor || '#6366f1';
506
+ var bg = '#09090f';
507
+ var sidebarBg = '#0c0c18';
508
+ var cardBg = '#12122a';
509
+ var text1 = '#f0f0f8';
510
+ var text2 = '#a0a0b8';
511
+ var text3 = '#686880';
512
+ var divider = '#1a1a32';
513
+ var sideW = 260;
514
+
515
+ var metrics = args.metrics || [
516
+ { label: 'Total Revenue', value: '$48,290', change: '+12.5%', positive: true },
517
+ { label: 'Active Users', value: '2,420', change: '+8.3%', positive: true },
518
+ { label: 'Conversion', value: '3.24%', change: '-0.8%', positive: false },
519
+ { label: 'Avg. Session', value: '4m 32s', change: '+15.2%', positive: true },
520
+ ];
521
+
522
+ var cmds = [];
523
+
524
+ cmds.push(hframe('Dashboard', { width: W, height: 900, fill: bg, gap: 0 }));
525
+
526
+ // Sidebar
527
+ var sideIdx = cmds.length;
528
+ cmds.push(ap('$0.id', frame('Sidebar', {
529
+ width: sideW, height: 900, paddingTop: 24, paddingBottom: 24, paddingLeft: 20, paddingRight: 20,
530
+ gap: 4, fill: sidebarBg,
531
+ })));
532
+
533
+ cmds.push(ap('$' + sideIdx + '.id', text('◆ Dashboard', {
534
+ fontSize: 16, color: text1, fontName: { family: 'Inter', style: 'Bold' },
535
+ })));
536
+
537
+ cmds.push(ap('$' + sideIdx + '.id', frame('_gap', { width: 1, height: 20, fill: sidebarBg })));
538
+
539
+ var sideNavItems = [
540
+ { label: 'Overview', active: true },
541
+ { label: 'Analytics', active: false },
542
+ { label: 'Customers', active: false },
543
+ { label: 'Products', active: false },
544
+ { label: 'Settings', active: false },
545
+ ];
546
+
547
+ for (var si = 0; si < sideNavItems.length; si++) {
548
+ var navItem = sideNavItems[si];
549
+ var niIdx = cmds.length;
550
+ cmds.push(ap('$' + sideIdx + '.id', hframe(navItem.label, {
551
+ height: 40, paddingLeft: 12, paddingRight: 12, paddingTop: 0, paddingBottom: 0,
552
+ gap: 8, cornerRadius: 8,
553
+ fill: navItem.active ? '#1a1a36' : sidebarBg,
554
+ counterAxisAlignItems: 'CENTER', primaryAxisSizingMode: 'FILL',
555
+ })));
556
+ cmds.push(ap('$' + niIdx + '.id', text(navItem.label, {
557
+ fontSize: 14, color: navItem.active ? text1 : text3,
558
+ fontName: { family: 'Inter', style: navItem.active ? 'Medium' : 'Regular' },
559
+ })));
560
+ }
561
+
562
+ // Sidebar divider
563
+ cmds.push(ap('$0.id', rect('Sidebar Divider', { width: 1, height: 900, fill: divider })));
564
+
565
+ // Main
566
+ var mainIdx = cmds.length;
567
+ cmds.push(ap('$0.id', frame('Main', {
568
+ width: W - sideW - 1, height: 900,
569
+ paddingTop: 32, paddingBottom: 32, paddingLeft: 40, paddingRight: 40,
570
+ gap: 28, fill: bg,
571
+ })));
572
+
573
+ // Header
574
+ cmds.push(ap('$' + mainIdx + '.id', text('Overview', {
575
+ fontSize: 28, color: text1, fontName: { family: 'Inter', style: 'Bold' },
576
+ })));
577
+
578
+ // Metrics row
579
+ var metricsRowIdx = cmds.length;
580
+ cmds.push(ap('$' + mainIdx + '.id', hframe('Metrics', {
581
+ gap: 20, fill: bg,
582
+ })));
583
+
584
+ var metricW = Math.floor((W - sideW - 1 - 80 - (metrics.length - 1) * 20) / metrics.length);
585
+ for (var mi = 0; mi < metrics.length; mi++) {
586
+ var mIdx = cmds.length;
587
+ cmds.push(ap('$' + metricsRowIdx + '.id', frame(metrics[mi].label, {
588
+ width: metricW, paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24,
589
+ gap: 8, fill: cardBg, cornerRadius: 12,
590
+ })));
591
+ cmds.push(ap('$' + mIdx + '.id', text(metrics[mi].label, {
592
+ fontSize: 13, color: text3, fontName: { family: 'Inter', style: 'Medium' },
593
+ })));
594
+ cmds.push(ap('$' + mIdx + '.id', text(metrics[mi].value, {
595
+ fontSize: 32, color: text1, fontName: { family: 'Inter', style: 'Bold' },
596
+ })));
597
+ cmds.push(ap('$' + mIdx + '.id', text(metrics[mi].change, {
598
+ fontSize: 13, color: metrics[mi].positive ? '#4ade80' : '#f87171',
599
+ fontName: { family: 'Inter', style: 'Semi Bold' },
600
+ })));
601
+ }
602
+
603
+ // Chart
604
+ var chartIdx = cmds.length;
605
+ cmds.push(ap('$' + mainIdx + '.id', frame('Chart Area', {
606
+ paddingTop: 28, paddingBottom: 28, paddingLeft: 28, paddingRight: 28,
607
+ gap: 16, fill: cardBg, cornerRadius: 12,
608
+ primaryAxisSizingMode: 'FILL',
609
+ })));
610
+
611
+ var chartHeaderIdx = cmds.length;
612
+ cmds.push(ap('$' + chartIdx + '.id', hframe('Chart Header', {
613
+ gap: 12, fill: cardBg, counterAxisAlignItems: 'CENTER',
614
+ })));
615
+ cmds.push(ap('$' + chartHeaderIdx + '.id', text('Revenue Over Time', {
616
+ fontSize: 18, color: text1, fontName: { family: 'Inter', style: 'Semi Bold' },
617
+ })));
618
+
619
+ cmds.push(ap('$' + chartIdx + '.id', rect('Chart Placeholder', {
620
+ width: W - sideW - 1 - 80 - 56, height: 240, fill: '#16163a', cornerRadius: 8,
621
+ })));
622
+
623
+ return {
624
+ commands: cmds,
625
+ description: 'Dashboard with sidebar (' + sideNavItems.length + ' nav items), header, ' + metrics.length + ' metric cards, and chart area. ' + cmds.length + ' elements.',
626
+ };
627
+ }
628
+
629
+ // ═══════════════════════════════════════════
630
+ // SECTION BLUEPRINTS
631
+ // ═══════════════════════════════════════════
632
+
633
+ export function buildSection(sectionType, args) {
634
+ switch (sectionType) {
635
+ case 'hero': return buildHeroSection(args);
636
+ case 'features': return buildFeaturesSection(args);
637
+ case 'pricing': return buildPricingSection(args);
638
+ case 'cta': return buildCTASection(args);
639
+ case 'testimonials': return buildTestimonialsSection(args);
640
+ case 'faq': return buildFAQSection(args);
641
+ default: return buildHeroSection(args);
642
+ }
643
+ }
644
+
645
+ function buildHeroSection(args) {
646
+ var W = args.width || 1440;
647
+ var brand = args.brandColor || '#6366f1';
648
+ var bg = '#09090f';
649
+ var cmds = [];
650
+
651
+ cmds.push(frame('Hero', { width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48, gap: 32, fill: bg, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' }));
652
+ cmds.push(ap('$0.id', text(args.heading || 'Build something great', { fontSize: 64, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
653
+ cmds.push(ap('$0.id', text(args.subheading || 'The platform for modern teams.', { fontSize: 20, color: '#a0a0b8', textAlignHorizontal: 'CENTER' })));
654
+
655
+ var btnIdx = cmds.length;
656
+ cmds.push(ap('$0.id', hframe('CTA', { height: 52, paddingLeft: 28, paddingRight: 28, paddingTop: 0, paddingBottom: 0, fill: brand, cornerRadius: 12, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' })));
657
+ cmds.push(ap('$' + btnIdx + '.id', text(args.ctaText || 'Get started', { fontSize: 16, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
658
+
659
+ return { commands: cmds, description: 'Hero section with heading, subtitle, and CTA.' };
660
+ }
661
+
662
+ function buildFeaturesSection(args) {
663
+ var W = args.width || 1440;
664
+ var features = args.features || [
665
+ { title: 'Fast', desc: 'Blazing fast performance out of the box.' },
666
+ { title: 'Secure', desc: 'Enterprise-grade security by default.' },
667
+ { title: 'Scalable', desc: 'Grows seamlessly with your team.' },
668
+ ];
669
+ var cmds = [];
670
+
671
+ cmds.push(frame('Features', { width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48, gap: 48, fill: '#09090f', counterAxisAlignItems: 'CENTER' }));
672
+ cmds.push(ap('$0.id', text(args.heading || 'Features', { fontSize: 40, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
673
+
674
+ var rowIdx = cmds.length;
675
+ cmds.push(ap('$0.id', hframe('Cards', { width: 1120, gap: 24, fill: '#09090f' })));
676
+
677
+ for (var i = 0; i < features.length; i++) {
678
+ var cw = Math.floor((1120 - (features.length - 1) * 24) / features.length);
679
+ var ci = cmds.length;
680
+ cmds.push(ap('$' + rowIdx + '.id', frame(features[i].title, { width: cw, paddingTop: 32, paddingBottom: 32, paddingLeft: 28, paddingRight: 28, gap: 16, fill: '#12122a', cornerRadius: 16 })));
681
+ cmds.push(ap('$' + ci + '.id', text(features[i].title, { fontSize: 20, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Semi Bold' } })));
682
+ cmds.push(ap('$' + ci + '.id', text(features[i].desc, { fontSize: 15, color: '#a0a0b8' })));
683
+ }
684
+
685
+ return { commands: cmds, description: 'Features section with ' + features.length + ' cards.' };
686
+ }
687
+
688
+ function buildPricingSection(args) { return buildPricingPage(args); }
689
+
690
+ function buildCTASection(args) {
691
+ var W = args.width || 1440;
692
+ var brand = args.brandColor || '#6366f1';
693
+ var cmds = [];
694
+
695
+ cmds.push(frame('CTA', { width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48, gap: 28, fill: '#0f0f1c', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' }));
696
+ cmds.push(ap('$0.id', text(args.heading || 'Ready to start?', { fontSize: 44, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
697
+ cmds.push(ap('$0.id', text(args.subheading || 'Join thousands of teams shipping faster.', { fontSize: 18, color: '#a0a0b8', textAlignHorizontal: 'CENTER' })));
698
+ var btnIdx = cmds.length;
699
+ cmds.push(ap('$0.id', hframe('CTA Button', { height: 56, paddingLeft: 32, paddingRight: 32, paddingTop: 0, paddingBottom: 0, fill: brand, cornerRadius: 12, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' })));
700
+ cmds.push(ap('$' + btnIdx + '.id', text(args.ctaText || 'Get started free', { fontSize: 17, color: '#ffffff', fontName: { family: 'Inter', style: 'Semi Bold' } })));
701
+
702
+ return { commands: cmds, description: 'CTA section with heading, subtitle, and button.' };
703
+ }
704
+
705
+ function buildTestimonialsSection(args) {
706
+ var W = args.width || 1440;
707
+ var testimonials = args.testimonials || [
708
+ { quote: 'This product transformed how our team works. The speed and reliability are unmatched.', author: 'Sarah Chen', role: 'CTO, TechCorp' },
709
+ { quote: 'Best tool we adopted this year. Our deployment time dropped by 80%.', author: 'Marcus Rivera', role: 'VP Engineering, ScaleUp' },
710
+ ];
711
+ var cmds = [];
712
+
713
+ cmds.push(frame('Testimonials', { width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48, gap: 48, fill: '#09090f', counterAxisAlignItems: 'CENTER' }));
714
+ cmds.push(ap('$0.id', text('Trusted by the best teams', { fontSize: 40, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
715
+
716
+ var rowIdx = cmds.length;
717
+ cmds.push(ap('$0.id', hframe('Cards', { width: 1120, gap: 24, fill: '#09090f' })));
718
+
719
+ for (var i = 0; i < testimonials.length; i++) {
720
+ var tw = Math.floor((1120 - (testimonials.length - 1) * 24) / testimonials.length);
721
+ var ti = cmds.length;
722
+ cmds.push(ap('$' + rowIdx + '.id', frame('Testimonial', { width: tw, paddingTop: 32, paddingBottom: 32, paddingLeft: 28, paddingRight: 28, gap: 20, fill: '#12122a', cornerRadius: 16 })));
723
+ cmds.push(ap('$' + ti + '.id', text('★★★★★', { fontSize: 16, color: '#fbbf24' })));
724
+ cmds.push(ap('$' + ti + '.id', text('"' + testimonials[i].quote + '"', { fontSize: 16, color: '#d0d0e0', fontName: { family: 'Inter', style: 'Regular' } })));
725
+ cmds.push(ap('$' + ti + '.id', text(testimonials[i].author, { fontSize: 15, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Semi Bold' } })));
726
+ cmds.push(ap('$' + ti + '.id', text(testimonials[i].role, { fontSize: 13, color: '#686880' })));
727
+ }
728
+
729
+ return { commands: cmds, description: 'Testimonials with ' + testimonials.length + ' cards.' };
730
+ }
731
+
732
+ function buildFAQSection(args) {
733
+ var W = args.width || 1440;
734
+ var faqs = args.faqs || [
735
+ { q: 'How do I get started?', a: 'Sign up for a free account and follow our quick-start guide. You will be up and running in less than 5 minutes.' },
736
+ { q: 'Can I cancel anytime?', a: 'Yes. No contracts, no cancellation fees. Cancel with one click from your dashboard.' },
737
+ { q: 'Do you offer support?', a: 'All plans include email support. Pro and Enterprise plans get priority support with guaranteed response times.' },
738
+ ];
739
+ var cmds = [];
740
+
741
+ cmds.push(frame('FAQ', { width: W, paddingTop: 96, paddingBottom: 96, paddingLeft: 48, paddingRight: 48, gap: 48, fill: '#09090f', counterAxisAlignItems: 'CENTER' }));
742
+ cmds.push(ap('$0.id', text('Frequently asked questions', { fontSize: 40, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Bold' }, textAlignHorizontal: 'CENTER' })));
743
+
744
+ var listIdx = cmds.length;
745
+ cmds.push(ap('$0.id', frame('FAQ List', { width: 720, gap: 12, fill: '#09090f' })));
746
+
747
+ for (var i = 0; i < faqs.length; i++) {
748
+ var fIdx = cmds.length;
749
+ cmds.push(ap('$' + listIdx + '.id', frame('FAQ ' + (i + 1), { paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, gap: 8, fill: '#12122a', cornerRadius: 12, primaryAxisSizingMode: 'FILL' })));
750
+ cmds.push(ap('$' + fIdx + '.id', text(faqs[i].q, { fontSize: 16, color: '#f0f0f8', fontName: { family: 'Inter', style: 'Semi Bold' } })));
751
+ cmds.push(ap('$' + fIdx + '.id', text(faqs[i].a, { fontSize: 14, color: '#a0a0b8' })));
752
+ }
753
+
754
+ return { commands: cmds, description: 'FAQ section with ' + faqs.length + ' items.' };
755
+ }
756
+
757
+ // ═══════════════════════════════════════════
758
+ // ROUTER
759
+ // ═══════════════════════════════════════════
760
+
761
+ export function getBlueprint(toolName, args) {
762
+ switch (toolName) {
763
+ case 'create_page':
764
+ switch (args.pageType) {
765
+ case 'landing': return buildLandingPage(args);
766
+ case 'pricing': return buildPricingPage(args);
767
+ case 'dashboard': return buildDashboardPage(args);
768
+ default: return buildLandingPage(args);
769
+ }
770
+ case 'create_section':
771
+ return buildSection(args.sectionType, args);
772
+ default:
773
+ return null;
774
+ }
775
+ }
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);