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 +38 -20
- package/package.json +5 -2
- package/src/blueprints.js +656 -0
- package/src/index.js +3 -0
- package/src/orchestrator.js +100 -0
- package/src/relay.js +174 -0
- package/src/server.js +150 -61
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
|
-
|
|
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
|
|
14
|
-
conductor-figma --
|
|
15
|
-
conductor-figma --
|
|
16
|
-
conductor-figma --
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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 (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
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 (
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
64
|
+
console.log('\n ' + TOOLS.length + ' tools total');
|
|
54
65
|
process.exit(0);
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
//
|
|
58
|
-
|
|
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.
|
|
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
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
16
|
+
var SERVER_INFO = { name: 'conductor-figma', version: '0.3.0' };
|
|
17
|
+
var CAPABILITIES = { tools: {} };
|
|
18
|
+
var relay = null;
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
20
|
+
// Tools that trigger blueprint orchestration (multi-command sequences)
|
|
21
|
+
var BLUEPRINT_TOOLS = new Set(['create_page', 'create_section']);
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 (
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|