conductor-figma 0.1.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
@@ -3,21 +3,28 @@
3
3
  import { startServer } from '../src/server.js';
4
4
  import { TOOLS, CATEGORIES } from '../src/tools/registry.js';
5
5
 
6
- const args = process.argv.slice(2);
6
+ var args = process.argv.slice(2);
7
7
 
8
8
  if (args.includes('--help') || args.includes('-h')) {
9
9
  console.log(`
10
10
  ⊞ CONDUCTOR — Design-intelligent MCP server for Figma
11
11
 
12
12
  Usage:
13
- conductor-figma Start MCP server (stdio)
14
- conductor-figma --list List all ${TOOLS.length} tools
15
- conductor-figma --categories Show tool categories
16
- conductor-figma --help Show this help
13
+ conductor-figma Start MCP server + orchestrator
14
+ conductor-figma --port 9800 Set WebSocket port (default: 9800)
15
+ conductor-figma --list List all ${TOOLS.length} tools
16
+ conductor-figma --categories Show tool categories
17
+ conductor-figma --help Show this help
17
18
 
18
- MCP Setup (Cursor):
19
- Add to ~/.cursor/mcp.json:
19
+ How it works:
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.
20
25
 
26
+ Setup:
27
+ ~/.cursor/mcp.json:
21
28
  {
22
29
  "mcpServers": {
23
30
  "conductor": {
@@ -27,32 +34,43 @@ if (args.includes('--help') || args.includes('-h')) {
27
34
  }
28
35
  }
29
36
 
30
- ${TOOLS.length} tools · ${Object.keys(CATEGORIES).length} categories · Zero dependencies
37
+ Then open the CONDUCTOR plugin in Figma and click Connect.
38
+
39
+ ${TOOLS.length} tools · ${Object.keys(CATEGORIES).length} categories
31
40
  Built by 0xDragoon · MIT License
32
41
  `);
33
42
  process.exit(0);
34
43
  }
35
44
 
