conductor-figma 3.3.2 → 3.3.3

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.
@@ -123,8 +123,9 @@ async function executeCommand(cmd, data) {
123
123
  frame.name = data.name || 'Frame'
124
124
  if (data.width) frame.resize(data.width, data.height || data.width)
125
125
  frame.layoutMode = data.direction || 'VERTICAL'
126
- frame.primaryAxisSizingMode = data.primaryAxisSizingMode === 'FILL' ? 'FIXED' : (data.primaryAxisSizingMode || 'HUG')
127
- frame.counterAxisSizingMode = data.counterAxisSizingMode || 'HUG'
126
+ frame.primaryAxisSizingMode = data.primaryAxisSizingMode === 'FILL' ? 'FIXED' : (data.primaryAxisSizingMode === 'FIXED' ? 'FIXED' : 'AUTO')
127
+ frame.counterAxisSizingMode = data.counterAxisSizingMode === 'FILL' ? 'FIXED' : (data.counterAxisSizingMode === 'FIXED' ? 'FIXED' : 'AUTO')
128
+ if (data.primaryAxisSizingMode === 'FILL') frame.layoutGrow = 1
128
129
  if (data.paddingTop !== undefined) frame.paddingTop = data.paddingTop
129
130
  if (data.paddingRight !== undefined) frame.paddingRight = data.paddingRight
130
131
  if (data.paddingBottom !== undefined) frame.paddingBottom = data.paddingBottom
@@ -303,8 +304,8 @@ async function executeCommand(cmd, data) {
303
304
  if (data.gap !== undefined) node.itemSpacing = data.gap
304
305
  if (data.primaryAxisAlignItems) node.primaryAxisAlignItems = data.primaryAxisAlignItems
305
306
  if (data.counterAxisAlignItems) node.counterAxisAlignItems = data.counterAxisAlignItems
306
- if (data.primaryAxisSizingMode) node.primaryAxisSizingMode = data.primaryAxisSizingMode === 'FILL' ? 'FIXED' : data.primaryAxisSizingMode
307
- if (data.counterAxisSizingMode) node.counterAxisSizingMode = data.counterAxisSizingMode === 'FILL' ? 'FIXED' : data.counterAxisSizingMode
307
+ if (data.primaryAxisSizingMode) node.primaryAxisSizingMode = data.primaryAxisSizingMode === 'FILL' ? 'FIXED' : (data.primaryAxisSizingMode === 'FIXED' ? 'FIXED' : 'AUTO')
308
+ if (data.counterAxisSizingMode) node.counterAxisSizingMode = data.counterAxisSizingMode === 'FILL' ? 'FIXED' : (data.counterAxisSizingMode === 'FIXED' ? 'FIXED' : 'AUTO')
308
309
  return { id: node.id, layout: 'set' }
309
310
  }
310
311
 
@@ -8,7 +8,7 @@
8
8
  "enableProposedApi": false,
9
9
  "editorType": ["figma"],
10
10
  "networkAccess": {
11
- "allowedDomains": ["none"],
12
- "reasoning": "Conductor connects to a local WebSocket server only"
11
+ "allowedDomains": ["*"],
12
+ "reasoning": "Connects to local MCP WebSocket server on 127.0.0.1:3055"
13
13
  }
14
14
  }
