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.
- package/figma-plugin/code.js +5 -4
- 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 +31 -12
- package/src/tools/registry.js +14 -22
package/figma-plugin/code.js
CHANGED
|
@@ -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
|
|
127
|
-
frame.counterAxisSizingMode = data.counterAxisSizingMode
|
|
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": ["
|
|
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
|
|
|
@@ -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
|
package/src/tools/registry.js
CHANGED
|
@@ -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)
|