36
45
  if (args.includes('--list')) {
37
- for (const [catKey, cat] of Object.entries(CATEGORIES)) {
38
- const tools = TOOLS.filter(t => t.category === catKey);
39
- console.log(`\n ${cat.icon} ${cat.label} (${tools.length})`);
40
- for (const t of tools) {
41
- console.log(` ${t.name.padEnd(28)} ${t.description.slice(0, 70)}`);
46
+ for (var entries = Object.entries(CATEGORIES), i = 0; i < entries.length; i++) {
47
+ var catKey = entries[i][0], cat = entries[i][1];
48
+ var tools = TOOLS.filter(function(t) { return t.category === catKey; });
49
+ console.log('\n ' + cat.icon + ' ' + cat.label + ' (' + tools.length + ')');
50
+ for (var j = 0; j < tools.length; j++) {
51
+ console.log(' ' + tools[j].name.padEnd(28) + ' ' + tools[j].description.slice(0, 70));
42
52
  }
43
53
  }
44
- console.log(`\n ${TOOLS.length} tools total\n`);
54
+ console.log('\n ' + TOOLS.length + ' tools total\n');
45
55
  process.exit(0);
46
56
  }
47
57
 
48
58
  if (args.includes('--categories')) {
49
- for (const [key, cat] of Object.entries(CATEGORIES)) {
50
- const tools = TOOLS.filter(t => t.category === key);
51
- console.log(` ${cat.icon} ${cat.label.padEnd(18)} ${tools.length} tools`);
59
+ for (var entries2 = Object.entries(CATEGORIES), k = 0; k < entries2.length; k++) {
60
+ var key = entries2[k][0], cat2 = entries2[k][1];
61
+ var count = TOOLS.filter(function(t) { return t.category === key; }).length;
62
+ console.log(' ' + cat2.icon + ' ' + cat2.label.padEnd(18) + ' ' + count + ' tools');
52
63
  }
53
- console.log(`\n ${TOOLS.length} tools total`);
64
+ console.log('\n ' + TOOLS.length + ' tools total');
54
65
  process.exit(0);
55
66
  }
56
67
 
57
- // Default: start MCP server
58
- startServer();
68
+ // Parse port
69
+ var port = 9800;
70
+ var portIdx = args.indexOf('--port');
71
+ if (portIdx !== -1 && args[portIdx + 1]) {
72
+ port = parseInt(args[portIdx + 1]) || 9800;
73
+ }
74
+
75
+ // Start MCP server with relay
76
+ startServer({ port: port });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-figma",
3
- "version": "0.1.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",
@@ -33,5 +33,8 @@
33
33
  "engines": {
34
34
  "node": ">=18.0.0"
35
35
  },
36
- "dependencies": {}
36
+ "dependencies": {},
37
+ "optionalDependencies": {
38
+ "ws": "^8.0.0"
39
+ }
37
40
  }
@@ -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
@@ -3,6 +3,9 @@
3
3
  // ═══════════════════════════════════════════
4
4
 
5
5
  export { startServer } from './server.js';
6
+ export { Relay } from './relay.js';
7
+ export { executeSequence } from './orchestrator.js';
8
+ export { getBlueprint, buildLandingPage, buildPricingPage, buildDashboardPage, buildSection } from './blueprints.js';
6
9
  export { TOOLS, CATEGORIES, getToolByName, getToolsByCategory, getAllToolNames } from './tools/registry.js';
7
10
  export { handleTool } from './tools/handlers.js';
8
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/relay.js ADDED
@@ -0,0 +1,174 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — WebSocket Relay
3
+ // ═══════════════════════════════════════════
4
+ // Bridges MCP stdio (from Cursor) to WebSocket (to Figma plugin).
5
+ //
6
+ // Flow:
7
+ // Cursor → MCP stdio → CONDUCTOR → design logic (local)
8
+ // → figma commands (WebSocket) → Plugin → Figma API
9
+ // ← results ← WebSocket ←
10
+ // ← MCP stdio ← CONDUCTOR ←
11
+
12
+ import { createServer } from 'node:http';
13
+
14
+ // Tools that need Figma (sent over WebSocket to plugin)
15
+ const FIGMA_COMMANDS = new Set([
16
+ // Create
17
+ 'create_frame', 'create_text', 'create_rect', 'create_section', 'create_component',
18
+ // Layout
19
+ 'set_auto_layout', 'set_constraints', 'apply_grid', 'align_nodes',
20
+ // Style
21
+ 'set_fills', 'set_strokes', 'set_effects', 'set_corner_radius', 'set_opacity',
22
+ // Typography
23
+ 'set_text_props', 'load_font',
24
+ // Read
25
+ 'get_selection', 'get_page_info', 'get_styles', 'get_components',
26
+ 'read_node', 'read_tree', 'read_spacing', 'read_colors', 'read_typography',
27
+ // Edit
28
+ 'rename_node', 'move_node', 'resize_node', 'delete_node',
29
+ 'clone_node', 'group_nodes', 'ungroup_node', 'reorder_node',
30
+ // Export
31
+ 'export_png', 'export_svg',
32
+ // Viewport
33
+ 'zoom_to', 'scroll_to',
34
+ // Meta
35
+ 'ping',
36
+ ]);
37
+
38
+ export class Relay {
39
+ constructor(port) {
40
+ this.port = port || 9800;
41
+ this.pluginSocket = null;
42
+ this.pendingCallbacks = new Map();
43
+ this.cmdId = 0;
44
+ this.server = null;
45
+ this.wss = null;
46
+ }
47
+
48
+ async start() {
49
+ // Dynamic import ws (may not be installed — we bundle our own minimal WS)
50
+ let WebSocketServer;
51
+ try {
52
+ const ws = await import('ws');
53
+ WebSocketServer = ws.WebSocketServer || ws.default.WebSocketServer;
54
+ } catch (e) {
55
+ process.stderr.write('CONDUCTOR relay: "ws" package not found. Install with: npm install ws\n');
56
+ process.stderr.write('Falling back to MCP-only mode (no Figma bridge).\n');
57
+ return false;
58
+ }
59
+
60
+ this.server = createServer();
61
+ this.wss = new WebSocketServer({ server: this.server });
62
+
63
+ this.wss.on('connection', (socket) => {
64
+ this.pluginSocket = socket;
65
+ process.stderr.write('CONDUCTOR relay: Figma plugin connected\n');
66
+
67
+ socket.on('message', (data) => {
68
+ try {
69
+ const msg = JSON.parse(data.toString());
70
+ this.handlePluginMessage(msg);
71
+ } catch (e) {
72
+ // ignore
73
+ }
74
+ });
75
+
76
+ socket.on('close', () => {
77
+ this.pluginSocket = null;
78
+ process.stderr.write('CONDUCTOR relay: Figma plugin disconnected\n');
79
+ });
80
+
81
+ socket.on('error', () => {
82
+ this.pluginSocket = null;
83
+ });
84
+ });
85
+
86
+ return new Promise((resolve) => {
87
+ this.server.listen(this.port, () => {
88
+ process.stderr.write(`CONDUCTOR relay: WebSocket listening on ws://localhost:${this.port}\n`);
89
+ resolve(true);
90
+ });
91
+ });
92
+ }
93
+
94
+ handlePluginMessage(msg) {
95
+ if (msg.type === 'plugin_ready') {
96
+ process.stderr.write(`CONDUCTOR relay: Plugin ready (v${msg.version || '?'})\n`);
97
+ return;
98
+ }
99
+
100
+ if (msg.type === 'result' && msg.id !== undefined) {
101
+ const callback = this.pendingCallbacks.get(msg.id);
102
+ if (callback) {
103
+ this.pendingCallbacks.delete(msg.id);
104
+ callback(msg.data || {});
105
+ }
106
+ return;
107
+ }
108
+ }
109
+
110
+ isConnected() {
111
+ return this.pluginSocket !== null && this.pluginSocket.readyState === 1; // WebSocket.OPEN
112
+ }
113
+
114
+ /**
115
+ * Send a command to the Figma plugin and wait for result.
116
+ * Returns a Promise that resolves with the plugin's response.
117
+ */
118
+ sendToPlugin(commandType, commandData, timeout) {
119
+ timeout = timeout || 15000;
120
+
121
+ return new Promise((resolve, reject) => {
122
+ if (!this.isConnected()) {
123
+ resolve({ error: 'Figma plugin not connected. Open the CONDUCTOR plugin in Figma and click Connect.' });
124
+ return;
125
+ }
126
+
127
+ var id = ++this.cmdId;
128
+ var timer = setTimeout(function() {
129
+ this.pendingCallbacks.delete(id);
130
+ resolve({ error: 'Timeout waiting for Figma plugin response' });
131
+ }.bind(this), timeout);
132
+
133
+ this.pendingCallbacks.set(id, function(result) {
134
+ clearTimeout(timer);
135
+ resolve(result);
136
+ });
137
+
138
+ this.pluginSocket.send(JSON.stringify({
139
+ type: 'command',
140
+ id: id,
141
+ command: { type: commandType, data: commandData || {} },
142
+ }));
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Check if a tool name maps to a Figma command.
148
+ */
149
+ isFigmaCommand(toolName) {
150
+ return FIGMA_COMMANDS.has(toolName);
151
+ }
152
+
153
+ /**
154
+ * Map a high-level tool call to one or more Figma commands.
155
+ * Some tools (like create_page) produce multiple Figma commands.
156
+ * Some tools (like color_palette) are pure design logic — no Figma needed.
157
+ */
158
+ getFigmaCommand(toolName, toolArgs) {
159
+ // Direct mappings — tool name IS the Figma command
160
+ if (FIGMA_COMMANDS.has(toolName)) {
161
+ return { command: toolName, data: toolArgs };
162
+ }
163
+
164
+ // Tools that generate Figma commands from design logic output
165
+ // The handler produces a JSON response with an "action" field
166
+ // that maps to a Figma command
167
+ return null;
168
+ }
169
+
170
+ stop() {
171
+ if (this.wss) this.wss.close();
172
+ if (this.server) this.server.close();
173
+ }
174
+ }
package/src/server.js CHANGED
@@ -1,115 +1,204 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — MCP Server
2
+ // CONDUCTOR — MCP Server + Orchestrator
3
3
  // ═══════════════════════════════════════════
4
- // Model Context Protocol server over stdio.
5
- // Registers 61 design-intelligent tools for AI editors.
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
6
9
 
7
10
  import { TOOLS } from './tools/registry.js';
8
11
  import { handleTool } from './tools/handlers.js';
12
+ import { Relay } from './relay.js';
13
+ import { getBlueprint } from './blueprints.js';
14
+ import { executeSequence } from './orchestrator.js';
9
15
 
10
- const SERVER_INFO = {
11
- name: 'conductor-figma',
12
- version: '0.1.0',
13
- };
16
+ var SERVER_INFO = { name: 'conductor-figma', version: '0.3.0' };
17
+ var CAPABILITIES = { tools: {} };
18
+ var relay = null;
14
19
 
15
- const CAPABILITIES = {
16
- tools: {},
17
- };
20
+ // Tools that trigger blueprint orchestration (multi-command sequences)
21
+ var BLUEPRINT_TOOLS = new Set(['create_page', 'create_section']);
18
22
 
19
- /**
20
- * Start the MCP server on stdio.
21
- * Reads JSON-RPC messages from stdin, writes responses to stdout.
22
- */
23
- export function startServer() {
24
- let buffer = '';
23
+ export async function startServer(options) {
24
+ options = options || {};
25
+ var port = options.port || 9800;
26
+
27
+ relay = new Relay(port);
28
+ var relayStarted = await relay.start();
29
+
30
+ if (relayStarted) {
31
+ process.stderr.write('CONDUCTOR v0.3.0: MCP + orchestrator ready (' + TOOLS.length + ' tools, ws://localhost:' + port + ')\n');
32
+ } else {
33
+ process.stderr.write('CONDUCTOR v0.3.0: MCP ready (' + TOOLS.length + ' tools, no relay — install ws)\n');
34
+ }
35
+
36
+ var buffer = '';
25
37
 
26
38
  process.stdin.setEncoding('utf-8');
27
- process.stdin.on('data', (chunk) => {
39
+ process.stdin.on('data', function(chunk) {
28
40
  buffer += chunk;
29
-
30
- // Process complete lines
31
- const lines = buffer.split('\n');
41
+ var lines = buffer.split('\n');
32
42
  buffer = lines.pop() || '';
33
43
 
34
- for (const line of lines) {
35
- const trimmed = line.trim();
44
+ for (var i = 0; i < lines.length; i++) {
45
+ var trimmed = lines[i].trim();
36
46
  if (!trimmed) continue;
37
-
38
47
  try {
39
- const message = JSON.parse(trimmed);
40
- handleMessage(message);
48
+ handleMessage(JSON.parse(trimmed));
41
49
  } catch (err) {
42
50
  sendError(null, -32700, 'Parse error');
43
51
  }
44
52
  }
45
53
  });
46
54
 
47
- process.stdin.on('end', () => {
55
+ process.stdin.on('end', function() {
56
+ if (relay) relay.stop();
48
57
  process.exit(0);
49
58
  });
50
-
51
- // Log startup to stderr (not stdout — that's for MCP protocol)
52
- process.stderr.write(`CONDUCTOR MCP server started (${TOOLS.length} tools)\n`);
53
59
  }
54
60
 
55
- function handleMessage(msg) {
56
- // JSON-RPC 2.0
57
- const { id, method, params } = msg;
61
+ async function handleMessage(msg) {
62
+ var id = msg.id;
63
+ var method = msg.method;
64
+ var params = msg.params || {};
58
65
 
59
66
  switch (method) {
60
67
  case 'initialize':
61
- sendResult(id, {
62
- protocolVersion: '2024-11-05',
63
- serverInfo: SERVER_INFO,
64
- capabilities: CAPABILITIES,
65
- });
68
+ sendResult(id, { protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, capabilities: CAPABILITIES });
66
69
  break;
67
70
 
68
71
  case 'initialized':
69
- // Notification, no response needed
70
72
  break;
71
73
 
72
74
  case 'tools/list':
73
75
  sendResult(id, {
74
- tools: TOOLS.map(t => ({
75
- name: t.name,
76
- description: t.description,
77
- inputSchema: t.inputSchema,
78
- })),
76
+ tools: TOOLS.map(function(t) { return { name: t.name, description: t.description, inputSchema: t.inputSchema }; }),
79
77
  });
80
78
  break;
81
79
 
82
- case 'tools/call': {
83
- const toolName = params?.name;
84
- const toolArgs = params?.arguments || {};
85
-
86
- if (!toolName) {
87
- sendError(id, -32602, 'Missing tool name');
88
- return;
89
- }
90
-
91
- const result = handleTool(toolName, toolArgs, null);
92
- sendResult(id, result);
80
+ case 'tools/call':
81
+ var toolName = params.name;
82
+ var toolArgs = params.arguments || {};
83
+ if (!toolName) { sendError(id, -32602, 'Missing tool name'); return; }
84
+ await handleToolCall(id, toolName, toolArgs);
93
85
  break;
94
- }
95
86
 
96
87
  case 'ping':
97
88
  sendResult(id, {});
98
89
  break;
99
90
 
100
91
  default:
101
- if (id !== undefined) {
102
- sendError(id, -32601, `Method not found: ${method}`);
92
+ if (id !== undefined) sendError(id, -32601, 'Method not found: ' + method);
93
+ }
94
+ }
95
+
96
+ async function handleToolCall(id, toolName, toolArgs) {
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
+ }
103
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
+ }
104
134
  }
135
+
136
+ // ═══ DIRECT FIGMA COMMANDS ═══
137
+ // Single commands forwarded directly to plugin
138
+ if (relay && relay.isFigmaCommand(toolName) && relay.isConnected()) {
139
+ process.stderr.write('CONDUCTOR: -> Figma: ' + toolName + '\n');
140
+ try {
141
+ var figmaResult = await relay.sendToPlugin(toolName, toolArgs);
142
+ process.stderr.write('CONDUCTOR: <- Figma: ' + (figmaResult.name || figmaResult.id || 'ok') + '\n');
143
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(figmaResult, null, 2) }] });
144
+ } catch (err) {
145
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify({ error: String(err) }) }] });
146
+ }
147
+ return;
148
+ }
149
+
150
+ // ═══ DESIGN INTELLIGENCE (local) ═══
151
+ var result = handleTool(toolName, toolArgs, null);
152
+
153
+ // Check if handler produced a Figma action to forward
154
+ if (relay && relay.isConnected()) {
155
+ try {
156
+ var data = JSON.parse(result.content[0].text);
157
+ if (data.action && relay.isFigmaCommand(data.action)) {
158
+ process.stderr.write('CONDUCTOR: -> Figma (via ' + toolName + '): ' + data.action + '\n');
159
+ var fResult = await relay.sendToPlugin(data.action, data);
160
+ process.stderr.write('CONDUCTOR: <- Figma: ' + (fResult.name || fResult.id || 'ok') + '\n');
161
+ data._figmaResult = fResult;
162
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
163
+ return;
164
+ }
165
+ } catch (e) { /* not JSON or no action */ }
166
+ }
167
+
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
187
+ if (relay && relay.isFigmaCommand(toolName) && !relay.isConnected()) {
188
+ try {
189
+ var parsed = JSON.parse(result.content[0].text);
190
+ parsed._note = 'Figma plugin not connected. Connect the CONDUCTOR plugin in Figma to execute on canvas.';
191
+ result = { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
192
+ } catch (e) { /* ignore */ }
193
+ }
194
+
195
+ sendResult(id, result);
105
196
  }
106
197
 
107
198
  function sendResult(id, result) {
108
- const response = { jsonrpc: '2.0', id, result };
109
- process.stdout.write(JSON.stringify(response) + '\n');
199
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, result: result }) + '\n');
110
200
  }
111
201
 
112
202
  function sendError(id, code, message) {
113
- const response = { jsonrpc: '2.0', id, error: { code, message } };
114
- process.stdout.write(JSON.stringify(response) + '\n');
203
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, error: { code: code, message: message } }) + '\n');
115
204
  }