elementor-mcp-agent 1.0.0 → 1.1.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/README.md +20 -5
- package/dist/server.js +711 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -448,8 +448,8 @@ async function restoreBackup(siteId, postId, data_meta_key, settings_meta_key) {
|
|
|
448
448
|
const r2 = await sshWpCli(site, `post meta get ${postId} ${settings_meta_key}`, { timeout_ms: 3e4 });
|
|
449
449
|
if (r2.exitCode === 0 && r2.stdout) settings_value = r2.stdout;
|
|
450
450
|
}
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
451
|
+
const writeData2 = await sshWpCli(site, `post meta update ${postId} _elementor_data ${shellQuote(data_value)}`, { timeout_ms: 3e4 });
|
|
452
|
+
if (writeData2.exitCode !== 0) throw new Error(`Restore write failed: ${writeData2.stderr}`);
|
|
453
453
|
if (settings_value !== void 0) {
|
|
454
454
|
await sshWpCli(site, `post meta update ${postId} _elementor_page_settings ${shellQuote(settings_value)}`, { timeout_ms: 3e4 });
|
|
455
455
|
}
|
|
@@ -854,6 +854,12 @@ function* walkElements(data, path = [], depth = 0) {
|
|
|
854
854
|
}
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
|
+
function findElementById(data, id) {
|
|
858
|
+
for (const { element } of walkElements(data)) {
|
|
859
|
+
if (element.id === id) return element;
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
857
863
|
function findReplaceInWidgets(data, find, replace, options = {}) {
|
|
858
864
|
let count = 0;
|
|
859
865
|
const flags = options.caseSensitive ? "g" : "gi";
|
|
@@ -2115,6 +2121,697 @@ var checkElementorVersionsTool = defineTool({
|
|
|
2115
2121
|
}
|
|
2116
2122
|
});
|
|
2117
2123
|
|
|
2124
|
+
// src/tools/widgets.ts
|
|
2125
|
+
import { z as z8 } from "zod";
|
|
2126
|
+
init_wp_rest();
|
|
2127
|
+
|
|
2128
|
+
// src/elementor/widget-ops.ts
|
|
2129
|
+
function readWidget(data, widgetId) {
|
|
2130
|
+
return findElementById(data, widgetId);
|
|
2131
|
+
}
|
|
2132
|
+
function updateWidgetSettings(data, widgetId, patch) {
|
|
2133
|
+
const widget = findElementById(data, widgetId);
|
|
2134
|
+
if (!widget || widget.elType !== "widget") return false;
|
|
2135
|
+
widget.settings = { ...widget.settings, ...patch };
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
function deleteWidget(data, widgetId) {
|
|
2139
|
+
function removeFrom(arr) {
|
|
2140
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2141
|
+
if (arr[i].id === widgetId) {
|
|
2142
|
+
arr.splice(i, 1);
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
if (arr[i].elements && removeFrom(arr[i].elements)) return true;
|
|
2146
|
+
}
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
return removeFrom(data);
|
|
2150
|
+
}
|
|
2151
|
+
function findParent(data, widgetId) {
|
|
2152
|
+
for (const { element } of walkElements(data)) {
|
|
2153
|
+
if (element.elements?.some((e) => e.id === widgetId)) return element;
|
|
2154
|
+
}
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
function duplicateWidget(data, widgetId) {
|
|
2158
|
+
const parent = findParent(data, widgetId);
|
|
2159
|
+
if (!parent || !parent.elements) return { ok: false };
|
|
2160
|
+
const idx = parent.elements.findIndex((e) => e.id === widgetId);
|
|
2161
|
+
if (idx < 0) return { ok: false };
|
|
2162
|
+
const clone = JSON.parse(JSON.stringify(parent.elements[idx]));
|
|
2163
|
+
clone.id = generateId();
|
|
2164
|
+
parent.elements.splice(idx + 1, 0, clone);
|
|
2165
|
+
return { ok: true, new_widget_id: clone.id };
|
|
2166
|
+
}
|
|
2167
|
+
function swapWidgetType(data, widgetId, newType, newSettings = {}) {
|
|
2168
|
+
const widget = findElementById(data, widgetId);
|
|
2169
|
+
if (!widget || widget.elType !== "widget") return false;
|
|
2170
|
+
widget.widgetType = newType;
|
|
2171
|
+
widget.settings = newSettings;
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
function addWidget(data, parentId, widgetType, settings = {}) {
|
|
2175
|
+
const parent = findElementById(data, parentId);
|
|
2176
|
+
if (!parent) return { ok: false };
|
|
2177
|
+
if (!parent.elements) parent.elements = [];
|
|
2178
|
+
const newWidget = {
|
|
2179
|
+
id: generateId(),
|
|
2180
|
+
elType: "widget",
|
|
2181
|
+
widgetType,
|
|
2182
|
+
settings,
|
|
2183
|
+
elements: [],
|
|
2184
|
+
isInner: false
|
|
2185
|
+
};
|
|
2186
|
+
parent.elements.push(newWidget);
|
|
2187
|
+
return { ok: true, new_widget_id: newWidget.id };
|
|
2188
|
+
}
|
|
2189
|
+
function moveWidget(data, widgetId, newParentId, position = -1) {
|
|
2190
|
+
const widget = findElementById(data, widgetId);
|
|
2191
|
+
if (!widget) return false;
|
|
2192
|
+
const oldParent = findParent(data, widgetId);
|
|
2193
|
+
if (!oldParent || !oldParent.elements) return false;
|
|
2194
|
+
const newParent = findElementById(data, newParentId);
|
|
2195
|
+
if (!newParent) return false;
|
|
2196
|
+
if (!newParent.elements) newParent.elements = [];
|
|
2197
|
+
const idx = oldParent.elements.findIndex((e) => e.id === widgetId);
|
|
2198
|
+
if (idx < 0) return false;
|
|
2199
|
+
oldParent.elements.splice(idx, 1);
|
|
2200
|
+
if (position < 0 || position >= newParent.elements.length) {
|
|
2201
|
+
newParent.elements.push(widget);
|
|
2202
|
+
} else {
|
|
2203
|
+
newParent.elements.splice(position, 0, widget);
|
|
2204
|
+
}
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
function generateId() {
|
|
2208
|
+
return Math.random().toString(16).slice(2, 9).padEnd(7, "0");
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/tools/widgets.ts
|
|
2212
|
+
init_backup();
|
|
2213
|
+
init_css_flush();
|
|
2214
|
+
init_policies();
|
|
2215
|
+
init_confirmation();
|
|
2216
|
+
async function fetchData(siteId, pageId) {
|
|
2217
|
+
const page = await wpRequest(
|
|
2218
|
+
`/wp/v2/pages/${pageId}?context=edit&_fields=meta`,
|
|
2219
|
+
{ siteId }
|
|
2220
|
+
);
|
|
2221
|
+
const v = page.meta?._elementor_data;
|
|
2222
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2223
|
+
return { raw, data: parseElementorData(raw) };
|
|
2224
|
+
}
|
|
2225
|
+
async function writeData(siteId, pageId, data) {
|
|
2226
|
+
const ser = serializeElementorData(data);
|
|
2227
|
+
const validation = validateElementorData(ser);
|
|
2228
|
+
if (!validation.valid) {
|
|
2229
|
+
throw new Error("Validation failed after edit: " + validation.errors.join("; "));
|
|
2230
|
+
}
|
|
2231
|
+
await wpRequest(`/wp/v2/pages/${pageId}`, {
|
|
2232
|
+
siteId,
|
|
2233
|
+
method: "PUT",
|
|
2234
|
+
body: { meta: { _elementor_data: ser } }
|
|
2235
|
+
});
|
|
2236
|
+
const flush = await flushCSS(siteId, pageId);
|
|
2237
|
+
return { method: flush.method };
|
|
2238
|
+
}
|
|
2239
|
+
var readWidgetTool = defineTool({
|
|
2240
|
+
name: "read_widget",
|
|
2241
|
+
description: "Fetch a single widget's full settings by id. Use list_widgets_in_page to find the id first.",
|
|
2242
|
+
inputSchema: z8.object({
|
|
2243
|
+
site_id: z8.string().optional(),
|
|
2244
|
+
page_id: z8.number().int().positive(),
|
|
2245
|
+
widget_id: z8.string().min(1)
|
|
2246
|
+
}),
|
|
2247
|
+
outputSchema: z8.object({
|
|
2248
|
+
widget_id: z8.string(),
|
|
2249
|
+
widget_type: z8.string().optional(),
|
|
2250
|
+
settings: z8.record(z8.any())
|
|
2251
|
+
}),
|
|
2252
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2253
|
+
async handler(input) {
|
|
2254
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2255
|
+
const w = readWidget(data, input.widget_id);
|
|
2256
|
+
if (!w) throw new Error(`Widget ${input.widget_id} not found on page ${input.page_id}`);
|
|
2257
|
+
return { widget_id: w.id, widget_type: w.widgetType, settings: w.settings };
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
var updateWidgetSettingsTool = defineTool({
|
|
2261
|
+
name: "update_widget_settings",
|
|
2262
|
+
description: "Shallow-merge a partial settings object into one widget. Backs up the page first; validates the result; auto-flushes CSS. Two-call confirmation flow.",
|
|
2263
|
+
inputSchema: z8.object({
|
|
2264
|
+
site_id: z8.string().optional(),
|
|
2265
|
+
page_id: z8.number().int().positive(),
|
|
2266
|
+
widget_id: z8.string().min(1),
|
|
2267
|
+
settings_patch: z8.record(z8.any()),
|
|
2268
|
+
confirmation: z8.string().optional()
|
|
2269
|
+
}),
|
|
2270
|
+
outputSchema: z8.object({
|
|
2271
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2272
|
+
page_id: z8.number(),
|
|
2273
|
+
widget_id: z8.string(),
|
|
2274
|
+
keys_changed: z8.array(z8.string()),
|
|
2275
|
+
confirmation_token: z8.string().optional(),
|
|
2276
|
+
backup_meta_key: z8.string().optional(),
|
|
2277
|
+
css_flush: z8.string().optional()
|
|
2278
|
+
}),
|
|
2279
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2280
|
+
async handler(input) {
|
|
2281
|
+
if (!input.confirmation) {
|
|
2282
|
+
const token = issueConfirmation("update_widget_settings", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2283
|
+
return {
|
|
2284
|
+
mode: "dry_run",
|
|
2285
|
+
page_id: input.page_id,
|
|
2286
|
+
widget_id: input.widget_id,
|
|
2287
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2288
|
+
confirmation_token: token
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
const conf = consumeConfirmation(input.confirmation, "update_widget_settings");
|
|
2292
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2293
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2294
|
+
if (!updateWidgetSettings(data, input.widget_id, input.settings_patch)) {
|
|
2295
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2296
|
+
}
|
|
2297
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2298
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2299
|
+
return {
|
|
2300
|
+
mode: "applied",
|
|
2301
|
+
page_id: input.page_id,
|
|
2302
|
+
widget_id: input.widget_id,
|
|
2303
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2304
|
+
backup_meta_key: backup.meta_key,
|
|
2305
|
+
css_flush: w.method
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
var deleteWidgetTool = defineTool({
|
|
2310
|
+
name: "delete_widget",
|
|
2311
|
+
description: "Remove a widget from a page by id. Two-call confirmation. Backs up before deleting.",
|
|
2312
|
+
inputSchema: z8.object({
|
|
2313
|
+
site_id: z8.string().optional(),
|
|
2314
|
+
page_id: z8.number().int().positive(),
|
|
2315
|
+
widget_id: z8.string().min(1),
|
|
2316
|
+
confirmation: z8.string().optional()
|
|
2317
|
+
}),
|
|
2318
|
+
outputSchema: z8.object({
|
|
2319
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2320
|
+
page_id: z8.number(),
|
|
2321
|
+
widget_id: z8.string(),
|
|
2322
|
+
confirmation_token: z8.string().optional(),
|
|
2323
|
+
backup_meta_key: z8.string().optional(),
|
|
2324
|
+
css_flush: z8.string().optional()
|
|
2325
|
+
}),
|
|
2326
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2327
|
+
async handler(input) {
|
|
2328
|
+
if (!input.confirmation) {
|
|
2329
|
+
const token = issueConfirmation("delete_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2330
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, confirmation_token: token };
|
|
2331
|
+
}
|
|
2332
|
+
const conf = consumeConfirmation(input.confirmation, "delete_widget");
|
|
2333
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2334
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2335
|
+
if (!deleteWidget(data, input.widget_id)) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2336
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2337
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2338
|
+
return {
|
|
2339
|
+
mode: "applied",
|
|
2340
|
+
page_id: input.page_id,
|
|
2341
|
+
widget_id: input.widget_id,
|
|
2342
|
+
backup_meta_key: backup.meta_key,
|
|
2343
|
+
css_flush: w.method
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
var duplicateWidgetTool = defineTool({
|
|
2348
|
+
name: "duplicate_widget",
|
|
2349
|
+
description: "Duplicate a widget in place (right after the original). The clone gets a new id. Two-call confirmation.",
|
|
2350
|
+
inputSchema: z8.object({
|
|
2351
|
+
site_id: z8.string().optional(),
|
|
2352
|
+
page_id: z8.number().int().positive(),
|
|
2353
|
+
widget_id: z8.string().min(1),
|
|
2354
|
+
confirmation: z8.string().optional()
|
|
2355
|
+
}),
|
|
2356
|
+
outputSchema: z8.object({
|
|
2357
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2358
|
+
page_id: z8.number(),
|
|
2359
|
+
source_widget_id: z8.string(),
|
|
2360
|
+
new_widget_id: z8.string().optional(),
|
|
2361
|
+
confirmation_token: z8.string().optional(),
|
|
2362
|
+
backup_meta_key: z8.string().optional(),
|
|
2363
|
+
css_flush: z8.string().optional()
|
|
2364
|
+
}),
|
|
2365
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2366
|
+
async handler(input) {
|
|
2367
|
+
if (!input.confirmation) {
|
|
2368
|
+
const token = issueConfirmation("duplicate_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2369
|
+
return { mode: "dry_run", page_id: input.page_id, source_widget_id: input.widget_id, confirmation_token: token };
|
|
2370
|
+
}
|
|
2371
|
+
const conf = consumeConfirmation(input.confirmation, "duplicate_widget");
|
|
2372
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2373
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2374
|
+
const r = duplicateWidget(data, input.widget_id);
|
|
2375
|
+
if (!r.ok) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2376
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2377
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2378
|
+
return {
|
|
2379
|
+
mode: "applied",
|
|
2380
|
+
page_id: input.page_id,
|
|
2381
|
+
source_widget_id: input.widget_id,
|
|
2382
|
+
new_widget_id: r.new_widget_id,
|
|
2383
|
+
backup_meta_key: backup.meta_key,
|
|
2384
|
+
css_flush: w.method
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
var swapWidgetTypeTool = defineTool({
|
|
2389
|
+
name: "swap_widget_type",
|
|
2390
|
+
description: "Replace a widget's type (e.g., heading \u2192 button) while preserving its position. Provide full new_settings \u2014 the old settings are NOT carried over (different widget types have incompatible schemas). Two-call confirmation.",
|
|
2391
|
+
inputSchema: z8.object({
|
|
2392
|
+
site_id: z8.string().optional(),
|
|
2393
|
+
page_id: z8.number().int().positive(),
|
|
2394
|
+
widget_id: z8.string().min(1),
|
|
2395
|
+
new_widget_type: z8.string().min(1),
|
|
2396
|
+
new_settings: z8.record(z8.any()).default({}),
|
|
2397
|
+
confirmation: z8.string().optional()
|
|
2398
|
+
}),
|
|
2399
|
+
outputSchema: z8.object({
|
|
2400
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2401
|
+
page_id: z8.number(),
|
|
2402
|
+
widget_id: z8.string(),
|
|
2403
|
+
new_widget_type: z8.string(),
|
|
2404
|
+
confirmation_token: z8.string().optional(),
|
|
2405
|
+
backup_meta_key: z8.string().optional(),
|
|
2406
|
+
css_flush: z8.string().optional()
|
|
2407
|
+
}),
|
|
2408
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2409
|
+
async handler(input) {
|
|
2410
|
+
if (!input.confirmation) {
|
|
2411
|
+
const token = issueConfirmation("swap_widget_type", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2412
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_widget_type: input.new_widget_type, confirmation_token: token };
|
|
2413
|
+
}
|
|
2414
|
+
const conf = consumeConfirmation(input.confirmation, "swap_widget_type");
|
|
2415
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2416
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2417
|
+
if (!swapWidgetType(data, input.widget_id, input.new_widget_type, input.new_settings)) {
|
|
2418
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2419
|
+
}
|
|
2420
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2421
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2422
|
+
return {
|
|
2423
|
+
mode: "applied",
|
|
2424
|
+
page_id: input.page_id,
|
|
2425
|
+
widget_id: input.widget_id,
|
|
2426
|
+
new_widget_type: input.new_widget_type,
|
|
2427
|
+
backup_meta_key: backup.meta_key,
|
|
2428
|
+
css_flush: w.method
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
var addWidgetTool = defineTool({
|
|
2433
|
+
name: "add_widget",
|
|
2434
|
+
description: "Append a new widget to a parent container (section, column, or container) on a page. Two-call confirmation.",
|
|
2435
|
+
inputSchema: z8.object({
|
|
2436
|
+
site_id: z8.string().optional(),
|
|
2437
|
+
page_id: z8.number().int().positive(),
|
|
2438
|
+
parent_id: z8.string().min(1).describe("Id of the section/column/container that will receive the widget."),
|
|
2439
|
+
widget_type: z8.string().min(1).describe("e.g., 'heading', 'text-editor', 'button', 'image'."),
|
|
2440
|
+
settings: z8.record(z8.any()).default({}),
|
|
2441
|
+
confirmation: z8.string().optional()
|
|
2442
|
+
}),
|
|
2443
|
+
outputSchema: z8.object({
|
|
2444
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2445
|
+
page_id: z8.number(),
|
|
2446
|
+
parent_id: z8.string(),
|
|
2447
|
+
widget_type: z8.string(),
|
|
2448
|
+
new_widget_id: z8.string().optional(),
|
|
2449
|
+
confirmation_token: z8.string().optional(),
|
|
2450
|
+
backup_meta_key: z8.string().optional(),
|
|
2451
|
+
css_flush: z8.string().optional()
|
|
2452
|
+
}),
|
|
2453
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2454
|
+
async handler(input) {
|
|
2455
|
+
if (!input.confirmation) {
|
|
2456
|
+
const token = issueConfirmation("add_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2457
|
+
return { mode: "dry_run", page_id: input.page_id, parent_id: input.parent_id, widget_type: input.widget_type, confirmation_token: token };
|
|
2458
|
+
}
|
|
2459
|
+
const conf = consumeConfirmation(input.confirmation, "add_widget");
|
|
2460
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2461
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2462
|
+
const r = addWidget(data, input.parent_id, input.widget_type, input.settings);
|
|
2463
|
+
if (!r.ok) throw new Error(`Parent ${input.parent_id} not found`);
|
|
2464
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2465
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2466
|
+
return {
|
|
2467
|
+
mode: "applied",
|
|
2468
|
+
page_id: input.page_id,
|
|
2469
|
+
parent_id: input.parent_id,
|
|
2470
|
+
widget_type: input.widget_type,
|
|
2471
|
+
new_widget_id: r.new_widget_id,
|
|
2472
|
+
backup_meta_key: backup.meta_key,
|
|
2473
|
+
css_flush: w.method
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
var moveWidgetTool = defineTool({
|
|
2478
|
+
name: "move_widget",
|
|
2479
|
+
description: "Move a widget to a different parent (or different position in the same parent). Two-call confirmation.",
|
|
2480
|
+
inputSchema: z8.object({
|
|
2481
|
+
site_id: z8.string().optional(),
|
|
2482
|
+
page_id: z8.number().int().positive(),
|
|
2483
|
+
widget_id: z8.string().min(1),
|
|
2484
|
+
new_parent_id: z8.string().min(1),
|
|
2485
|
+
position: z8.number().int().default(-1).describe("0-based position in the new parent. -1 = append."),
|
|
2486
|
+
confirmation: z8.string().optional()
|
|
2487
|
+
}),
|
|
2488
|
+
outputSchema: z8.object({
|
|
2489
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2490
|
+
page_id: z8.number(),
|
|
2491
|
+
widget_id: z8.string(),
|
|
2492
|
+
new_parent_id: z8.string(),
|
|
2493
|
+
confirmation_token: z8.string().optional(),
|
|
2494
|
+
backup_meta_key: z8.string().optional(),
|
|
2495
|
+
css_flush: z8.string().optional()
|
|
2496
|
+
}),
|
|
2497
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2498
|
+
async handler(input) {
|
|
2499
|
+
if (!input.confirmation) {
|
|
2500
|
+
const token = issueConfirmation("move_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2501
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_parent_id: input.new_parent_id, confirmation_token: token };
|
|
2502
|
+
}
|
|
2503
|
+
const conf = consumeConfirmation(input.confirmation, "move_widget");
|
|
2504
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2505
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2506
|
+
if (!moveWidget(data, input.widget_id, input.new_parent_id, input.position)) {
|
|
2507
|
+
throw new Error(`Widget or parent not found`);
|
|
2508
|
+
}
|
|
2509
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2510
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2511
|
+
return {
|
|
2512
|
+
mode: "applied",
|
|
2513
|
+
page_id: input.page_id,
|
|
2514
|
+
widget_id: input.widget_id,
|
|
2515
|
+
new_parent_id: input.new_parent_id,
|
|
2516
|
+
backup_meta_key: backup.meta_key,
|
|
2517
|
+
css_flush: w.method
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
// src/tools/bulk.ts
|
|
2523
|
+
import { z as z9 } from "zod";
|
|
2524
|
+
init_wp_rest();
|
|
2525
|
+
init_config();
|
|
2526
|
+
init_backup();
|
|
2527
|
+
init_css_flush();
|
|
2528
|
+
init_policies();
|
|
2529
|
+
init_confirmation();
|
|
2530
|
+
async function listElementorPageIds(siteId) {
|
|
2531
|
+
const out = [];
|
|
2532
|
+
let page = 1;
|
|
2533
|
+
for (; ; ) {
|
|
2534
|
+
const items = await wpRequest("/wp/v2/pages", {
|
|
2535
|
+
siteId,
|
|
2536
|
+
query: {
|
|
2537
|
+
meta_key: "_elementor_edit_mode",
|
|
2538
|
+
meta_value: "builder",
|
|
2539
|
+
context: "edit",
|
|
2540
|
+
per_page: 100,
|
|
2541
|
+
page,
|
|
2542
|
+
_fields: "id,title"
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
if (items.length === 0) break;
|
|
2546
|
+
out.push(...items.map((p) => ({ id: p.id, title: p.title.rendered })));
|
|
2547
|
+
if (items.length < 100) break;
|
|
2548
|
+
page++;
|
|
2549
|
+
if (page > 50) break;
|
|
2550
|
+
}
|
|
2551
|
+
return out;
|
|
2552
|
+
}
|
|
2553
|
+
var bulkFindReplaceSiteTool = defineTool({
|
|
2554
|
+
name: "bulk_find_replace_site",
|
|
2555
|
+
description: "Find/replace plain text in every Elementor page on a single site. TWO-CALL FLOW: dry-run returns per-page match_count + total + confirmation_token. Apply iterates each page (auto-backup + validate + flush per page). Slower than wp_search_replace but works without SSH and gives per-page granularity.",
|
|
2556
|
+
inputSchema: z9.object({
|
|
2557
|
+
site_id: z9.string().optional(),
|
|
2558
|
+
find: z9.string().min(1),
|
|
2559
|
+
replace: z9.string(),
|
|
2560
|
+
widget_type: z9.string().optional(),
|
|
2561
|
+
case_sensitive: z9.boolean().default(false),
|
|
2562
|
+
confirmation: z9.string().optional()
|
|
2563
|
+
}),
|
|
2564
|
+
outputSchema: z9.object({
|
|
2565
|
+
mode: z9.enum(["dry_run", "applied"]),
|
|
2566
|
+
site_id: z9.string(),
|
|
2567
|
+
pages_scanned: z9.number(),
|
|
2568
|
+
total_match_count: z9.number(),
|
|
2569
|
+
pages_with_matches: z9.array(z9.object({
|
|
2570
|
+
page_id: z9.number(),
|
|
2571
|
+
title: z9.string(),
|
|
2572
|
+
match_count: z9.number()
|
|
2573
|
+
})),
|
|
2574
|
+
pages_applied: z9.array(z9.object({
|
|
2575
|
+
page_id: z9.number(),
|
|
2576
|
+
backup_meta_key: z9.string().optional(),
|
|
2577
|
+
css_flush: z9.string().optional(),
|
|
2578
|
+
mode: z9.enum(["applied", "rolled_back", "skipped"]),
|
|
2579
|
+
error: z9.string().optional()
|
|
2580
|
+
})).optional(),
|
|
2581
|
+
confirmation_token: z9.string().optional(),
|
|
2582
|
+
expires_in_seconds: z9.number().optional()
|
|
2583
|
+
}),
|
|
2584
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2585
|
+
async handler(input) {
|
|
2586
|
+
const pages = await listElementorPageIds(input.site_id);
|
|
2587
|
+
const matches = [];
|
|
2588
|
+
for (const p of pages) {
|
|
2589
|
+
try {
|
|
2590
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2591
|
+
const v = page.meta?._elementor_data;
|
|
2592
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2593
|
+
const data = parseElementorData(raw);
|
|
2594
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2595
|
+
widgetType: input.widget_type,
|
|
2596
|
+
caseSensitive: input.case_sensitive
|
|
2597
|
+
});
|
|
2598
|
+
if (dry.replacementCount > 0) matches.push({ page_id: p.id, title: p.title, match_count: dry.replacementCount });
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const total = matches.reduce((s, m) => s + m.match_count, 0);
|
|
2603
|
+
if (!input.confirmation) {
|
|
2604
|
+
if (total === 0) {
|
|
2605
|
+
return {
|
|
2606
|
+
mode: "dry_run",
|
|
2607
|
+
site_id: loadConfig().default_site_id ?? "default",
|
|
2608
|
+
pages_scanned: pages.length,
|
|
2609
|
+
total_match_count: 0,
|
|
2610
|
+
pages_with_matches: []
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
const token = issueConfirmation("bulk_find_replace_site", { find: input.find, replace: input.replace, page_ids: matches.map((m) => m.page_id) }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2614
|
+
return {
|
|
2615
|
+
mode: "dry_run",
|
|
2616
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2617
|
+
pages_scanned: pages.length,
|
|
2618
|
+
total_match_count: total,
|
|
2619
|
+
pages_with_matches: matches,
|
|
2620
|
+
confirmation_token: token,
|
|
2621
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
const conf = consumeConfirmation(input.confirmation, "bulk_find_replace_site");
|
|
2625
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2626
|
+
const applied = [];
|
|
2627
|
+
for (const m of matches) {
|
|
2628
|
+
try {
|
|
2629
|
+
const page = await wpRequest(`/wp/v2/pages/${m.page_id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2630
|
+
const v = page.meta?._elementor_data;
|
|
2631
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2632
|
+
const data = parseElementorData(raw);
|
|
2633
|
+
findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2634
|
+
const ser = serializeElementorData(data);
|
|
2635
|
+
const validation = validateElementorData(ser);
|
|
2636
|
+
if (!validation.valid) {
|
|
2637
|
+
applied.push({ page_id: m.page_id, mode: "rolled_back", error: validation.errors.join("; ") });
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const backup = await fullBackup(input.site_id, m.page_id);
|
|
2641
|
+
await wpRequest(`/wp/v2/pages/${m.page_id}`, {
|
|
2642
|
+
siteId: input.site_id,
|
|
2643
|
+
method: "PUT",
|
|
2644
|
+
body: { meta: { _elementor_data: ser } }
|
|
2645
|
+
});
|
|
2646
|
+
const flush = await flushCSS(input.site_id, m.page_id);
|
|
2647
|
+
applied.push({ page_id: m.page_id, backup_meta_key: backup.meta_key, css_flush: flush.method, mode: "applied" });
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
applied.push({ page_id: m.page_id, mode: "skipped", error: e.message });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return {
|
|
2653
|
+
mode: "applied",
|
|
2654
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2655
|
+
pages_scanned: pages.length,
|
|
2656
|
+
total_match_count: total,
|
|
2657
|
+
pages_with_matches: matches,
|
|
2658
|
+
pages_applied: applied
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
var fleetFindReplaceTool = defineTool({
|
|
2663
|
+
name: "fleet_find_replace",
|
|
2664
|
+
description: "Find/replace plain text across every Elementor page of every site in the pool. Same flow as bulk_find_replace_site but iterates across sites. Returns per-site + grand-total summary. Dry-run first; second call applies. Use sparingly \u2014 this is the nuclear option.",
|
|
2665
|
+
inputSchema: z9.object({
|
|
2666
|
+
find: z9.string().min(1),
|
|
2667
|
+
replace: z9.string(),
|
|
2668
|
+
site_ids: z9.array(z9.string()).optional().describe("Subset of sites to hit. Defaults to all."),
|
|
2669
|
+
widget_type: z9.string().optional(),
|
|
2670
|
+
case_sensitive: z9.boolean().default(false),
|
|
2671
|
+
confirmation: z9.string().optional()
|
|
2672
|
+
}),
|
|
2673
|
+
outputSchema: z9.object({
|
|
2674
|
+
mode: z9.enum(["dry_run", "applied"]),
|
|
2675
|
+
sites_scanned: z9.number(),
|
|
2676
|
+
total_match_count: z9.number(),
|
|
2677
|
+
by_site: z9.array(z9.object({
|
|
2678
|
+
site_id: z9.string(),
|
|
2679
|
+
url: z9.string(),
|
|
2680
|
+
pages_scanned: z9.number(),
|
|
2681
|
+
matches: z9.number(),
|
|
2682
|
+
error: z9.string().optional()
|
|
2683
|
+
})),
|
|
2684
|
+
confirmation_token: z9.string().optional(),
|
|
2685
|
+
expires_in_seconds: z9.number().optional()
|
|
2686
|
+
}),
|
|
2687
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2688
|
+
async handler(input) {
|
|
2689
|
+
const cfg = loadConfig();
|
|
2690
|
+
const targets = input.site_ids ? cfg.sites.filter((s) => input.site_ids?.includes(s.id)) : cfg.sites;
|
|
2691
|
+
const by_site = [];
|
|
2692
|
+
let total = 0;
|
|
2693
|
+
for (const site of targets) {
|
|
2694
|
+
try {
|
|
2695
|
+
const pages = await listElementorPageIds(site.id);
|
|
2696
|
+
let siteMatches = 0;
|
|
2697
|
+
for (const p of pages) {
|
|
2698
|
+
try {
|
|
2699
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2700
|
+
const v = page.meta?._elementor_data;
|
|
2701
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2702
|
+
const data = parseElementorData(raw);
|
|
2703
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2704
|
+
widgetType: input.widget_type,
|
|
2705
|
+
caseSensitive: input.case_sensitive
|
|
2706
|
+
});
|
|
2707
|
+
siteMatches += dry.replacementCount;
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: pages.length, matches: siteMatches });
|
|
2712
|
+
total += siteMatches;
|
|
2713
|
+
} catch (e) {
|
|
2714
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: 0, matches: 0, error: e.message });
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (!input.confirmation) {
|
|
2718
|
+
if (total === 0) {
|
|
2719
|
+
return {
|
|
2720
|
+
mode: "dry_run",
|
|
2721
|
+
sites_scanned: by_site.length,
|
|
2722
|
+
total_match_count: 0,
|
|
2723
|
+
by_site
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
const token = issueConfirmation("fleet_find_replace", { find: input.find, replace: input.replace }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2727
|
+
return {
|
|
2728
|
+
mode: "dry_run",
|
|
2729
|
+
sites_scanned: by_site.length,
|
|
2730
|
+
total_match_count: total,
|
|
2731
|
+
by_site,
|
|
2732
|
+
confirmation_token: token,
|
|
2733
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
const conf = consumeConfirmation(input.confirmation, "fleet_find_replace");
|
|
2737
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2738
|
+
for (const site of targets) {
|
|
2739
|
+
const pages = await listElementorPageIds(site.id);
|
|
2740
|
+
for (const p of pages) {
|
|
2741
|
+
try {
|
|
2742
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2743
|
+
const v = page.meta?._elementor_data;
|
|
2744
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2745
|
+
const data = parseElementorData(raw);
|
|
2746
|
+
const r = findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2747
|
+
if (r.replacementCount === 0) continue;
|
|
2748
|
+
const ser = serializeElementorData(data);
|
|
2749
|
+
const validation = validateElementorData(ser);
|
|
2750
|
+
if (!validation.valid) continue;
|
|
2751
|
+
await fullBackup(site.id, p.id);
|
|
2752
|
+
await wpRequest(`/wp/v2/pages/${p.id}`, {
|
|
2753
|
+
siteId: site.id,
|
|
2754
|
+
method: "PUT",
|
|
2755
|
+
body: { meta: { _elementor_data: ser } }
|
|
2756
|
+
});
|
|
2757
|
+
await flushCSS(site.id, p.id);
|
|
2758
|
+
} catch {
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
return {
|
|
2763
|
+
mode: "applied",
|
|
2764
|
+
sites_scanned: by_site.length,
|
|
2765
|
+
total_match_count: total,
|
|
2766
|
+
by_site
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
var restoreFromFileTool = defineTool({
|
|
2771
|
+
name: "restore_from_file",
|
|
2772
|
+
description: "Restore a page from a JSON backup file (created by ANY earlier op with backup_to_file=true or by direct fullBackup with to_file). Requires the file_path returned by that backup. Two-call confirmation.",
|
|
2773
|
+
inputSchema: z9.object({
|
|
2774
|
+
site_id: z9.string().optional(),
|
|
2775
|
+
page_id: z9.number().int().positive(),
|
|
2776
|
+
file_path: z9.string().min(1),
|
|
2777
|
+
confirmation: z9.string().optional()
|
|
2778
|
+
}),
|
|
2779
|
+
outputSchema: z9.object({
|
|
2780
|
+
mode: z9.enum(["dry_run", "restored"]),
|
|
2781
|
+
page_id: z9.number(),
|
|
2782
|
+
file_path: z9.string(),
|
|
2783
|
+
method: z9.enum(["wp-cli", "rest"]).optional(),
|
|
2784
|
+
pre_restore_backup_meta_key: z9.string().optional(),
|
|
2785
|
+
css_flush: z9.string().optional(),
|
|
2786
|
+
confirmation_token: z9.string().optional()
|
|
2787
|
+
}),
|
|
2788
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2789
|
+
async handler(input) {
|
|
2790
|
+
if (!input.confirmation) {
|
|
2791
|
+
const token = issueConfirmation("restore_from_file", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2792
|
+
return {
|
|
2793
|
+
mode: "dry_run",
|
|
2794
|
+
page_id: input.page_id,
|
|
2795
|
+
file_path: input.file_path,
|
|
2796
|
+
confirmation_token: token
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
const conf = consumeConfirmation(input.confirmation, "restore_from_file");
|
|
2800
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2801
|
+
const pre = await fullBackup(input.site_id, input.page_id);
|
|
2802
|
+
const r = await restoreFromFile(input.site_id, input.page_id, input.file_path);
|
|
2803
|
+
const flush = await flushCSS(input.site_id, input.page_id);
|
|
2804
|
+
return {
|
|
2805
|
+
mode: "restored",
|
|
2806
|
+
page_id: input.page_id,
|
|
2807
|
+
file_path: input.file_path,
|
|
2808
|
+
method: r.method,
|
|
2809
|
+
pre_restore_backup_meta_key: pre.meta_key,
|
|
2810
|
+
css_flush: flush.method
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2118
2815
|
// src/tools/index.ts
|
|
2119
2816
|
var tools = [
|
|
2120
2817
|
// Sites & health
|
|
@@ -2131,11 +2828,23 @@ var tools = [
|
|
|
2131
2828
|
listElementorBackupsTool,
|
|
2132
2829
|
restoreElementorBackupTool,
|
|
2133
2830
|
duplicateElementorPageTool,
|
|
2831
|
+
// Widget-level CRUD (v1.1)
|
|
2832
|
+
readWidgetTool,
|
|
2833
|
+
updateWidgetSettingsTool,
|
|
2834
|
+
deleteWidgetTool,
|
|
2835
|
+
duplicateWidgetTool,
|
|
2836
|
+
swapWidgetTypeTool,
|
|
2837
|
+
addWidgetTool,
|
|
2838
|
+
moveWidgetTool,
|
|
2134
2839
|
// Templates
|
|
2135
2840
|
listTemplatesTool,
|
|
2136
2841
|
exportTemplateTool,
|
|
2137
2842
|
importTemplateTool,
|
|
2138
2843
|
applyTemplateToPageTool,
|
|
2844
|
+
// Bulk + fleet (v1.1)
|
|
2845
|
+
bulkFindReplaceSiteTool,
|
|
2846
|
+
fleetFindReplaceTool,
|
|
2847
|
+
restoreFromFileTool,
|
|
2139
2848
|
// WP-CLI escape
|
|
2140
2849
|
wpCliRunTool,
|
|
2141
2850
|
wpSearchReplaceTool,
|