@@ -39,7 +39,7 @@ function setStatus(connected) {
39
39
 
40
40
  function connect() {
41
41
  try {
42
- ws = new WebSocket(`ws://localhost:${port}`);
42
+ ws = new WebSocket(`ws://127.0.0.1:${port}`);
43
43
  ws.onopen = () => {
44
44
  setStatus(true);
45
45
  log('Connected to MCP server');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-figma",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "Design-intelligent MCP server for Figma. 201 design-intelligent tools for Figma. Every tool knows typography, spacing, color, accessibility. Not a shape proxy — a design engine.",
5
5
  "author": "0xDragoon",
6
6
  "license": "MIT",
package/src/bridge.js CHANGED
@@ -1,47 +1,100 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR v3 — WebSocket Bridge
2
+ // CONDUCTOR v3.3 — WebSocket Bridge
3
3
  // ═══════════════════════════════════════════
4
4
 
5
- import { WebSocketServer } from 'ws'
5
+ import { WebSocketServer, WebSocket } from 'ws'
6
6
 
7
7
  function log() { process.stderr.write('[bridge] ' + Array.prototype.join.call(arguments, ' ') + '\n') }
8
8
 
9
9
  export function createBridge(port) {
10
10
  port = port || parseInt(process.env.CONDUCTOR_PORT) || 3055
11
11
  var wss = null
12
- var client = null
12
+ var figmaClient = null
13
+ var proxyConn = null
13
14
  var pending = new Map()
14
- var msgId = 0
15
+ var msgId = 1000
15
16
  var isOwner = false
17
+ var isProxy = false
16
18
 
17
19
  function start() {
18
20
  return new Promise(function(resolve) {
19
21
  try {
20
- wss = new WebSocketServer({ port: port })
21
- wss.on('listening', function() { isOwner = true; log('WebSocket server on port ' + port); resolve() })
22
+ wss = new WebSocketServer({ port: port, host: '0.0.0.0' })
23
+ wss.on('listening', function() {
24
+ isOwner = true
25
+ log('WebSocket server on port ' + port)
26
+ resolve()
27
+ })
22
28
  wss.on('connection', function(ws) {
23
- client = ws
24
- log('Figma plugin connected')
25
- ws.on('message', function(data) {
29
+ var role = null // 'figma' or 'proxy'
30
+
31
+ ws.on('message', function(rawData) {
26
32
  try {
27
- var msg = JSON.parse(data.toString())
33
+ var msg = JSON.parse(rawData.toString())
34
+
35
+ // Proxy identification
36
+ if (msg._identify === 'proxy') {
37
+ role = 'proxy'
38
+ log('Proxy client identified')
39
+ return
40
+ }
41
+
42
+ // Proxy forwarding a command to Figma
43
+ if (msg._proxy && msg.command) {
44
+ if (role !== 'proxy') { role = 'proxy'; log('Proxy client identified (via command)') }
45
+ if (figmaClient && figmaClient.readyState === 1) {
46
+ pending.set(msg.id, { ws: ws, type: 'proxy' })
47
+ figmaClient.send(JSON.stringify({ id: msg.id, command: msg.command, data: msg.data }))
48
+ } else {
49
+ ws.send(JSON.stringify({ id: msg.id, error: 'Figma plugin not connected' }))
50
+ }
51
+ return
52
+ }
53
+
54
+ // Response from Figma
28
55
  if (msg.id && pending.has(msg.id)) {
29
56
  var p = pending.get(msg.id)
30
- clearTimeout(p.timer)
31
57
  pending.delete(msg.id)
32
- if (msg.error) p.reject(new Error(msg.error))
33
- else p.resolve(msg.result)
58
+ if (p.type === 'proxy') {
59
+ // Route back to proxy
60
+ if (p.ws && p.ws.readyState === 1) p.ws.send(rawData.toString())
61
+ } else if (p.type === 'local') {
62
+ clearTimeout(p.timer)
63
+ if (msg.error) p.reject(new Error(msg.error))
64
+ else p.resolve(msg.result)
65
+ }
66
+ return
67
+ }
68
+
69
+ // If we get here with a result/error, it's Figma responding
70
+ if (!role) {
71
+ role = 'figma'
72
+ figmaClient = ws
73
+ log('Figma plugin connected')
34
74
  }
35
75
  } catch (e) { log('Parse error:', e.message) }
36
76
  })
37
- ws.on('close', function() { client = null; log('Figma plugin disconnected') })
77
+
78
+ ws.on('close', function() {
79
+ if (role === 'figma') { figmaClient = null; log('Figma plugin disconnected') }
80
+ if (role === 'proxy') { log('Proxy client disconnected') }
81
+ })
38
82
  ws.on('error', function(e) { log('WS error:', e.message) })
83
+
84
+ // Auto-identify as Figma if no message in 3s (Figma sends nothing on connect)
85
+ setTimeout(function() {
86
+ if (!role) {
87
+ role = 'figma'
88
+ figmaClient = ws
89
+ log('Figma plugin connected (auto)')
90
+ }
91
+ }, 3000)
39
92
  })
40
93
  wss.on('error', function(e) {
41
94
  if (e.code === 'EADDRINUSE') {
42
- log('Port ' + port + ' in use — another Conductor instance is running. This instance will handle MCP only.')
95
+ log('Port ' + port + ' in use — connecting as proxy')
43
96
  wss = null
44
- resolve()
97
+ connectAsProxy(resolve)
45
98
  } else {
46
99
  log('Server error:', e.message)
47
100
  resolve()
@@ -54,21 +107,61 @@ export function createBridge(port) {
54
107
  })
55
108
  }
56
109
 
57
- function stop() { if (wss && isOwner) { wss.close(); wss = null } }
110
+ function connectAsProxy(resolve) {
111
+ try {
112
+ proxyConn = new WebSocket('ws://127.0.0.1:' + port)
113
+ proxyConn.on('open', function() {
114
+ isProxy = true
115
+ // Identify ourselves immediately so server doesn't think we're Figma
116
+ proxyConn.send(JSON.stringify({ _identify: 'proxy' }))
117
+ log('Proxy connected to server on port ' + port)
118
+ resolve()
119
+ })
120
+ proxyConn.on('message', function(data) {
121
+ try {
122
+ var msg = JSON.parse(data.toString())
123
+ if (msg.id && pending.has(msg.id)) {
124
+ var p = pending.get(msg.id)
125
+ clearTimeout(p.timer)
126
+ pending.delete(msg.id)
127
+ if (msg.error) p.reject(new Error(msg.error))
128
+ else p.resolve(msg.result)
129
+ }
130
+ } catch (e) { log('Proxy parse error:', e.message) }
131
+ })
132
+ proxyConn.on('close', function() { isProxy = false; proxyConn = null; log('Proxy disconnected') })
133
+ proxyConn.on('error', function(e) { log('Proxy error:', e.message); isProxy = false; proxyConn = null; resolve() })
134
+ } catch (e) { log('Proxy connect error:', e.message); resolve() }
135
+ }
136
+
137
+ function stop() {
138
+ if (wss && isOwner) { wss.close(); wss = null }
139
+ if (proxyConn) { proxyConn.close(); proxyConn = null }
140
+ }
58
141
 
59
- function isConnected() { return client !== null && client.readyState === 1 }
142
+ function isConnected() {
143
+ if (isOwner) return figmaClient !== null && figmaClient.readyState === 1
144
+ if (isProxy) return proxyConn !== null && proxyConn.readyState === 1
145
+ return false
146
+ }
60
147
 
61
148
  function send(command, data, timeout) {
62
- timeout = timeout || 15000
149
+ timeout = timeout || 60000
63
150
  return new Promise(function(resolve, reject) {
64
- if (!isConnected()) return reject(new Error('Figma plugin not connected. Run the Conductor plugin in Figma, and make sure the WebSocket server is running on port ' + port))
151
+ if (!isConnected()) return reject(new Error('Figma plugin not connected'))
65
152
  var id = ++msgId
66
153
  var timer = setTimeout(function() {
67
154
  pending.delete(id)
68
- reject(new Error('Timeout waiting for Figma response (' + timeout + 'ms)'))
155
+ reject(new Error('Timeout (' + timeout + 'ms)'))
69
156
  }, timeout)
70
- pending.set(id, { resolve: resolve, reject: reject, timer: timer })
71
- client.send(JSON.stringify({ id: id, command: command, data: data }))
157
+
158
+ if (isOwner && figmaClient) {
159
+ pending.set(id, { resolve: resolve, reject: reject, timer: timer, type: 'local' })
160
+ figmaClient.send(JSON.stringify({ id: id, command: command, data: data }))
161
+ } else if (isProxy && proxyConn) {
162
+ pending.set(id, { resolve: resolve, reject: reject, timer: timer, type: 'local' })
163
+ proxyConn.send(JSON.stringify({ id: id, command: command, data: data, _proxy: true }))
164
+ }
72
165
  })
73
166
  }
74
167
 
package/src/server.js CHANGED
@@ -2,18 +2,14 @@ import { TOOL_LIST, TOOL_COUNT, getTool, CATEGORIES } from './tools/registry.js'
2
2
  import { handleTool } from './tools/handlers.js'
3
3
  import { createBridge } from './bridge.js'
4
4
 
5
- var VERSION = '3.0.4'
6
- var bridge = null
7
- var bridgeStarted = false
5
+ var VERSION = '3.3.1'
6
+ var bridge = createBridge()
7
+ bridge.start()
8
+ var bridgeStarted = true
8
9
 
9
10
  function log() { process.stderr.write('[conductor] ' + Array.prototype.join.call(arguments, ' ') + '\n') }
10
11
 
11
12
  function ensureBridge() {
12
- if (!bridgeStarted) {
13
- bridgeStarted = true
14
- bridge = createBridge()
15
- bridge.start()
16
- }
17
13
  return bridge
18
14
  }
19
15
 
@@ -10,6 +10,8 @@ import {
10
10
  import { composeSmartComponent, composeSection, composePage, runSequence } from '../design/composer.js'
11
11
  import { interpretDesign, applyDesignParams, getInterpretationSummary } from '../design/interpreter.js'
12
12
 
13
+ function log() { process.stderr.write('[conductor] ' + Array.prototype.join.call(arguments, ' ') + '\n') }
14
+
13
15
  // ─── Icon SVG Library ───
14
16
  const ICONS = {
15
17
  'arrow-right': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
@@ -78,27 +80,45 @@ export async function handleTool(name, args, bridge) {
78
80
 
79
81
  // Create page root
80
82
  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
- })
83
+ log('Creating root frame...')
84
+ var rootResult
85
+ try {
86
+ rootResult = await bridge.send('create_frame', {
87
+ name: interpretation.industry + ' — ' + interpretation.mood,
88
+ direction: 'VERTICAL', width: W, gap: 0,
89
+ fill: pageColors.bg
90
+ })
91
+ } catch (e) {
92
+ log('Root frame error:', e.message)
93
+ throw new Error('Failed to create root frame: ' + e.message)
94
+ }
86
95
 
96
+ if (!rootResult || !rootResult.id) {
97
+ log('Root result invalid:', JSON.stringify(rootResult))
98
+ throw new Error('Root frame created but no ID returned. Response: ' + JSON.stringify(rootResult))
99
+ }
100
+
101
+ log('Root frame created:', rootResult.id)
87
102
  var totalElements = 1
88
103
  var sectionResults = []
89
104
 
90
105
  // Build each detected section
91
106
  for (var di = 0; di < interpretation.sections.length; di++) {
92
107
  var secType = interpretation.sections[di]
108
+ log('Building section:', secType)
93
109
  var secCmds = composeSection(secType, interpretation.content, brandColor, mode, W)
94
110
  if (secCmds && secCmds.length > 0) {
95
- // Apply mood-based design params
96
111
  secCmds = applyDesignParams(secCmds, params)
97
- // Parent first element to page root
98
112
  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 })
113
+ try {
114
+ var secRes = await runSequence(bridge, secCmds)
115
+ totalElements += secRes.length
116
+ sectionResults.push({ section: secType, elements: secRes.length })
117
+ log('Section ' + secType + ': ' + secRes.length + ' elements')
118
+ } catch (e) {
119
+ log('Section ' + secType + ' error:', e.message)
120
+ sectionResults.push({ section: secType, error: e.message })
121
+ }
102
122
  }
103
123
  }
104
124
 
@@ -133,8 +153,7 @@ export async function handleTool(name, args, bridge) {
133
153
  var rootResult = await bridge.send('create_frame', {
134
154
  name: (args.type || 'Page').charAt(0).toUpperCase() + (args.type || 'page').slice(1) + ' Page',
135
155
  direction: 'VERTICAL', width: pageSpec.width, gap: 0,
136
- fill: semanticColors(pageSpec.brand, pageSpec.mode || 'dark').bg,
137
- primaryAxisSizingMode: 'HUG'
156
+ fill: semanticColors(pageSpec.brand, pageSpec.mode || 'dark').bg
138
157
  })
139
158
  // Build each section inside the page
140
159
  var totalElements = 1
@@ -1116,6 +1116,18 @@ const EFFECTS = {
1116
1116
  }, 'effects'),
1117
1117
  }
1118
1118
 
1119
+ // ═══ DESIGN FROM PROMPT (2) ═══
1120
+ const INTERPRET = {
1121
+ 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.', {
1122
+ 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"'),
1123
+ width: opt('number', 'Frame width', 1440),
1124
+ }, 'interpret'),
1125
+
1126
+ 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.', {
1127
+ prompt: req('string', 'Design description to analyze'),
1128
+ }, 'interpret'),
1129
+ }
1130
+
1119
1131
  // ═══ ASSEMBLE ALL TOOLS ═══
1120
1132
  export const ALL_TOOLS = {
1121
1133
  ...CREATE, ...MODIFY, ...VECTOR, ...READ,
@@ -1123,6 +1135,7 @@ export const ALL_TOOLS = {
1123
1135
  ...BATCH, ...DESIGN_SYSTEM, ...RESPONSIVE,
1124
1136
  ...TYPOGRAPHY, ...COLOR, ...PROTOTYPE,
1125
1137
  ...PAGE, ...LIBRARY, ...ANNOTATION, ...EFFECTS,
1138
+ ...INTERPRET,
1126
1139
  }
1127
1140
 
1128
1141
  export const TOOL_LIST = Object.values(ALL_TOOLS)
@@ -1146,29 +1159,8 @@ export const CATEGORIES = {
1146
1159
  library: Object.keys(LIBRARY),
1147
1160
  annotation: Object.keys(ANNOTATION),
1148
1161
  effects: Object.keys(EFFECTS),
1162
+ interpret: Object.keys(INTERPRET),
1149
1163
  }
1150
1164
 
1151
1165
  export function getTool(name) { return ALL_TOOLS[name] || null }
1152
1166
  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)