conductor-figma 3.3.1 → 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.
- package/figma-plugin/manifest.json +2 -2
- package/figma-plugin/ui.html +1 -1
- package/package.json +1 -1
- package/src/bridge.js +116 -23
- package/src/server.js +4 -8
- package/src/tools/handlers.js +30 -10
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"enableProposedApi": false,
|
|
9
9
|
"editorType": ["figma"],
|
|
10
10
|
"networkAccess": {
|
|
11
|
-
"allowedDomains": ["
|
|
12
|
-
"reasoning": "
|
|
11
|
+
"allowedDomains": ["*"],
|
|
12
|
+
"reasoning": "Connects to local MCP WebSocket server on 127.0.0.1:3055"
|
|
13
13
|
}
|
|
14
14
|
}
|
package/figma-plugin/ui.html
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-figma",
|
|
3
|
-
"version": "3.3.
|
|
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
|
|
12
|
+
var figmaClient = null
|
|
13
|
+
var proxyConn = null
|
|
13
14
|
var pending = new Map()
|
|
14
|
-
var msgId =
|
|
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() {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
ws.on('message', function(
|
|
29
|
+
var role = null // 'figma' or 'proxy'
|
|
30
|
+
|
|
31
|
+
ws.on('message', function(rawData) {
|
|
26
32
|
try {
|
|
27
|
-
var msg = JSON.parse(
|
|
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 (
|
|
33
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
|
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() {
|
|
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 ||
|
|
149
|
+
timeout = timeout || 60000
|
|
63
150
|
return new Promise(function(resolve, reject) {
|
|
64
|
-
if (!isConnected()) return reject(new Error('Figma plugin not connected
|
|
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
|
|
155
|
+
reject(new Error('Timeout (' + timeout + 'ms)'))
|
|
69
156
|
}, timeout)
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
6
|
-
var bridge =
|
|
7
|
-
|
|
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
|
|
package/src/tools/handlers.js
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|