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.
@@ -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.2'
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
- // ─── JSON-RPC over stdio ───
25
- var buffer = ''
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 headerEnd = buffer.indexOf('\r\n\r\n')
32
- if (headerEnd === -1) break
33
- var header = buffer.slice(0, headerEnd)
34
- var match = header.match(/Content-Length:\s*(\d+)/i)
35
- if (!match) { buffer = buffer.slice(headerEnd + 4); continue }
36
- var len = parseInt(match[1])
37
- var bodyStart = headerEnd + 4
38
- if (buffer.length < bodyStart + len) break
39
- var body = buffer.slice(bodyStart, bodyStart + len)
40
- buffer = buffer.slice(bodyStart + len)
41
- try {
42
- var msg = JSON.parse(body)
43
- handleMessage(msg)
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
- var json = JSON.stringify(msg)
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
 
@@ -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
 
@@ -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)