conductor-figma 3.0.3 → 3.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/README.md +138 -48
- package/figma-plugin/code.js +10 -10
- package/figma-plugin/manifest.json +2 -2
- package/package.json +1 -1
- package/src/design/composer.js +638 -0
- package/src/design/interpreter.js +295 -0
- package/src/server.js +37 -29
- package/src/tools/handlers.js +86 -0
- package/src/tools/registry.js +22 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// CONDUCTOR v3.2 — Design Interpreter
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// Takes any natural language prompt and extracts
|
|
5
|
+
// design intent: mood, palette, depth, density,
|
|
6
|
+
// typography character, layout strategy.
|
|
7
|
+
//
|
|
8
|
+
// This is the brain. The composer is the hands.
|
|
9
|
+
|
|
10
|
+
import { semanticColors, typeScale, snap, SHADOWS } from './intelligence.js'
|
|
11
|
+
|
|
12
|
+
// ─── Mood Detection ───
|
|
13
|
+
// Scans prompt text for style/mood signals
|
|
14
|
+
var MOOD_SIGNALS = {
|
|
15
|
+
// Mood → keywords that trigger it
|
|
16
|
+
minimal: ['minimal','clean','simple','stripped','bare','zen','quiet','calm','whitespace','restrained'],
|
|
17
|
+
bold: ['bold','strong','loud','impactful','powerful','striking','dramatic','intense','punchy'],
|
|
18
|
+
playful: ['playful','fun','friendly','casual','warm','cheerful','vibrant','colorful','quirky','whimsical'],
|
|
19
|
+
luxury: ['luxury','brand','premium','elegant','refined','sophisticated','exclusive','high-end','classy','prestige'],
|
|
20
|
+
corporate: ['corporate','professional','enterprise','business','formal','serious','trustworthy','reliable'],
|
|
21
|
+
dark: ['dark','moody','noir','night','shadow','deep','mysterious','brooding'],
|
|
22
|
+
techy: ['tech','developer','code','hacker','terminal','engineer','devtool','api','saas'],
|
|
23
|
+
organic: ['organic','natural','earthy','soft','gentle','rounded','flowing','human'],
|
|
24
|
+
brutalist: ['brutalist','raw','rough','unpolished','industrial','grunge','concrete'],
|
|
25
|
+
editorial: ['editorial','magazine','publication','article','blog','journal','story','content'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Industry Detection ───
|
|
29
|
+
var INDUSTRY_SIGNALS = {
|
|
30
|
+
fintech: ['fintech','banking','finance','payment','money','wallet','trading','invest','crypto'],
|
|
31
|
+
health: ['health','medical','wellness','fitness','care','therapy','meditation','mindful','yoga'],
|
|
32
|
+
ecommerce: ['ecommerce','shop','store','product','retail','marketplace','buy','sell','cart'],
|
|
33
|
+
education: ['education','learn','course','student','school','teach','academy','tutorial'],
|
|
34
|
+
saas: ['saas','dashboard','analytics','tool','platform','software','app','manage','workflow'],
|
|
35
|
+
creative: ['creative','design','art','portfolio','agency','studio','photographer','gallery'],
|
|
36
|
+
food: ['food','restaurant','recipe','cook','menu','delivery','cafe','bar','dining'],
|
|
37
|
+
travel: ['travel','booking','hotel','flight','destination','trip','adventure','explore'],
|
|
38
|
+
social: ['social','community','network','chat','message','connect','share','follow'],
|
|
39
|
+
ai: ['ai','artificial','intelligence','machine','learning','model','neural','gpt','llm'],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Component Detection ───
|
|
43
|
+
var COMPONENT_SIGNALS = {
|
|
44
|
+
hero: ['hero','landing','header','above fold','headline','main banner','splash'],
|
|
45
|
+
features: ['features','capabilities','what we do','benefits','why us','highlights'],
|
|
46
|
+
pricing: ['pricing','plans','tiers','cost','subscription','free trial','enterprise'],
|
|
47
|
+
testimonials: ['testimonials','reviews','quotes','customers say','social proof','trust'],
|
|
48
|
+
dashboard: ['dashboard','admin','panel','metrics','kpi','analytics','overview','stats'],
|
|
49
|
+
form: ['form','signup','login','register','contact','input','submit','subscribe'],
|
|
50
|
+
gallery: ['gallery','portfolio','showcase','grid','images','photos','work'],
|
|
51
|
+
faq: ['faq','questions','answers','help','support'],
|
|
52
|
+
cta: ['cta','call to action','get started','sign up','try','start'],
|
|
53
|
+
footer: ['footer','bottom','links','copyright'],
|
|
54
|
+
nav: ['nav','navigation','menu','header bar','topbar'],
|
|
55
|
+
stats: ['stats','numbers','metrics','counters','data points'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectSignals(text, signalMap) {
|
|
59
|
+
var lower = text.toLowerCase()
|
|
60
|
+
var scores = {}
|
|
61
|
+
for (var key in signalMap) {
|
|
62
|
+
if (signalMap.hasOwnProperty(key)) {
|
|
63
|
+
var keywords = signalMap[key]
|
|
64
|
+
var score = 0
|
|
65
|
+
for (var i = 0; i < keywords.length; i++) {
|
|
66
|
+
if (lower.indexOf(keywords[i]) !== -1) score++
|
|
67
|
+
}
|
|
68
|
+
if (score > 0) scores[key] = score
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Sort by score descending
|
|
72
|
+
var sorted = Object.keys(scores).sort(function(a, b) { return scores[b] - scores[a] })
|
|
73
|
+
return sorted
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Design Parameters from Mood ───
|
|
77
|
+
var MOOD_PARAMS = {
|
|
78
|
+
minimal: {
|
|
79
|
+
cornerRadius: 8, shadowDepth: 'sm', density: 'spacious', typeRatio: 'major2',
|
|
80
|
+
headingWeight: 'Semi Bold', bodyWeight: 'Regular', bgContrast: 'low',
|
|
81
|
+
iconStyle: 'outline', dividers: true, badgeStyle: 'subtle',
|
|
82
|
+
sectionPadding: 120, cardPadding: 32, gap: 24, headingSize: 56,
|
|
83
|
+
},
|
|
84
|
+
bold: {
|
|
85
|
+
cornerRadius: 16, shadowDepth: 'lg', density: 'normal', typeRatio: 'major3',
|
|
86
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'high',
|
|
87
|
+
iconStyle: 'filled', dividers: false, badgeStyle: 'filled',
|
|
88
|
+
sectionPadding: 96, cardPadding: 28, gap: 20, headingSize: 72,
|
|
89
|
+
},
|
|
90
|
+
playful: {
|
|
91
|
+
cornerRadius: 20, shadowDepth: 'md', density: 'normal', typeRatio: 'minor3',
|
|
92
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'medium',
|
|
93
|
+
iconStyle: 'filled', dividers: false, badgeStyle: 'pill',
|
|
94
|
+
sectionPadding: 96, cardPadding: 28, gap: 20, headingSize: 64,
|
|
95
|
+
},
|
|
96
|
+
luxury: {
|
|
97
|
+
cornerRadius: 4, shadowDepth: 'sm', density: 'spacious', typeRatio: 'perfect4',
|
|
98
|
+
headingWeight: 'Medium', bodyWeight: 'Light', bgContrast: 'low',
|
|
99
|
+
iconStyle: 'outline', dividers: true, badgeStyle: 'outline',
|
|
100
|
+
sectionPadding: 140, cardPadding: 40, gap: 32, headingSize: 60,
|
|
101
|
+
},
|
|
102
|
+
corporate: {
|
|
103
|
+
cornerRadius: 8, shadowDepth: 'md', density: 'normal', typeRatio: 'major2',
|
|
104
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'medium',
|
|
105
|
+
iconStyle: 'filled', dividers: true, badgeStyle: 'subtle',
|
|
106
|
+
sectionPadding: 96, cardPadding: 28, gap: 20, headingSize: 48,
|
|
107
|
+
},
|
|
108
|
+
dark: {
|
|
109
|
+
cornerRadius: 12, shadowDepth: 'lg', density: 'normal', typeRatio: 'major2',
|
|
110
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'high',
|
|
111
|
+
iconStyle: 'filled', dividers: false, badgeStyle: 'filled',
|
|
112
|
+
sectionPadding: 96, cardPadding: 28, gap: 20, headingSize: 64,
|
|
113
|
+
},
|
|
114
|
+
techy: {
|
|
115
|
+
cornerRadius: 8, shadowDepth: 'sm', density: 'dense', typeRatio: 'major2',
|
|
116
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'medium',
|
|
117
|
+
iconStyle: 'outline', dividers: true, badgeStyle: 'code',
|
|
118
|
+
sectionPadding: 80, cardPadding: 24, gap: 16, headingSize: 52,
|
|
119
|
+
},
|
|
120
|
+
organic: {
|
|
121
|
+
cornerRadius: 24, shadowDepth: 'md', density: 'spacious', typeRatio: 'minor3',
|
|
122
|
+
headingWeight: 'Medium', bodyWeight: 'Regular', bgContrast: 'low',
|
|
123
|
+
iconStyle: 'filled', dividers: false, badgeStyle: 'pill',
|
|
124
|
+
sectionPadding: 112, cardPadding: 32, gap: 24, headingSize: 56,
|
|
125
|
+
},
|
|
126
|
+
brutalist: {
|
|
127
|
+
cornerRadius: 0, shadowDepth: 'none', density: 'dense', typeRatio: 'perfect5',
|
|
128
|
+
headingWeight: 'Bold', bodyWeight: 'Regular', bgContrast: 'extreme',
|
|
129
|
+
iconStyle: 'none', dividers: true, badgeStyle: 'border',
|
|
130
|
+
sectionPadding: 64, cardPadding: 20, gap: 12, headingSize: 80,
|
|
131
|
+
},
|
|
132
|
+
editorial: {
|
|
133
|
+
cornerRadius: 4, shadowDepth: 'sm', density: 'spacious', typeRatio: 'major3',
|
|
134
|
+
headingWeight: 'Medium', bodyWeight: 'Regular', bgContrast: 'low',
|
|
135
|
+
iconStyle: 'none', dividers: true, badgeStyle: 'subtle',
|
|
136
|
+
sectionPadding: 120, cardPadding: 32, gap: 28, headingSize: 56,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Color Palettes by Industry ───
|
|
141
|
+
var INDUSTRY_COLORS = {
|
|
142
|
+
fintech: { brand:'#0ea5e9', accent:'#06b6d4', mode:'dark' },
|
|
143
|
+
health: { brand:'#10b981', accent:'#34d399', mode:'light' },
|
|
144
|
+
ecommerce: { brand:'#f59e0b', accent:'#fbbf24', mode:'light' },
|
|
145
|
+
education: { brand:'#6366f1', accent:'#818cf8', mode:'light' },
|
|
146
|
+
saas: { brand:'#6366f1', accent:'#a78bfa', mode:'dark' },
|
|
147
|
+
creative: { brand:'#ec4899', accent:'#f472b6', mode:'dark' },
|
|
148
|
+
food: { brand:'#ef4444', accent:'#f87171', mode:'light' },
|
|
149
|
+
travel: { brand:'#0ea5e9', accent:'#38bdf8', mode:'light' },
|
|
150
|
+
social: { brand:'#8b5cf6', accent:'#a78bfa', mode:'dark' },
|
|
151
|
+
ai: { brand:'#6366f1', accent:'#818cf8', mode:'dark' },
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Main Interpreter ───
|
|
155
|
+
export function interpretDesign(prompt) {
|
|
156
|
+
// 1. Detect mood
|
|
157
|
+
var moods = detectSignals(prompt, MOOD_SIGNALS)
|
|
158
|
+
var primaryMood = moods[0] || 'techy'
|
|
159
|
+
|
|
160
|
+
// 2. Detect industry
|
|
161
|
+
var industries = detectSignals(prompt, INDUSTRY_SIGNALS)
|
|
162
|
+
var primaryIndustry = industries[0] || 'saas'
|
|
163
|
+
|
|
164
|
+
// 3. Detect needed components
|
|
165
|
+
var components = detectSignals(prompt, COMPONENT_SIGNALS)
|
|
166
|
+
// Always include nav and footer if building a page
|
|
167
|
+
var isPage = prompt.toLowerCase().indexOf('page') !== -1 || prompt.toLowerCase().indexOf('landing') !== -1 || prompt.toLowerCase().indexOf('website') !== -1 || prompt.toLowerCase().indexOf('site') !== -1
|
|
168
|
+
if (isPage) {
|
|
169
|
+
if (components.indexOf('nav') === -1) components.unshift('nav')
|
|
170
|
+
if (components.indexOf('footer') === -1) components.push('footer')
|
|
171
|
+
// Default sections if none detected
|
|
172
|
+
if (components.length <= 2) {
|
|
173
|
+
components = ['nav', 'hero', 'features', 'cta', 'footer']
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (components.length === 0) components = ['hero']
|
|
177
|
+
|
|
178
|
+
// 4. Get design parameters from mood
|
|
179
|
+
var params = MOOD_PARAMS[primaryMood] || MOOD_PARAMS.techy
|
|
180
|
+
|
|
181
|
+
// 5. Get color from industry (or extract from prompt)
|
|
182
|
+
var colorMatch = prompt.match(/#[0-9a-fA-F]{6}/)
|
|
183
|
+
var industryColor = INDUSTRY_COLORS[primaryIndustry] || INDUSTRY_COLORS.saas
|
|
184
|
+
var brandColor = colorMatch ? colorMatch[0] : industryColor.brand
|
|
185
|
+
|
|
186
|
+
// 6. Determine mode
|
|
187
|
+
var forceDark = prompt.toLowerCase().indexOf('dark') !== -1
|
|
188
|
+
var forceLight = prompt.toLowerCase().indexOf('light') !== -1
|
|
189
|
+
var mode = forceDark ? 'dark' : (forceLight ? 'light' : industryColor.mode)
|
|
190
|
+
|
|
191
|
+
// 7. Extract content hints from prompt
|
|
192
|
+
var contentHints = extractContentHints(prompt)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
mood: primaryMood,
|
|
196
|
+
industry: primaryIndustry,
|
|
197
|
+
sections: components,
|
|
198
|
+
params: params,
|
|
199
|
+
brandColor: brandColor,
|
|
200
|
+
mode: mode,
|
|
201
|
+
content: contentHints,
|
|
202
|
+
width: 1440,
|
|
203
|
+
meta: {
|
|
204
|
+
detectedMoods: moods.slice(0, 3),
|
|
205
|
+
detectedIndustries: industries.slice(0, 3),
|
|
206
|
+
detectedComponents: components,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Content Hint Extractor ───
|
|
212
|
+
function extractContentHints(prompt) {
|
|
213
|
+
var content = {}
|
|
214
|
+
var lower = prompt.toLowerCase()
|
|
215
|
+
|
|
216
|
+
// Try to find a brand name
|
|
217
|
+
var brandMatch = prompt.match(/(?:for|called|named)\s+["']?([A-Z][a-zA-Z]+)["']?/)
|
|
218
|
+
if (brandMatch) content.brand = brandMatch[1]
|
|
219
|
+
|
|
220
|
+
// Try to find a title/heading
|
|
221
|
+
var titleMatch = prompt.match(/(?:heading|title|headline)\s+["']([^"']+)["']/)
|
|
222
|
+
if (titleMatch) content.title = titleMatch[1]
|
|
223
|
+
|
|
224
|
+
// Try to find a CTA
|
|
225
|
+
var ctaMatch = prompt.match(/(?:button|cta)\s+(?:saying?\s+)?["']([^"']+)["']/)
|
|
226
|
+
if (ctaMatch) content.cta = ctaMatch[1]
|
|
227
|
+
|
|
228
|
+
// Feature count
|
|
229
|
+
var featureCountMatch = lower.match(/(\d+)\s+features?/)
|
|
230
|
+
if (featureCountMatch) content.featureCount = parseInt(featureCountMatch[1])
|
|
231
|
+
|
|
232
|
+
// Pricing tiers
|
|
233
|
+
var tierCountMatch = lower.match(/(\d+)\s+(?:pricing\s+)?tiers?/)
|
|
234
|
+
if (tierCountMatch) content.tierCount = parseInt(tierCountMatch[1])
|
|
235
|
+
|
|
236
|
+
return content
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Apply Design Params to Section ───
|
|
240
|
+
// This modifies the section composition based on interpreted mood
|
|
241
|
+
export function applyDesignParams(sectionCmds, params) {
|
|
242
|
+
for (var i = 0; i < sectionCmds.length; i++) {
|
|
243
|
+
var cmd = sectionCmds[i]
|
|
244
|
+
if (cmd.type === 'create_frame' && cmd.data) {
|
|
245
|
+
// Apply corner radius from mood
|
|
246
|
+
if (cmd.data.cornerRadius !== undefined && cmd.data.cornerRadius > 0) {
|
|
247
|
+
cmd.data.cornerRadius = params.cornerRadius
|
|
248
|
+
}
|
|
249
|
+
// Apply density
|
|
250
|
+
if (params.density === 'spacious') {
|
|
251
|
+
if (cmd.data.paddingTop && cmd.data.paddingTop >= 48) cmd.data.paddingTop = snap(cmd.data.paddingTop * 1.3)
|
|
252
|
+
if (cmd.data.paddingBottom && cmd.data.paddingBottom >= 48) cmd.data.paddingBottom = snap(cmd.data.paddingBottom * 1.3)
|
|
253
|
+
if (cmd.data.gap && cmd.data.gap >= 16) cmd.data.gap = snap(cmd.data.gap * 1.2)
|
|
254
|
+
} else if (params.density === 'dense') {
|
|
255
|
+
if (cmd.data.paddingTop && cmd.data.paddingTop >= 48) cmd.data.paddingTop = snap(cmd.data.paddingTop * 0.75)
|
|
256
|
+
if (cmd.data.paddingBottom && cmd.data.paddingBottom >= 48) cmd.data.paddingBottom = snap(cmd.data.paddingBottom * 0.75)
|
|
257
|
+
if (cmd.data.gap && cmd.data.gap >= 16) cmd.data.gap = snap(cmd.data.gap * 0.8)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Apply heading weight from mood
|
|
261
|
+
if (cmd.type === 'create_text' && cmd.data && cmd.data.fontName) {
|
|
262
|
+
if (cmd.data.fontSize >= 40 && cmd.data.fontName.style === 'Bold') {
|
|
263
|
+
cmd.data.fontName.style = params.headingWeight
|
|
264
|
+
}
|
|
265
|
+
// Apply heading size for hero headings
|
|
266
|
+
if (cmd.data.fontSize >= 56) {
|
|
267
|
+
cmd.data.fontSize = params.headingSize
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return sectionCmds
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Get Interpretation Summary ───
|
|
275
|
+
// Returns a human-readable description of what the interpreter decided
|
|
276
|
+
export function getInterpretationSummary(interpretation) {
|
|
277
|
+
var i = interpretation
|
|
278
|
+
return {
|
|
279
|
+
summary: 'Detected ' + i.mood + ' mood for ' + i.industry + ' industry. ' +
|
|
280
|
+
'Building ' + i.sections.length + ' sections: ' + i.sections.join(', ') + '. ' +
|
|
281
|
+
'Brand color: ' + i.brandColor + '. Mode: ' + i.mode + '.',
|
|
282
|
+
mood: i.mood,
|
|
283
|
+
industry: i.industry,
|
|
284
|
+
sections: i.sections,
|
|
285
|
+
brandColor: i.brandColor,
|
|
286
|
+
mode: i.mode,
|
|
287
|
+
params: {
|
|
288
|
+
cornerRadius: i.params.cornerRadius + 'px',
|
|
289
|
+
shadowDepth: i.params.shadowDepth,
|
|
290
|
+
density: i.params.density,
|
|
291
|
+
typeRatio: i.params.typeRatio,
|
|
292
|
+
headingSize: i.params.headingSize + 'px',
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/server.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR v3 — MCP Server (stdio)
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
|
|
5
1
|
import { TOOL_LIST, TOOL_COUNT, getTool, CATEGORIES } from './tools/registry.js'
|
|
6
2
|
import { handleTool } from './tools/handlers.js'
|
|
7
3
|
import { createBridge } from './bridge.js'
|
|
8
4
|
|
|
9
|
-
var VERSION = '3.0.
|
|
5
|
+
var VERSION = '3.0.4'
|
|
10
6
|
var bridge = null
|
|
11
7
|
var bridgeStarted = false
|
|
12
8
|
|
|
@@ -21,34 +17,44 @@ function ensureBridge() {
|
|
|
21
17
|
return bridge
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
if (process.stdout._handle && process.stdout._handle.setBlocking) {
|
|
21
|
+
process.stdout._handle.setBlocking(true)
|
|
22
|
+
}
|
|
26
23
|
|
|
24
|
+
var buffer = ''
|
|
27
25
|
process.stdin.setEncoding('utf8')
|
|
28
26
|
process.stdin.on('data', function(chunk) {
|
|
29
27
|
buffer += chunk
|
|
28
|
+
processBuffer()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function processBuffer() {
|
|
30
32
|
while (true) {
|
|
31
|
-
var
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} catch (e) { log('Parse error:', e.message) }
|
|
33
|
+
var nlIdx = buffer.indexOf('\n')
|
|
34
|
+
if (nlIdx === -1) {
|
|
35
|
+
if (buffer.length > 0 && buffer[0] === '{') {
|
|
36
|
+
try { JSON.parse(buffer) } catch(e) { break }
|
|
37
|
+
tryParse(buffer)
|
|
38
|
+
buffer = ''
|
|
39
|
+
}
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
var line = buffer.slice(0, nlIdx).trim()
|
|
43
|
+
buffer = buffer.slice(nlIdx + 1)
|
|
44
|
+
if (line.length === 0) continue
|
|
45
|
+
if (line[0] === '{') tryParse(line)
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tryParse(str) {
|
|
50
|
+
try {
|
|
51
|
+
var msg = JSON.parse(str)
|
|
52
|
+
handleMessage(msg)
|
|
53
|
+
} catch (e) { log('Parse error:', e.message) }
|
|
54
|
+
}
|
|
47
55
|
|
|
48
56
|
function send(msg) {
|
|
49
|
-
|
|
50
|
-
var out = 'Content-Length: ' + Buffer.byteLength(json) + '\r\n\r\n' + json
|
|
51
|
-
process.stdout.write(out)
|
|
57
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
function respond(id, result) { send({ jsonrpc: '2.0', id: id, result: result }) }
|
|
@@ -57,21 +63,23 @@ function respondError(id, code, message) { send({ jsonrpc: '2.0', id: id, error:
|
|
|
57
63
|
function handleMessage(msg) {
|
|
58
64
|
var id = msg.id
|
|
59
65
|
var method = msg.method
|
|
60
|
-
var params = msg.params
|
|
66
|
+
var params = msg.params || {}
|
|
67
|
+
|
|
68
|
+
log('<- ' + method + (id !== undefined ? ' #' + id : ''))
|
|
61
69
|
|
|
62
70
|
switch (method) {
|
|
63
71
|
case 'initialize':
|
|
64
|
-
// Start bridge in background, respond immediately
|
|
65
72
|
ensureBridge()
|
|
66
73
|
log('Conductor v' + VERSION + ' ready — ' + TOOL_COUNT + ' tools')
|
|
67
74
|
respond(id, {
|
|
68
|
-
protocolVersion: '2024-11-05',
|
|
75
|
+
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
69
76
|
capabilities: { tools: { listChanged: false } },
|
|
70
77
|
serverInfo: { name: 'conductor-figma', version: VERSION },
|
|
71
78
|
})
|
|
72
79
|
return
|
|
73
80
|
|
|
74
81
|
case 'notifications/initialized':
|
|
82
|
+
case 'notifications/cancelled':
|
|
75
83
|
return
|
|
76
84
|
|
|
77
85
|
case 'tools/list':
|
|
@@ -107,7 +115,7 @@ function handleMessage(msg) {
|
|
|
107
115
|
return
|
|
108
116
|
|
|
109
117
|
default:
|
|
110
|
-
if (id) respondError(id, -32601, 'Unknown method: ' + method)
|
|
118
|
+
if (id !== undefined) respondError(id, -32601, 'Unknown method: ' + method)
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
|
package/src/tools/handlers.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
checkContrast, auditAccessibility, getDesignCraftGuide, resolveFontWeight,
|
|
8
8
|
hexToFigmaColor, linearGradient, radialGradient, SPACING, RADIUS, SHADOWS,
|
|
9
9
|
} from '../design/intelligence.js'
|
|
10
|
+
import { composeSmartComponent, composeSection, composePage, runSequence } from '../design/composer.js'
|
|
11
|
+
import { interpretDesign, applyDesignParams, getInterpretationSummary } from '../design/interpreter.js'
|
|
10
12
|
|
|
11
13
|
// ─── Icon SVG Library ───
|
|
12
14
|
const ICONS = {
|
|
@@ -58,6 +60,7 @@ export async function handleTool(name, args, bridge) {
|
|
|
58
60
|
case 'check_contrast': return checkContrast(args.foreground, args.background)
|
|
59
61
|
case 'suggest_color_palette': return semanticColors(args.brandColor, args.mode || 'dark')
|
|
60
62
|
case 'suggest_type_scale': return typeScale(args.baseSize || 16, args.ratio || 'major2')
|
|
63
|
+
case 'interpret_prompt': return getInterpretationSummary(interpretDesign(args.prompt))
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// Everything else needs Figma
|
|
@@ -65,6 +68,89 @@ export async function handleTool(name, args, bridge) {
|
|
|
65
68
|
throw new Error('Figma plugin not connected. Open Figma → Plugins → Development → Conductor, then try again.')
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
// ─── Design from prompt: the big one ───
|
|
72
|
+
if (name === 'design_from_prompt') {
|
|
73
|
+
var interpretation = interpretDesign(args.prompt)
|
|
74
|
+
var params = interpretation.params
|
|
75
|
+
var brandColor = interpretation.brandColor
|
|
76
|
+
var mode = interpretation.mode
|
|
77
|
+
var W = args.width || interpretation.width
|
|
78
|
+
|
|
79
|
+
// Create page root
|
|
80
|
+
var pageColors = semanticColors(brandColor, mode)
|
|
81
|
+
var rootResult = await bridge.send('create_frame', {
|
|
82
|
+
name: interpretation.industry + ' — ' + interpretation.mood,
|
|
83
|
+
direction: 'VERTICAL', width: W, gap: 0,
|
|
84
|
+
fill: pageColors.bg, primaryAxisSizingMode: 'HUG'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
var totalElements = 1
|
|
88
|
+
var sectionResults = []
|
|
89
|
+
|
|
90
|
+
// Build each detected section
|
|
91
|
+
for (var di = 0; di < interpretation.sections.length; di++) {
|
|
92
|
+
var secType = interpretation.sections[di]
|
|
93
|
+
var secCmds = composeSection(secType, interpretation.content, brandColor, mode, W)
|
|
94
|
+
if (secCmds && secCmds.length > 0) {
|
|
95
|
+
// Apply mood-based design params
|
|
96
|
+
secCmds = applyDesignParams(secCmds, params)
|
|
97
|
+
// Parent first element to page root
|
|
98
|
+
secCmds[0].data.parentId = rootResult.id
|
|
99
|
+
var secRes = await runSequence(bridge, secCmds)
|
|
100
|
+
totalElements += secRes.length
|
|
101
|
+
sectionResults.push({ section: secType, elements: secRes.length })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
interpretation: getInterpretationSummary(interpretation),
|
|
107
|
+
created: {
|
|
108
|
+
rootId: rootResult.id,
|
|
109
|
+
totalElements: totalElements,
|
|
110
|
+
sections: sectionResults,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Composition commands: generate multi-element designs ───
|
|
116
|
+
if (name === 'create_smart_component') {
|
|
117
|
+
var compCmds = composeSmartComponent(args.type, args.variant, args.label, args.brandColor, args.mode)
|
|
118
|
+
if (!compCmds) throw new Error('Unknown component type: ' + args.type)
|
|
119
|
+
var compResults = await runSequence(bridge, compCmds)
|
|
120
|
+
return { created: args.type, variant: args.variant || 'default', elements: compResults.length, rootId: compResults[0] && compResults[0].id }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (name === 'create_section') {
|
|
124
|
+
var secCmds = composeSection(args.type, args.content, args.brandColor, args.mode, args.width)
|
|
125
|
+
if (!secCmds || secCmds.length === 0) throw new Error('Unknown section type: ' + args.type)
|
|
126
|
+
var secResults = await runSequence(bridge, secCmds)
|
|
127
|
+
return { created: args.type + ' section', elements: secResults.length, rootId: secResults[0] && secResults[0].id }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (name === 'create_page') {
|
|
131
|
+
var pageSpec = composePage(args.type, args.content, args.brandColor, args.mode, args.width)
|
|
132
|
+
// Create page root frame
|
|
133
|
+
var rootResult = await bridge.send('create_frame', {
|
|
134
|
+
name: (args.type || 'Page').charAt(0).toUpperCase() + (args.type || 'page').slice(1) + ' Page',
|
|
135
|
+
direction: 'VERTICAL', width: pageSpec.width, gap: 0,
|
|
136
|
+
fill: semanticColors(pageSpec.brand, pageSpec.mode || 'dark').bg,
|
|
137
|
+
primaryAxisSizingMode: 'HUG'
|
|
138
|
+
})
|
|
139
|
+
// Build each section inside the page
|
|
140
|
+
var totalElements = 1
|
|
141
|
+
for (var si = 0; si < pageSpec.sections.length; si++) {
|
|
142
|
+
var secType = pageSpec.sections[si]
|
|
143
|
+
var secCmds2 = composeSection(secType, pageSpec.content, pageSpec.brand, pageSpec.mode, pageSpec.width)
|
|
144
|
+
if (secCmds2 && secCmds2.length > 0) {
|
|
145
|
+
// Set first command's parent to page root
|
|
146
|
+
secCmds2[0].data.parentId = rootResult.id
|
|
147
|
+
var secRes = await runSequence(bridge, secCmds2)
|
|
148
|
+
totalElements += secRes.length
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { created: args.type + ' page', sections: pageSpec.sections, elements: totalElements, rootId: rootResult.id }
|
|
152
|
+
}
|
|
153
|
+
|
|
68
154
|
// Apply design intelligence to args before sending
|
|
69
155
|
const enhanced = enhanceWithIntelligence(name, args)
|
|
70
156
|
|
package/src/tools/registry.js
CHANGED
|
@@ -1150,3 +1150,25 @@ export const CATEGORIES = {
|
|
|
1150
1150
|
|
|
1151
1151
|
export function getTool(name) { return ALL_TOOLS[name] || null }
|
|
1152
1152
|
export function getToolsByCategory(cat) { return CATEGORIES[cat] || [] }
|
|
1153
|
+
|
|
1154
|
+
// ═══ DESIGN FROM PROMPT (1) ═══
|
|
1155
|
+
const INTERPRET = {
|
|
1156
|
+
design_from_prompt: tool('design_from_prompt', 'Takes any natural language description and generates a complete Figma design. Analyzes mood (minimal, bold, playful, luxury, corporate, techy, organic, brutalist, editorial), detects industry (fintech, health, saas, ecommerce, etc.), picks appropriate colors, spacing density, shadow depth, corner radii, and typography. Then composes a multi-section design with 30-300+ elements.', {
|
|
1157
|
+
prompt: req('string', 'Natural language description. Examples: "A dark fintech dashboard with metrics and charts", "A playful education landing page with pricing", "A minimal luxury brand site for a candle company called Ember"'),
|
|
1158
|
+
width: opt('number', 'Frame width', 1440),
|
|
1159
|
+
}, 'interpret'),
|
|
1160
|
+
|
|
1161
|
+
interpret_prompt: tool('interpret_prompt', 'Analyzes a design prompt and returns the interpretation without generating anything. Shows what mood, industry, colors, and sections would be used. Good for previewing before generating.', {
|
|
1162
|
+
prompt: req('string', 'Design description to analyze'),
|
|
1163
|
+
}, 'interpret'),
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Update ALL_TOOLS
|
|
1167
|
+
Object.assign(ALL_TOOLS, INTERPRET)
|
|
1168
|
+
|
|
1169
|
+
// Refresh counts
|
|
1170
|
+
const _newList = Object.values(ALL_TOOLS)
|
|
1171
|
+
const _newCount = Object.keys(ALL_TOOLS).length
|
|
1172
|
+
|
|
1173
|
+
// Add category
|
|
1174
|
+
CATEGORIES.interpret = Object.keys(INTERPRET)
|