@noleemits/vision-builder-control-mcp 4.18.1 → 4.31.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/index.js +563 -75
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Noleemits Vision Builder Control MCP Server
|
|
4
4
|
*
|
|
5
5
|
* Provides 65 tools for building and managing WordPress/Elementor sites.
|
|
6
|
-
* v4.15.0: Injection engine v2 — icon-list injection, unfilled slot clearing
|
|
7
|
-
* v4.14.0: Pipeline quality fixes — smart counter parsing,
|
|
6
|
+
* v4.15.0: Injection engine v2 — icon-list injection, unfilled slot clearing.
|
|
7
|
+
* v4.14.0: Pipeline quality fixes — smart counter parsing, post-processing, quality scores in browse, injection completeness.
|
|
8
8
|
* v4.13.0: Phase 3 DX — display_conditions, null filter fix, garbage HTML cleanup, icon key docs, auto-inject convenience docs.
|
|
9
9
|
* v4.12.0: Phase 2 tools — move_element, clean_globals, find_replace URL search, set_front_page.
|
|
10
10
|
* v4.11.0: Phase 1 fixes — token patch semantics, trigger key auto-injection (background/typography), enhanced cache clear, flex-grow/grid convenience.
|
|
@@ -104,7 +104,7 @@ process.on('SIGINT', () => {
|
|
|
104
104
|
// CONFIG
|
|
105
105
|
// ================================================================
|
|
106
106
|
|
|
107
|
-
const VERSION = '4.
|
|
107
|
+
const VERSION = '4.31.0';
|
|
108
108
|
const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
|
|
109
109
|
|
|
110
110
|
// ================================================================
|
|
@@ -944,6 +944,39 @@ function rebrandSection(section, colorMap, fontMap, globalsMap, globalsResolver)
|
|
|
944
944
|
|
|
945
945
|
// ── Content Injection ──
|
|
946
946
|
|
|
947
|
+
// ───── Elementor v4 atomic-widget helpers ─────
|
|
948
|
+
// Atomic widgets (e-heading, e-paragraph, e-button) wrap their content in
|
|
949
|
+
// Html_V3_Prop_Type / Link_Prop_Type envelopes. These helpers read/write
|
|
950
|
+
// values into those envelopes without disturbing v3 (legacy) widgets.
|
|
951
|
+
|
|
952
|
+
function setAtomicHtmlV3(widget, propKey, value) {
|
|
953
|
+
if (!widget.settings) widget.settings = {};
|
|
954
|
+
widget.settings[propKey] = {
|
|
955
|
+
$$type: 'html-v3',
|
|
956
|
+
value: {
|
|
957
|
+
content: { $$type: 'string', value: String(value) },
|
|
958
|
+
children: []
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function setAtomicLinkUrl(widget, url) {
|
|
964
|
+
if (!widget.settings) widget.settings = {};
|
|
965
|
+
if (!widget.settings.link || widget.settings.link.$$type !== 'link') {
|
|
966
|
+
widget.settings.link = { $$type: 'link', value: {} };
|
|
967
|
+
}
|
|
968
|
+
if (!widget.settings.link.value) widget.settings.link.value = {};
|
|
969
|
+
widget.settings.link.value.destination = { $$type: 'url', value: String(url) };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function getAtomicHtmlV3(widget, propKey) {
|
|
973
|
+
return widget?.settings?.[propKey]?.value?.content?.value || '';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function getAtomicLinkUrl(widget) {
|
|
977
|
+
return widget?.settings?.link?.value?.destination?.value || '';
|
|
978
|
+
}
|
|
979
|
+
|
|
947
980
|
/**
|
|
948
981
|
* Extract an ordered list of content-bearing widgets from a section tree.
|
|
949
982
|
* Each entry: { widget, path, type, role }
|
|
@@ -976,6 +1009,12 @@ function extractContentSlots(element, path = []) {
|
|
|
976
1009
|
slots.push({ widget: element, path: [...path], type: 'icon-list', items: s.icon_list || [] });
|
|
977
1010
|
} else if (wt === 'icon') {
|
|
978
1011
|
slots.push({ widget: element, path: [...path], type: 'icon', icon: s.selected_icon || {} });
|
|
1012
|
+
} else if (wt === 'e-heading') {
|
|
1013
|
+
slots.push({ widget: element, path: [...path], type: 'heading', text: getAtomicHtmlV3(element, 'title'), atomic: true });
|
|
1014
|
+
} else if (wt === 'e-paragraph') {
|
|
1015
|
+
slots.push({ widget: element, path: [...path], type: 'text', text: getAtomicHtmlV3(element, 'paragraph'), atomic: true });
|
|
1016
|
+
} else if (wt === 'e-button') {
|
|
1017
|
+
slots.push({ widget: element, path: [...path], type: 'button', text: getAtomicHtmlV3(element, 'text'), url: getAtomicLinkUrl(element), atomic: true });
|
|
979
1018
|
}
|
|
980
1019
|
}
|
|
981
1020
|
|
|
@@ -1127,10 +1166,9 @@ function structureContentBlock(block) {
|
|
|
1127
1166
|
* Deep clones the section first to avoid mutation.
|
|
1128
1167
|
*/
|
|
1129
1168
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
* - jet_parallax_layout_list: [] on all containers
|
|
1132
|
-
* - plus_tooltip_content_desc on all widgets
|
|
1169
|
+
* Enforce standard settings on a template section.
|
|
1133
1170
|
* - layout: full_width on root container
|
|
1171
|
+
* - css_classes / _css_classes duality
|
|
1134
1172
|
*/
|
|
1135
1173
|
function addBoilerplate(section, isRoot = true) {
|
|
1136
1174
|
if (!section) return section;
|
|
@@ -1138,25 +1176,71 @@ function addBoilerplate(section, isRoot = true) {
|
|
|
1138
1176
|
if (!section.settings) section.settings = {};
|
|
1139
1177
|
section.settings.layout = 'full_width';
|
|
1140
1178
|
}
|
|
1141
|
-
if (section.elType === 'container') {
|
|
1142
|
-
if (!section.settings) section.settings = {};
|
|
1143
|
-
if (!section.settings.jet_parallax_layout_list) {
|
|
1144
|
-
section.settings.jet_parallax_layout_list = [];
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
if (section.widgetType) {
|
|
1148
|
-
if (!section.settings) section.settings = {};
|
|
1149
|
-
if (!section.settings.plus_tooltip_content_desc) {
|
|
1150
|
-
// PHP validator uses empty() which treats '' as empty — needs non-empty value
|
|
1151
|
-
section.settings.plus_tooltip_content_desc = 'Luctus nec ullamcorper mattis';
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
1179
|
if (Array.isArray(section.elements)) {
|
|
1155
1180
|
section.elements.forEach(child => addBoilerplate(child, false));
|
|
1156
1181
|
}
|
|
1157
1182
|
return section;
|
|
1158
1183
|
}
|
|
1159
1184
|
|
|
1185
|
+
/**
|
|
1186
|
+
* Detect repeating "card" containers — sibling containers where each contains
|
|
1187
|
+
* exactly 1 heading + ≤1 text-editor in its subtree. Many template kits use this
|
|
1188
|
+
* pattern for services/features/pricing grids. Returns the largest card group
|
|
1189
|
+
* found (array of {container, headingWidget, textWidget}).
|
|
1190
|
+
*/
|
|
1191
|
+
function findCardPairs(element, excludeWidgets = new Set()) {
|
|
1192
|
+
function countWidgets(el, types) {
|
|
1193
|
+
const counts = {};
|
|
1194
|
+
types.forEach(t => counts[t] = 0);
|
|
1195
|
+
(function walk(e) {
|
|
1196
|
+
if (!e) return;
|
|
1197
|
+
if (e.widgetType && types.includes(e.widgetType) && !excludeWidgets.has(e)) {
|
|
1198
|
+
counts[e.widgetType] = (counts[e.widgetType] || 0) + 1;
|
|
1199
|
+
}
|
|
1200
|
+
if (Array.isArray(e.elements)) e.elements.forEach(walk);
|
|
1201
|
+
})(el);
|
|
1202
|
+
return counts;
|
|
1203
|
+
}
|
|
1204
|
+
function findFirst(el, type) {
|
|
1205
|
+
if (!el) return null;
|
|
1206
|
+
if (el.widgetType === type && !excludeWidgets.has(el)) return el;
|
|
1207
|
+
if (Array.isArray(el.elements)) {
|
|
1208
|
+
for (const child of el.elements) {
|
|
1209
|
+
const found = findFirst(child, type);
|
|
1210
|
+
if (found) return found;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Global scan: collect all card-like containers anywhere in the tree
|
|
1217
|
+
const allCards = [];
|
|
1218
|
+
(function walk(el) {
|
|
1219
|
+
if (!el) return;
|
|
1220
|
+
if (el.elType === 'container') {
|
|
1221
|
+
const counts = countWidgets(el, ['heading', 'text-editor', 'icon-box', 'testimonial', 'counter', 'toggle']);
|
|
1222
|
+
// Card pattern: exactly 1 heading, ≤1 text-editor, no icon-box/testimonial/counter/toggle
|
|
1223
|
+
// (those have their own injection paths)
|
|
1224
|
+
if (counts['heading'] === 1 &&
|
|
1225
|
+
counts['text-editor'] <= 1 &&
|
|
1226
|
+
counts['icon-box'] === 0 &&
|
|
1227
|
+
counts['testimonial'] === 0 &&
|
|
1228
|
+
counts['counter'] === 0 &&
|
|
1229
|
+
counts['toggle'] === 0) {
|
|
1230
|
+
allCards.push({
|
|
1231
|
+
container: el,
|
|
1232
|
+
headingWidget: findFirst(el, 'heading'),
|
|
1233
|
+
textWidget: findFirst(el, 'text-editor'),
|
|
1234
|
+
});
|
|
1235
|
+
return; // don't recurse into a matched card — its subtree is consumed
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (Array.isArray(el.elements)) el.elements.forEach(walk);
|
|
1239
|
+
})(element);
|
|
1240
|
+
|
|
1241
|
+
return allCards;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1160
1244
|
function injectContent(section, content) {
|
|
1161
1245
|
const injected = JSON.parse(JSON.stringify(section));
|
|
1162
1246
|
const slots = extractContentSlots(injected);
|
|
@@ -1175,21 +1259,78 @@ function injectContent(section, content) {
|
|
|
1175
1259
|
const paragraphs = content.paragraphs || [];
|
|
1176
1260
|
const quotes = content.quotes || [];
|
|
1177
1261
|
const faq = content.faq || [];
|
|
1262
|
+
const items = (content.items || []);
|
|
1178
1263
|
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1264
|
+
// Track widgets already consumed by card-pair injection so later loops skip them.
|
|
1265
|
+
const consumed = new Set();
|
|
1266
|
+
|
|
1267
|
+
// Identify the main heading slot (h1/h2 or first) so card detection can skip it.
|
|
1268
|
+
let mainHeadingIdx = -1;
|
|
1269
|
+
if (headings.length > 0) {
|
|
1270
|
+
mainHeadingIdx = headings.findIndex(h => {
|
|
1183
1271
|
const size = h.widget.settings?.header_size;
|
|
1184
1272
|
return size === 'h1' || size === 'h2' || !size;
|
|
1185
1273
|
});
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1274
|
+
if (mainHeadingIdx < 0) mainHeadingIdx = 0;
|
|
1275
|
+
}
|
|
1276
|
+
const mainHeadingWidget = mainHeadingIdx >= 0 ? headings[mainHeadingIdx].widget : null;
|
|
1277
|
+
|
|
1278
|
+
// ── Card-pair injection ──
|
|
1279
|
+
// When a section uses the "card grid" pattern (sibling containers each with
|
|
1280
|
+
// 1 heading + ≤1 text-editor), distribute content.items into those cards.
|
|
1281
|
+
// This runs before icon-list/icon-box injection to claim the proper slots.
|
|
1282
|
+
const excludeFromCards = new Set();
|
|
1283
|
+
if (mainHeadingWidget) excludeFromCards.add(mainHeadingWidget);
|
|
1284
|
+
const cardPairs = items.length > 0 ? findCardPairs(injected, excludeFromCards) : [];
|
|
1285
|
+
const useCards = cardPairs.length >= items.length &&
|
|
1286
|
+
iconBoxes.length === 0 &&
|
|
1287
|
+
imageBoxes.length === 0;
|
|
1288
|
+
if (useCards) {
|
|
1289
|
+
const HIDDEN_CLASSES = 'elementor-hidden-desktop elementor-hidden-tablet elementor-hidden-mobile';
|
|
1290
|
+
for (let i = 0; i < cardPairs.length; i++) {
|
|
1291
|
+
const { container, headingWidget, textWidget } = cardPairs[i];
|
|
1292
|
+
if (i < items.length) {
|
|
1293
|
+
if (headingWidget) {
|
|
1294
|
+
headingWidget.settings.title = items[i].title;
|
|
1295
|
+
consumed.add(headingWidget);
|
|
1296
|
+
changes++;
|
|
1297
|
+
}
|
|
1298
|
+
if (textWidget) {
|
|
1299
|
+
textWidget.settings.editor = items[i].description
|
|
1300
|
+
? `<p>${items[i].description}</p>`
|
|
1301
|
+
: '';
|
|
1302
|
+
consumed.add(textWidget);
|
|
1303
|
+
changes++;
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
// Excess card — hide via CSS class
|
|
1307
|
+
if (!container.settings) container.settings = {};
|
|
1308
|
+
container.settings._css_classes = HIDDEN_CLASSES;
|
|
1309
|
+
container.settings.css_classes = HIDDEN_CLASSES;
|
|
1310
|
+
changes++;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Inject headings: first heading = main heading, rest = subheading or paragraphs
|
|
1316
|
+
if (headings.length > 0 && content.heading) {
|
|
1317
|
+
const mi = mainHeadingIdx;
|
|
1318
|
+
if (!consumed.has(headings[mi].widget)) {
|
|
1319
|
+
if (headings[mi].atomic) {
|
|
1320
|
+
setAtomicHtmlV3(headings[mi].widget, 'title', content.heading);
|
|
1321
|
+
} else {
|
|
1322
|
+
headings[mi].widget.settings.title = content.heading;
|
|
1323
|
+
}
|
|
1324
|
+
changes++;
|
|
1325
|
+
}
|
|
1189
1326
|
|
|
1190
1327
|
// If there's a subheading slot before the main heading (common: label above title)
|
|
1191
|
-
if (mi > 0 && content.subheading) {
|
|
1192
|
-
headings[0].
|
|
1328
|
+
if (mi > 0 && content.subheading && !consumed.has(headings[0].widget)) {
|
|
1329
|
+
if (headings[0].atomic) {
|
|
1330
|
+
setAtomicHtmlV3(headings[0].widget, 'title', content.subheading);
|
|
1331
|
+
} else {
|
|
1332
|
+
headings[0].widget.settings.title = content.subheading;
|
|
1333
|
+
}
|
|
1193
1334
|
changes++;
|
|
1194
1335
|
}
|
|
1195
1336
|
|
|
@@ -1198,8 +1339,13 @@ function injectContent(section, content) {
|
|
|
1198
1339
|
for (let i = 0; i < headings.length; i++) {
|
|
1199
1340
|
if (i === mi) continue;
|
|
1200
1341
|
if (i < mi) continue; // already handled
|
|
1342
|
+
if (consumed.has(headings[i].widget)) continue;
|
|
1201
1343
|
if (paragraphs.length && paraIdx < paragraphs.length) {
|
|
1202
|
-
headings[i].
|
|
1344
|
+
if (headings[i].atomic) {
|
|
1345
|
+
setAtomicHtmlV3(headings[i].widget, 'title', paragraphs[paraIdx++]);
|
|
1346
|
+
} else {
|
|
1347
|
+
headings[i].widget.settings.title = paragraphs[paraIdx++];
|
|
1348
|
+
}
|
|
1203
1349
|
changes++;
|
|
1204
1350
|
}
|
|
1205
1351
|
}
|
|
@@ -1208,8 +1354,15 @@ function injectContent(section, content) {
|
|
|
1208
1354
|
// Inject text-editor widgets with paragraphs
|
|
1209
1355
|
let pIdx = 0;
|
|
1210
1356
|
for (const slot of texts) {
|
|
1357
|
+
if (consumed.has(slot.widget)) continue;
|
|
1211
1358
|
if (pIdx < paragraphs.length) {
|
|
1212
|
-
|
|
1359
|
+
const html = `<p>${paragraphs.slice(pIdx).join('</p><p>')}</p>`;
|
|
1360
|
+
if (slot.atomic) {
|
|
1361
|
+
// v4 e-paragraph stores in html-v3 wrapper at settings.paragraph
|
|
1362
|
+
setAtomicHtmlV3(slot.widget, 'paragraph', html);
|
|
1363
|
+
} else {
|
|
1364
|
+
slot.widget.settings.editor = html;
|
|
1365
|
+
}
|
|
1213
1366
|
pIdx = paragraphs.length;
|
|
1214
1367
|
changes++;
|
|
1215
1368
|
}
|
|
@@ -1221,7 +1374,6 @@ function injectContent(section, content) {
|
|
|
1221
1374
|
'fas fa-bolt', 'fas fa-heart', 'fas fa-cog', 'fas fa-gem',
|
|
1222
1375
|
'fas fa-rocket', 'fas fa-thumbs-up', 'fas fa-lightbulb', 'fas fa-bullseye',
|
|
1223
1376
|
];
|
|
1224
|
-
const items = (content.items || []);
|
|
1225
1377
|
for (let i = 0; i < iconBoxes.length && i < items.length; i++) {
|
|
1226
1378
|
iconBoxes[i].widget.settings.title_text = items[i].title;
|
|
1227
1379
|
if (items[i].description) {
|
|
@@ -1261,9 +1413,14 @@ function injectContent(section, content) {
|
|
|
1261
1413
|
// Inject icon-list widgets with items (bullet lists in about, pricing features, etc.)
|
|
1262
1414
|
const iconLists = slots.filter(s => s.type === 'icon-list');
|
|
1263
1415
|
// Items for icon-lists come from content.items that overflow past icon-boxes + image-boxes,
|
|
1264
|
-
// OR from content.items directly if there are no icon-boxes/image-boxes
|
|
1416
|
+
// OR from content.items directly if there are no icon-boxes/image-boxes.
|
|
1417
|
+
// Skip entirely if items were already consumed by card-pair injection.
|
|
1265
1418
|
const iconListItemStart = iconBoxes.length + imageBoxes.length;
|
|
1266
|
-
const iconListItems =
|
|
1419
|
+
const iconListItems = useCards
|
|
1420
|
+
? []
|
|
1421
|
+
: (items.length > iconListItemStart
|
|
1422
|
+
? items.slice(iconListItemStart)
|
|
1423
|
+
: (iconBoxes.length === 0 && imageBoxes.length === 0 ? items : []));
|
|
1267
1424
|
let iconListItemIdx = 0;
|
|
1268
1425
|
for (const il of iconLists) {
|
|
1269
1426
|
const existing = il.widget.settings.icon_list || [];
|
|
@@ -1291,9 +1448,15 @@ function injectContent(section, content) {
|
|
|
1291
1448
|
// Inject buttons — fill all available button slots, always set both text and URL
|
|
1292
1449
|
const contentButtons = content.buttons || [];
|
|
1293
1450
|
for (let i = 0; i < buttons.length && i < contentButtons.length; i++) {
|
|
1294
|
-
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
1451
|
+
const url = contentButtons[i].url || '#';
|
|
1452
|
+
if (buttons[i].atomic) {
|
|
1453
|
+
setAtomicHtmlV3(buttons[i].widget, 'text', contentButtons[i].text);
|
|
1454
|
+
setAtomicLinkUrl(buttons[i].widget, url);
|
|
1455
|
+
} else {
|
|
1456
|
+
buttons[i].widget.settings.text = contentButtons[i].text;
|
|
1457
|
+
if (!buttons[i].widget.settings.link) buttons[i].widget.settings.link = {};
|
|
1458
|
+
buttons[i].widget.settings.link.url = url;
|
|
1459
|
+
}
|
|
1297
1460
|
changes++;
|
|
1298
1461
|
}
|
|
1299
1462
|
|
|
@@ -1432,23 +1595,43 @@ function injectContent(section, content) {
|
|
|
1432
1595
|
const HIDDEN_CLASSES = 'elementor-hidden-desktop elementor-hidden-tablet elementor-hidden-mobile';
|
|
1433
1596
|
|
|
1434
1597
|
// Headings: clear unfilled ones (beyond main + subheading + paragraph-filled ones)
|
|
1598
|
+
// Skip widgets consumed by card-pair injection (their titles are real content).
|
|
1435
1599
|
const filledHeadingCount = (content.heading ? 1 : 0) + (content.subheading ? 1 : 0) + Math.min(paragraphs.length, Math.max(0, headings.length - 2));
|
|
1436
1600
|
for (let i = filledHeadingCount; i < headings.length; i++) {
|
|
1437
|
-
if (
|
|
1438
|
-
|
|
1439
|
-
|
|
1601
|
+
if (consumed.has(headings[i].widget)) continue;
|
|
1602
|
+
if (headings[i].atomic) {
|
|
1603
|
+
const current = getAtomicHtmlV3(headings[i].widget, 'title');
|
|
1604
|
+
if (current && current !== content.heading && current !== content.subheading) {
|
|
1605
|
+
setAtomicHtmlV3(headings[i].widget, 'title', '');
|
|
1606
|
+
changes++;
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
if (headings[i].widget.settings.title && headings[i].widget.settings.title !== content.heading && headings[i].widget.settings.title !== content.subheading) {
|
|
1610
|
+
headings[i].widget.settings.title = '';
|
|
1611
|
+
changes++;
|
|
1612
|
+
}
|
|
1440
1613
|
}
|
|
1441
1614
|
}
|
|
1442
1615
|
|
|
1443
|
-
// Text-editors: clear unfilled ones
|
|
1444
|
-
if (paragraphs.length === 0) {
|
|
1616
|
+
// Text-editors: clear unfilled ones (skip card-consumed widgets)
|
|
1617
|
+
if (paragraphs.length === 0 && !useCards) {
|
|
1445
1618
|
// No paragraphs at all — don't clear, the template text may be structural
|
|
1446
1619
|
} else {
|
|
1447
1620
|
// First text-editor got content; clear the rest
|
|
1448
|
-
for (let i =
|
|
1449
|
-
if (texts[i].widget
|
|
1450
|
-
|
|
1451
|
-
|
|
1621
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1622
|
+
if (consumed.has(texts[i].widget)) continue;
|
|
1623
|
+
// Skip the first un-consumed text-editor (it got paragraphs)
|
|
1624
|
+
if (paragraphs.length > 0 && i === 0 && !consumed.has(texts[0].widget)) continue;
|
|
1625
|
+
if (texts[i].atomic) {
|
|
1626
|
+
if (getAtomicHtmlV3(texts[i].widget, 'paragraph')) {
|
|
1627
|
+
setAtomicHtmlV3(texts[i].widget, 'paragraph', '');
|
|
1628
|
+
changes++;
|
|
1629
|
+
}
|
|
1630
|
+
} else {
|
|
1631
|
+
if (texts[i].widget.settings.editor) {
|
|
1632
|
+
texts[i].widget.settings.editor = '';
|
|
1633
|
+
changes++;
|
|
1634
|
+
}
|
|
1452
1635
|
}
|
|
1453
1636
|
}
|
|
1454
1637
|
}
|
|
@@ -1559,27 +1742,24 @@ const tokenCache = {
|
|
|
1559
1742
|
|
|
1560
1743
|
const DEFAULT_TOKENS = {
|
|
1561
1744
|
colors: {
|
|
1562
|
-
primary: '#
|
|
1563
|
-
primary_gradient_end: '#
|
|
1564
|
-
dark_bg: '#
|
|
1565
|
-
muted_text: '#
|
|
1745
|
+
primary: '#2563EB', primary_dark: '#1E40AF', primary_hover: '#1D4ED8',
|
|
1746
|
+
primary_gradient_end: '#1E3A8A', secondary: '#F59E0B', secondary_hover: '#D97706',
|
|
1747
|
+
dark_bg: '#1E293B', light_bg: '#F1F5F9', text: '#1E293B', white: '#FFFFFF',
|
|
1748
|
+
muted_text: '#64748B', light_gray_bg: '#F8FAFC',
|
|
1566
1749
|
},
|
|
1567
1750
|
typography: {
|
|
1568
|
-
heading_font: '
|
|
1751
|
+
heading_font: 'Inter', body_font: 'Inter',
|
|
1569
1752
|
button_weight: '600', button_transform: 'uppercase',
|
|
1570
1753
|
},
|
|
1571
1754
|
spacing: {
|
|
1572
|
-
section_class: 'space-block-lg', button_padding: '15px 30px', border_radius: '
|
|
1755
|
+
section_class: 'space-block-lg', button_padding: '15px 30px', border_radius: '8px',
|
|
1573
1756
|
},
|
|
1574
1757
|
urls: {
|
|
1575
|
-
site_base: '',
|
|
1758
|
+
site_base: '', cta_path: '/contact', about_path: '/about',
|
|
1576
1759
|
},
|
|
1577
1760
|
buttons: {
|
|
1578
1761
|
primary_class: 'primary-btn', secondary_class: 'secondary-btn',
|
|
1579
1762
|
},
|
|
1580
|
-
boilerplate: {
|
|
1581
|
-
jet_parallax: true, plus_tooltip: true, tooltip_text: 'Luctus nec ullamcorper mattis',
|
|
1582
|
-
},
|
|
1583
1763
|
globals_map: {
|
|
1584
1764
|
text: 'globals/colors?id=text', primary: 'globals/colors?id=primary', secondary: 'globals/colors?id=secondary',
|
|
1585
1765
|
},
|
|
@@ -1620,10 +1800,11 @@ async function getDesignTokens(forceRefresh = false) {
|
|
|
1620
1800
|
* Build Elementor button settings from tokens
|
|
1621
1801
|
*/
|
|
1622
1802
|
function buildButtonSettings(tokens, type) {
|
|
1623
|
-
const c = tokens.colors;
|
|
1624
|
-
const t = tokens.typography;
|
|
1625
|
-
const b = tokens.buttons;
|
|
1626
|
-
const
|
|
1803
|
+
const c = tokens.colors || {};
|
|
1804
|
+
const t = tokens.typography || {};
|
|
1805
|
+
const b = tokens.buttons || {};
|
|
1806
|
+
const s = tokens.spacing || {};
|
|
1807
|
+
const r = parseInt(s.border_radius) || 8;
|
|
1627
1808
|
|
|
1628
1809
|
if (type === 'primary') {
|
|
1629
1810
|
return {
|
|
@@ -1638,12 +1819,11 @@ function buildButtonSettings(tokens, type) {
|
|
|
1638
1819
|
button_background_hover_background: 'gradient',
|
|
1639
1820
|
button_background_hover_color: c.primary,
|
|
1640
1821
|
button_background_hover_color_b: c.primary_hover,
|
|
1641
|
-
border_radius: { unit: 'px', top:
|
|
1822
|
+
border_radius: { unit: 'px', top: String(r), right: String(r), bottom: String(r), left: String(r), isLinked: true },
|
|
1642
1823
|
button_padding: { unit: 'px', top: 15, right: 30, bottom: 15, left: 30, isLinked: false },
|
|
1643
1824
|
typography_typography: 'custom', typography_font_family: t.heading_font,
|
|
1644
1825
|
typography_font_weight: t.button_weight, typography_text_transform: t.button_transform,
|
|
1645
1826
|
_css_classes: b.primary_class,
|
|
1646
|
-
plus_tooltip_content_desc: bp.tooltip_text,
|
|
1647
1827
|
__globals__: { background_color: '', button_background_hover_color: tokens.globals_map?.primary || '', button_background_hover_color_b: '', hover_color: '' },
|
|
1648
1828
|
};
|
|
1649
1829
|
} else {
|
|
@@ -1654,12 +1834,11 @@ function buildButtonSettings(tokens, type) {
|
|
|
1654
1834
|
background_color: c.secondary,
|
|
1655
1835
|
button_background_hover_color: c.secondary_hover,
|
|
1656
1836
|
hover_color: '#000000',
|
|
1657
|
-
border_radius: { unit: 'px', top:
|
|
1837
|
+
border_radius: { unit: 'px', top: String(r), right: String(r), bottom: String(r), left: String(r), isLinked: true },
|
|
1658
1838
|
button_padding: { unit: 'px', top: 15, right: 30, bottom: 15, left: 30, isLinked: false },
|
|
1659
1839
|
typography_typography: 'custom', typography_font_family: t.heading_font,
|
|
1660
1840
|
typography_font_weight: t.button_weight, typography_text_transform: t.button_transform,
|
|
1661
1841
|
_css_classes: b.secondary_class,
|
|
1662
|
-
plus_tooltip_content_desc: bp.tooltip_text,
|
|
1663
1842
|
__globals__: { background_color: tokens.globals_map?.secondary || '', button_text_color: '', hover_color: tokens.globals_map?.text || '', button_background_hover_color: '' },
|
|
1664
1843
|
};
|
|
1665
1844
|
}
|
|
@@ -1678,7 +1857,7 @@ function getToolDefinitions() {
|
|
|
1678
1857
|
},
|
|
1679
1858
|
{
|
|
1680
1859
|
name: 'get_design_tokens',
|
|
1681
|
-
description: 'Get all design tokens (colors, typography, spacing, URLs, buttons,
|
|
1860
|
+
description: 'Get all design tokens (colors, typography, spacing, URLs, buttons, globals_map). Tokens are cached for 5 minutes.',
|
|
1682
1861
|
inputSchema: { type: 'object', properties: {} }
|
|
1683
1862
|
},
|
|
1684
1863
|
{
|
|
@@ -1700,7 +1879,7 @@ function getToolDefinitions() {
|
|
|
1700
1879
|
},
|
|
1701
1880
|
{
|
|
1702
1881
|
name: 'get_design_rules',
|
|
1703
|
-
description: 'Get ALL design conventions: buttons, colors, container/widget
|
|
1882
|
+
description: 'Get ALL design conventions: buttons, colors, container/widget rules, URLs, section types. Generated from current design tokens. ALWAYS call this before generating or editing pages.',
|
|
1704
1883
|
inputSchema: { type: 'object', properties: {} }
|
|
1705
1884
|
},
|
|
1706
1885
|
{
|
|
@@ -1737,7 +1916,7 @@ function getToolDefinitions() {
|
|
|
1737
1916
|
},
|
|
1738
1917
|
{
|
|
1739
1918
|
name: 'validate_page',
|
|
1740
|
-
description: 'Run design validation on an Elementor page (CSS duals,
|
|
1919
|
+
description: 'Run design validation on an Elementor page (CSS duals, root layout, duplicate IDs, etc.)',
|
|
1741
1920
|
inputSchema: {
|
|
1742
1921
|
type: 'object',
|
|
1743
1922
|
properties: {
|
|
@@ -2058,6 +2237,7 @@ function getToolDefinitions() {
|
|
|
2058
2237
|
search: { type: 'string', description: 'Text to find (literal string or regex pattern)' },
|
|
2059
2238
|
replace: { type: 'string', description: 'Replacement text' },
|
|
2060
2239
|
regex: { type: 'boolean', description: 'Treat search as regex pattern. Default: false (literal).' },
|
|
2240
|
+
case_insensitive: { type: 'boolean', description: 'Ignore case when matching. Default: false.' },
|
|
2061
2241
|
page_id: { type: 'number', description: 'Optional: only search this page' },
|
|
2062
2242
|
dry_run: { type: 'boolean', description: 'Preview changes without saving. Default: true (SAFE). Set to false to apply changes.' },
|
|
2063
2243
|
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
@@ -2117,6 +2297,8 @@ function getToolDefinitions() {
|
|
|
2117
2297
|
status: { type: 'string', description: 'Comma-separated: publish,draft,private (default: all three)' },
|
|
2118
2298
|
search: { type: 'string', description: 'Search by keyword' },
|
|
2119
2299
|
category: { type: 'string', description: 'Filter by category: ID (number) or slug (string). Only applies to post types with categories.' },
|
|
2300
|
+
date_after: { type: 'string', description: 'Filter posts after this date (YYYY-MM-DD)' },
|
|
2301
|
+
date_before: { type: 'string', description: 'Filter posts before this date (YYYY-MM-DD)' },
|
|
2120
2302
|
per_page: { type: 'number', description: 'Results per page, max 100 (default: 50)' },
|
|
2121
2303
|
page: { type: 'number', description: 'Page number (default: 1)' }
|
|
2122
2304
|
}
|
|
@@ -2367,6 +2549,198 @@ function getToolDefinitions() {
|
|
|
2367
2549
|
required: ['template_id']
|
|
2368
2550
|
}
|
|
2369
2551
|
},
|
|
2552
|
+
|
|
2553
|
+
// ── Nav menus (v4.29.0) ──
|
|
2554
|
+
{
|
|
2555
|
+
name: 'list_nav_menus',
|
|
2556
|
+
description: 'List all WordPress nav menus with their IDs, names, slugs, and item counts.',
|
|
2557
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
name: 'create_nav_menu',
|
|
2561
|
+
description: 'Create a new WordPress nav menu. If a menu with the same name exists, returns its existing ID.',
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
type: 'object',
|
|
2564
|
+
properties: {
|
|
2565
|
+
name: { type: 'string', description: 'Menu name (e.g. "Primary", "Footer")' }
|
|
2566
|
+
},
|
|
2567
|
+
required: ['name']
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
{
|
|
2571
|
+
name: 'set_menu_items',
|
|
2572
|
+
description: 'Bulk-replace all items in a nav menu. Each item: {title, url (for type=custom), type ("custom"|"post_type"|"taxonomy"), object ("page"|"post"|"category"), object_id, parent_index (for nesting — 0-based index of parent in this same array), target ("_blank")}. Items are inserted in order; nested items must come AFTER their parent in the array.',
|
|
2573
|
+
inputSchema: {
|
|
2574
|
+
type: 'object',
|
|
2575
|
+
properties: {
|
|
2576
|
+
menu_id: { type: 'number', description: 'Menu ID from list_nav_menus or create_nav_menu' },
|
|
2577
|
+
items: {
|
|
2578
|
+
type: 'array',
|
|
2579
|
+
description: 'Ordered list of menu items (replaces all existing items in the menu)',
|
|
2580
|
+
items: {
|
|
2581
|
+
type: 'object',
|
|
2582
|
+
properties: {
|
|
2583
|
+
title: { type: 'string' },
|
|
2584
|
+
url: { type: 'string', description: 'Required when type="custom"' },
|
|
2585
|
+
type: { type: 'string', enum: ['custom', 'post_type', 'taxonomy'], description: 'Default: "custom"' },
|
|
2586
|
+
object: { type: 'string', description: 'e.g. "page", "post", "category" — required when type !== "custom"' },
|
|
2587
|
+
object_id: { type: 'number', description: 'WP object ID — required when type !== "custom"' },
|
|
2588
|
+
parent_index: { type: 'number', description: 'Index in this array of the parent item (for nesting). Omit for top-level.' },
|
|
2589
|
+
target: { type: 'string', enum: ['_blank', ''], description: 'Optional. "_blank" opens in new tab.' }
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
},
|
|
2594
|
+
required: ['menu_id', 'items']
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
name: 'list_menu_locations',
|
|
2599
|
+
description: 'List the theme\'s registered nav menu locations and which menu (if any) is currently assigned to each.',
|
|
2600
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
name: 'assign_menu_to_location',
|
|
2604
|
+
description: 'Assign a nav menu to a theme location (e.g. "menu-1" / "primary"). Use list_menu_locations first to see available locations.',
|
|
2605
|
+
inputSchema: {
|
|
2606
|
+
type: 'object',
|
|
2607
|
+
properties: {
|
|
2608
|
+
location: { type: 'string', description: 'Theme location key (e.g. "menu-1")' },
|
|
2609
|
+
menu_id: { type: 'number', description: 'Menu ID to assign' }
|
|
2610
|
+
},
|
|
2611
|
+
required: ['location', 'menu_id']
|
|
2612
|
+
}
|
|
2613
|
+
},
|
|
2614
|
+
|
|
2615
|
+
// ── Custom post types & taxonomies (v4.31.0) ──
|
|
2616
|
+
{
|
|
2617
|
+
name: 'register_cpt',
|
|
2618
|
+
description: 'Register a custom post type. Stored in `nvbc_post_types` WP option and registered on every init. Pass standard register_post_type args in snake_case. Required: slug. Common: label, plural_label, hierarchical (bool), supports (array), rewrite_slug, has_archive, taxonomies (array of taxonomy slugs), menu_icon. Pass `rewrite_slug: ""` for root-level URLs (locations pattern).',
|
|
2619
|
+
inputSchema: {
|
|
2620
|
+
type: 'object',
|
|
2621
|
+
properties: {
|
|
2622
|
+
slug: { type: 'string', description: 'CPT slug (e.g. "practice-area")' },
|
|
2623
|
+
label: { type: 'string', description: 'Singular label (e.g. "Practice Area")' },
|
|
2624
|
+
plural_label: { type: 'string', description: 'Plural label (e.g. "Practice Areas")' },
|
|
2625
|
+
public: { type: 'boolean' },
|
|
2626
|
+
show_in_rest: { type: 'boolean', description: 'Default: true (required for Gutenberg + REST API)' },
|
|
2627
|
+
hierarchical: { type: 'boolean', description: 'Allow parent/child URLs (default: false)' },
|
|
2628
|
+
supports: { type: 'array', items: { type: 'string' }, description: 'Default: ["title","editor","thumbnail","revisions","page-attributes"]' },
|
|
2629
|
+
has_archive: { type: 'boolean' },
|
|
2630
|
+
rewrite_slug: { type: 'string', description: 'URL slug. Pass empty string for root-level URLs.' },
|
|
2631
|
+
rewrite_with_front: { type: 'boolean' },
|
|
2632
|
+
menu_icon: { type: 'string', description: 'Dashicon class (e.g. "dashicons-businessperson")' },
|
|
2633
|
+
menu_position: { type: 'number' },
|
|
2634
|
+
taxonomies: { type: 'array', items: { type: 'string' }, description: 'Built-in or custom taxonomy slugs to attach' },
|
|
2635
|
+
show_in_menu: { type: 'boolean' },
|
|
2636
|
+
show_in_nav_menus: { type: 'boolean' },
|
|
2637
|
+
exclude_from_search:{ type: 'boolean' }
|
|
2638
|
+
},
|
|
2639
|
+
required: ['slug']
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
name: 'list_post_types',
|
|
2644
|
+
description: 'List all registered post types (built-in + NVBC-managed). Returns slug, labels, hierarchical, has_archive, rewrite_slug, and a `managed` flag indicating which are NVBC-managed (deletable via delete_cpt).',
|
|
2645
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2646
|
+
},
|
|
2647
|
+
{
|
|
2648
|
+
name: 'delete_cpt',
|
|
2649
|
+
description: 'Unregister an NVBC-managed CPT. Existing posts of that type are NOT deleted; they just become unrenderable until you re-register the CPT. Built-in post types cannot be deleted.',
|
|
2650
|
+
inputSchema: {
|
|
2651
|
+
type: 'object',
|
|
2652
|
+
properties: { slug: { type: 'string' } },
|
|
2653
|
+
required: ['slug']
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
name: 'register_taxonomy',
|
|
2658
|
+
description: 'Register a custom taxonomy. Stored in `nvbc_taxonomies` WP option. Pass post_types as array of CPT slugs to attach the taxonomy to.',
|
|
2659
|
+
inputSchema: {
|
|
2660
|
+
type: 'object',
|
|
2661
|
+
properties: {
|
|
2662
|
+
slug: { type: 'string' },
|
|
2663
|
+
label: { type: 'string' },
|
|
2664
|
+
plural_label: { type: 'string' },
|
|
2665
|
+
post_types: { type: 'array', items: { type: 'string' } },
|
|
2666
|
+
hierarchical: { type: 'boolean', description: 'true = category-style, false = tag-style' },
|
|
2667
|
+
rewrite_slug: { type: 'string' },
|
|
2668
|
+
public: { type: 'boolean' },
|
|
2669
|
+
show_in_rest: { type: 'boolean' },
|
|
2670
|
+
show_admin_column: { type: 'boolean' }
|
|
2671
|
+
},
|
|
2672
|
+
required: ['slug']
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
name: 'delete_taxonomy',
|
|
2677
|
+
description: 'Unregister an NVBC-managed taxonomy. Term assignments persist in the database; just the taxonomy itself stops being registered.',
|
|
2678
|
+
inputSchema: {
|
|
2679
|
+
type: 'object',
|
|
2680
|
+
properties: { slug: { type: 'string' } },
|
|
2681
|
+
required: ['slug']
|
|
2682
|
+
}
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: 'flush_rewrites',
|
|
2686
|
+
description: 'Force a flush of WordPress rewrite rules. Normally not needed — register_cpt/register_taxonomy auto-flush. Use this if archive/single URLs return 404 after a manual change.',
|
|
2687
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
name: 'import_acf_groups_native',
|
|
2691
|
+
description: 'Import ACF Pro field groups in their native JSON export format (output of Tools → Export Field Groups). Preserves repeaters, flexible content, conditional logic, sub_fields, field keys exactly as in the source export. Use this when you have a pre-prepared ACF JSON file. The simpler create_acf_field_group flattens schema; this one is bit-for-bit faithful.',
|
|
2692
|
+
inputSchema: {
|
|
2693
|
+
type: 'object',
|
|
2694
|
+
properties: {
|
|
2695
|
+
groups: {
|
|
2696
|
+
type: 'array',
|
|
2697
|
+
description: 'Array of ACF field group export objects (each must have title + fields)',
|
|
2698
|
+
items: { type: 'object' }
|
|
2699
|
+
}
|
|
2700
|
+
},
|
|
2701
|
+
required: ['groups']
|
|
2702
|
+
}
|
|
2703
|
+
},
|
|
2704
|
+
|
|
2705
|
+
// ── Media import (v4.30.0) ──
|
|
2706
|
+
{
|
|
2707
|
+
name: 'import_image',
|
|
2708
|
+
description: 'Sideload a single image into the WordPress media library from a public URL (preferred) or base64. Idempotent: if the same source URL was imported before, returns the existing attachment without re-fetching. Returns {attachment_id, url, was_existing, alt, title}.',
|
|
2709
|
+
inputSchema: {
|
|
2710
|
+
type: 'object',
|
|
2711
|
+
properties: {
|
|
2712
|
+
url: { type: 'string', description: 'Public URL the WP server can fetch (e.g. https://example.com/image.jpg)' },
|
|
2713
|
+
base64: { type: 'string', description: 'Alternative to url — raw base64 or data: URI. Requires filename.' },
|
|
2714
|
+
filename: { type: 'string', description: 'Filename to save as. Required when uploading via base64; inferred from URL otherwise.' },
|
|
2715
|
+
alt: { type: 'string', description: 'Alt text (saved as _wp_attachment_image_alt)' },
|
|
2716
|
+
title: { type: 'string', description: 'Attachment title (defaults to filename without extension)' }
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
{
|
|
2721
|
+
name: 'import_images_bulk',
|
|
2722
|
+
description: 'Sideload many images in one call. Same dedup-by-source-URL behavior as import_image. Partial failures do not fail the batch — every item gets a per-item result. Returns {total, imported, existing, failed, results: [{index, success, attachment_id?, url?, was_existing?, error?}]}.',
|
|
2723
|
+
inputSchema: {
|
|
2724
|
+
type: 'object',
|
|
2725
|
+
properties: {
|
|
2726
|
+
items: {
|
|
2727
|
+
type: 'array',
|
|
2728
|
+
description: 'List of images to import. Each item supports the same fields as import_image.',
|
|
2729
|
+
items: {
|
|
2730
|
+
type: 'object',
|
|
2731
|
+
properties: {
|
|
2732
|
+
url: { type: 'string' },
|
|
2733
|
+
base64: { type: 'string' },
|
|
2734
|
+
filename: { type: 'string' },
|
|
2735
|
+
alt: { type: 'string' },
|
|
2736
|
+
title: { type: 'string' }
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
},
|
|
2741
|
+
required: ['items']
|
|
2742
|
+
}
|
|
2743
|
+
},
|
|
2370
2744
|
{
|
|
2371
2745
|
name: 'audit_placeholders',
|
|
2372
2746
|
description: 'Scan all published Elementor pages for placeholder text (e.g. "Add Your Heading Text Here", "Lorem ipsum"), empty sections with no content, and broken anchor links (#). Optionally include library templates.',
|
|
@@ -2953,7 +3327,7 @@ function getToolDefinitions() {
|
|
|
2953
3327
|
},
|
|
2954
3328
|
{
|
|
2955
3329
|
name: 'validate_all_pages',
|
|
2956
|
-
description: 'Validate all Elementor pages against design patterns (CSS class duality,
|
|
3330
|
+
description: 'Validate all Elementor pages against design patterns (CSS class duality, root layout, duplicate IDs). Returns per-page scores and aggregate summary.',
|
|
2957
3331
|
inputSchema: {
|
|
2958
3332
|
type: 'object',
|
|
2959
3333
|
properties: {
|
|
@@ -3073,7 +3447,6 @@ async function handleToolCall(name, args) {
|
|
|
3073
3447
|
const t = tokens.typography || {};
|
|
3074
3448
|
const s = tokens.spacing || {};
|
|
3075
3449
|
const u = tokens.urls || {};
|
|
3076
|
-
const bp = tokens.boilerplate || {};
|
|
3077
3450
|
const gm = tokens.globals_map || {};
|
|
3078
3451
|
|
|
3079
3452
|
return ok(
|
|
@@ -3081,17 +3454,16 @@ async function handleToolCall(name, args) {
|
|
|
3081
3454
|
`--- BUTTONS ---\n` +
|
|
3082
3455
|
`Primary: gradient ${c.primary} -> ${c.primary_dark}, text ${c.white}, class "${tokens.buttons?.primary_class}"\n` +
|
|
3083
3456
|
`Secondary: solid ${c.secondary}, text ${c.text}, class "${tokens.buttons?.secondary_class}"\n` +
|
|
3457
|
+
`Border radius: ${s.border_radius}\n` +
|
|
3084
3458
|
`Rules:\n - Hero: ALWAYS secondary\n - Light bg: primary\n - Dark bg (1 btn): secondary\n - Dark bg (2 btn): BOTH secondary\n\n` +
|
|
3085
3459
|
`--- COLORS (hex) ---\n${Object.entries(c).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n\n` +
|
|
3086
3460
|
`--- COLORS (__globals__) ---\n${Object.entries(gm).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n\n` +
|
|
3087
3461
|
`--- CONTAINER RULES ---\n` +
|
|
3088
3462
|
` Root: layout = "full_width"\n` +
|
|
3089
|
-
` ALL containers: jet_parallax_layout_list = []\n` +
|
|
3090
3463
|
` css_classes MUST match _css_classes\n` +
|
|
3091
3464
|
` Section spacing: "${s.section_class}"\n\n` +
|
|
3092
3465
|
`--- WIDGET RULES ---\n` +
|
|
3093
|
-
` ALL widgets: elements = []\n` +
|
|
3094
|
-
` ALL widgets: plus_tooltip_content_desc = "${bp.tooltip_text}"\n\n` +
|
|
3466
|
+
` ALL widgets: elements = []\n\n` +
|
|
3095
3467
|
`--- TYPOGRAPHY ---\n Heading: ${t.heading_font}\n Body: ${t.body_font}\n Button: ${t.button_weight} ${t.button_transform}\n\n` +
|
|
3096
3468
|
`--- URLS ---\n${Object.entries(u).map(([k, v]) => ` ${k}: ${v}`).join('\n')}`
|
|
3097
3469
|
);
|
|
@@ -3222,7 +3594,7 @@ async function handleToolCall(name, args) {
|
|
|
3222
3594
|
if (!r.success || !r.components) {
|
|
3223
3595
|
return ok(`Failed to list components: ${r.message || 'Unknown error'}`);
|
|
3224
3596
|
}
|
|
3225
|
-
let out = `${r.count} Design Components Available\n\n`;
|
|
3597
|
+
let out = `${r.count} Design Components Available (v3 unmarked, v4 atomic marked [v4])\n\n`;
|
|
3226
3598
|
const grouped = {};
|
|
3227
3599
|
r.components.forEach(c => {
|
|
3228
3600
|
if (!grouped[c.category]) grouped[c.category] = [];
|
|
@@ -3231,7 +3603,9 @@ async function handleToolCall(name, args) {
|
|
|
3231
3603
|
Object.entries(grouped).forEach(([category, items]) => {
|
|
3232
3604
|
out += `--- ${category.toUpperCase()} (${items.length}) ---\n`;
|
|
3233
3605
|
items.forEach(item => {
|
|
3234
|
-
|
|
3606
|
+
const engine = item.meta?.engine || 'v3';
|
|
3607
|
+
const mark = engine === 'v4' ? ' [v4]' : '';
|
|
3608
|
+
out += ` - ${item.name}${mark}\n ${item.meta.description}\n`;
|
|
3235
3609
|
if (item.meta.variables?.length) {
|
|
3236
3610
|
out += ` Variables: ${item.meta.variables.map(v => v.name).join(', ')}\n`;
|
|
3237
3611
|
}
|
|
@@ -3618,6 +3992,7 @@ async function handleToolCall(name, args) {
|
|
|
3618
3992
|
search: stripCDATA(args.search),
|
|
3619
3993
|
replace: stripCDATA(args.replace),
|
|
3620
3994
|
regex: parseBool(args.regex, false),
|
|
3995
|
+
case_insensitive: parseBool(args.case_insensitive, false),
|
|
3621
3996
|
dry_run: parseBool(args.dry_run, true), // default TRUE (safe)
|
|
3622
3997
|
};
|
|
3623
3998
|
if (args.page_id) body.page_id = args.page_id;
|
|
@@ -3732,6 +4107,8 @@ async function handleToolCall(name, args) {
|
|
|
3732
4107
|
if (args.status) params.set('status', args.status);
|
|
3733
4108
|
if (args.search) params.set('search', args.search);
|
|
3734
4109
|
if (args.category) params.set('category', args.category);
|
|
4110
|
+
if (args.date_after) params.set('date_after', args.date_after);
|
|
4111
|
+
if (args.date_before) params.set('date_before', args.date_before);
|
|
3735
4112
|
if (args.per_page) params.set('per_page', args.per_page);
|
|
3736
4113
|
if (args.page) params.set('page', args.page);
|
|
3737
4114
|
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
@@ -4060,6 +4437,117 @@ async function handleToolCall(name, args) {
|
|
|
4060
4437
|
}
|
|
4061
4438
|
}
|
|
4062
4439
|
|
|
4440
|
+
case 'list_nav_menus': {
|
|
4441
|
+
const r = await apiCall('/menus');
|
|
4442
|
+
if (!r.menus || !r.menus.length) return ok('No nav menus found.');
|
|
4443
|
+
const lines = r.menus.map(m => `[${m.id}] ${m.name} (slug: ${m.slug}, ${m.count} items)`);
|
|
4444
|
+
return ok(`Nav menus (${r.menus.length}):\n${lines.join('\n')}`);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
case 'create_nav_menu': {
|
|
4448
|
+
const r = await apiCall('/menus', 'POST', { name: args.name });
|
|
4449
|
+
return ok(r.created
|
|
4450
|
+
? `Menu created: "${r.name}" (ID ${r.id})`
|
|
4451
|
+
: `Menu already existed: "${r.name}" (ID ${r.id})`);
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
case 'set_menu_items': {
|
|
4455
|
+
const r = await apiCall(`/menus/${args.menu_id}/items`, 'POST', { items: args.items });
|
|
4456
|
+
const errLines = (r.errors || []).map(e => ` • #${e.index}: ${e.message}`);
|
|
4457
|
+
return ok(
|
|
4458
|
+
`Menu ${r.menu_id}: ${r.created.length} item(s) inserted.` +
|
|
4459
|
+
(errLines.length ? `\n${r.errors.length} error(s):\n${errLines.join('\n')}` : '')
|
|
4460
|
+
);
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
case 'list_menu_locations': {
|
|
4464
|
+
const r = await apiCall('/menu-locations');
|
|
4465
|
+
if (!r.locations || !r.locations.length) return ok('Theme has no registered nav menu locations.');
|
|
4466
|
+
const lines = r.locations.map(l => `${l.location} (${l.label}) → ${l.menu_id ? 'menu #' + l.menu_id : '(none assigned)'}`);
|
|
4467
|
+
return ok(`Theme nav menu locations:\n${lines.join('\n')}`);
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
case 'assign_menu_to_location': {
|
|
4471
|
+
const r = await apiCall('/menu-locations', 'POST', { location: args.location, menu_id: args.menu_id });
|
|
4472
|
+
return ok(`Assigned menu #${r.menu_id} to location "${r.location}".`);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
case 'register_cpt': {
|
|
4476
|
+
const r = await apiCall('/post-types', 'POST', args);
|
|
4477
|
+
return ok(`${r.created ? 'Registered' : 'Updated'} CPT "${r.slug}"\n hierarchical: ${r.def.hierarchical}\n rewrite_slug: ${r.def.rewrite_slug || '(default)'}\n has_archive: ${r.def.has_archive}\n supports: ${(r.def.supports || []).join(', ')}`);
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
case 'list_post_types': {
|
|
4481
|
+
const r = await apiCall('/post-types');
|
|
4482
|
+
const lines = (r.post_types || []).map(pt => {
|
|
4483
|
+
const flags = [];
|
|
4484
|
+
if (pt.hierarchical) flags.push('hierarchical');
|
|
4485
|
+
if (pt.has_archive) flags.push('archive');
|
|
4486
|
+
if (pt.managed) flags.push('NVBC-managed');
|
|
4487
|
+
return ` ${pt.slug.padEnd(20)} ${pt.plural_label}${flags.length ? ' [' + flags.join(', ') + ']' : ''}`;
|
|
4488
|
+
});
|
|
4489
|
+
return ok(`Post types (${(r.post_types || []).length}):\n${lines.join('\n')}`);
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
case 'delete_cpt': {
|
|
4493
|
+
const r = await apiCall(`/post-types/${encodeURIComponent(args.slug)}`, 'DELETE');
|
|
4494
|
+
return ok(`Removed CPT "${r.removed}" from NVBC-managed list. Existing posts of this type are still in the DB.`);
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
case 'register_taxonomy': {
|
|
4498
|
+
const r = await apiCall('/taxonomies-managed', 'POST', args);
|
|
4499
|
+
return ok(`${r.created ? 'Registered' : 'Updated'} taxonomy "${r.slug}"\n hierarchical: ${r.def.hierarchical}\n attached to: ${(r.def.post_types || []).join(', ') || '(none)'}`);
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
case 'delete_taxonomy': {
|
|
4503
|
+
const r = await apiCall(`/taxonomies-managed/${encodeURIComponent(args.slug)}`, 'DELETE');
|
|
4504
|
+
return ok(`Removed taxonomy "${r.removed}" from NVBC-managed list.`);
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
case 'flush_rewrites': {
|
|
4508
|
+
await apiCall('/flush-rewrites', 'POST');
|
|
4509
|
+
return ok('Rewrite rules flushed.');
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
case 'import_acf_groups_native': {
|
|
4513
|
+
const r = await apiCall('/acf/import-groups-native', 'POST', { groups: args.groups });
|
|
4514
|
+
let out = `ACF native import: ${r.total} group(s) → ${r.imported.length} imported, ${r.errors.length} errors\n`;
|
|
4515
|
+
out += r.imported.map(g => ` ✓ ${g.title} (id: ${g.id}, key: ${g.key})`).join('\n');
|
|
4516
|
+
if (r.errors.length) {
|
|
4517
|
+
out += '\n' + r.errors.map(e => ` ✗ #${e.index} ${e.title || ''}: ${e.error}`).join('\n');
|
|
4518
|
+
}
|
|
4519
|
+
return ok(out);
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
case 'import_image': {
|
|
4523
|
+
const body = {};
|
|
4524
|
+
if (args.url) body.url = args.url;
|
|
4525
|
+
if (args.base64) body.base64 = args.base64;
|
|
4526
|
+
if (args.filename) body.filename = args.filename;
|
|
4527
|
+
if (args.alt) body.alt = args.alt;
|
|
4528
|
+
if (args.title) body.title = args.title;
|
|
4529
|
+
const r = await apiCall('/media/import', 'POST', body);
|
|
4530
|
+
return ok(
|
|
4531
|
+
`${r.was_existing ? 'Existing' : 'Imported'} attachment #${r.attachment_id}\n` +
|
|
4532
|
+
` url: ${r.url}\n` +
|
|
4533
|
+
(r.alt ? ` alt: ${r.alt}\n` : '') +
|
|
4534
|
+
(r.title ? ` title: ${r.title}` : '')
|
|
4535
|
+
);
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
case 'import_images_bulk': {
|
|
4539
|
+
const r = await apiCall('/media/import-bulk', 'POST', { items: args.items });
|
|
4540
|
+
let out = `Bulk import: ${r.total} requested → ${r.imported} new, ${r.existing} existing, ${r.failed} failed\n`;
|
|
4541
|
+
// Always include the per-item attachment IDs so the caller can wire them into pages
|
|
4542
|
+
// without a second round-trip. Failures are flagged inline.
|
|
4543
|
+
const lines = r.results.map(item =>
|
|
4544
|
+
item.success
|
|
4545
|
+
? ` #${item.index} → att ${item.attachment_id}${item.was_existing ? ' (existing)' : ''} [${item.source_url || '(base64)'}]`
|
|
4546
|
+
: ` #${item.index} FAILED ${item.error}: ${item.message || ''} [${item.source_url || '(base64)'}]`
|
|
4547
|
+
);
|
|
4548
|
+
return ok(out + lines.join('\n'));
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4063
4551
|
case 'audit_placeholders': {
|
|
4064
4552
|
const qs = args.include_templates ? '?include_templates=1' : '';
|
|
4065
4553
|
const r = await apiCall(`/audit-placeholders${qs}`);
|
|
@@ -4791,7 +5279,7 @@ async function handleToolCall(name, args) {
|
|
|
4791
5279
|
}
|
|
4792
5280
|
|
|
4793
5281
|
case 'append_section': {
|
|
4794
|
-
//
|
|
5282
|
+
// Enforce root settings on appended sections
|
|
4795
5283
|
if (args.section) addBoilerplate(args.section);
|
|
4796
5284
|
const body = {
|
|
4797
5285
|
target_id: args.target_id,
|
|
@@ -5222,7 +5710,7 @@ async function handleToolCall(name, args) {
|
|
|
5222
5710
|
}
|
|
5223
5711
|
}
|
|
5224
5712
|
|
|
5225
|
-
//
|
|
5713
|
+
// Enforce standard settings (root layout: full_width)
|
|
5226
5714
|
addBoilerplate(final);
|
|
5227
5715
|
|
|
5228
5716
|
// Append to page
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noleemits/vision-builder-control-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.31.0",
|
|
4
4
|
"description": "Vision Builder Control MCP server - design token-driven page builder tools for WordPress/Elementor websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|