elementor-mcp-agent 1.0.0 → 1.2.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 +49 -5
- package/dist/server.js +865 -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,851 @@ var checkElementorVersionsTool = defineTool({
|
|
|
2115
2121
|
}
|
|
2116
2122
|
});
|
|
2117
2123
|
|
|
2124
|
+
// src/tools/widgets.ts
|
|
2125
|
+
import { z as z9 } 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
|
+
|
|
2217
|
+
// src/elementor/verify.ts
|
|
2218
|
+
init_wp_rest();
|
|
2219
|
+
import { z as z8 } from "zod";
|
|
2220
|
+
var VerificationSchema = z8.object({
|
|
2221
|
+
/** Plain-English description of what we re-read and compared. */
|
|
2222
|
+
method: z8.string(),
|
|
2223
|
+
/** Whether a canonical re-read of the page succeeded. */
|
|
2224
|
+
reread_ok: z8.boolean(),
|
|
2225
|
+
/** Op-specific: did the change we requested actually persist? */
|
|
2226
|
+
matches_requested: z8.boolean(),
|
|
2227
|
+
/** Op-specific extra data (e.g. the actual persisted widget settings). */
|
|
2228
|
+
persisted: z8.record(z8.any()).optional(),
|
|
2229
|
+
/** Free-text notes / explanation when matches_requested is false. */
|
|
2230
|
+
notes: z8.string().optional()
|
|
2231
|
+
});
|
|
2232
|
+
async function verifyWrite(args) {
|
|
2233
|
+
try {
|
|
2234
|
+
const page = await wpRequest(
|
|
2235
|
+
`/wp/v2/pages/${args.pageId}?context=edit&_fields=meta`,
|
|
2236
|
+
{ siteId: args.siteId }
|
|
2237
|
+
);
|
|
2238
|
+
const v = page.meta?._elementor_data;
|
|
2239
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2240
|
+
const data = parseElementorData(raw);
|
|
2241
|
+
const result = args.predicate(data);
|
|
2242
|
+
return {
|
|
2243
|
+
method: args.description,
|
|
2244
|
+
reread_ok: true,
|
|
2245
|
+
matches_requested: result.ok,
|
|
2246
|
+
persisted: result.persisted,
|
|
2247
|
+
notes: result.notes
|
|
2248
|
+
};
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
return {
|
|
2251
|
+
method: args.description,
|
|
2252
|
+
reread_ok: false,
|
|
2253
|
+
matches_requested: false,
|
|
2254
|
+
notes: `Canonical re-read failed: ${err.message}`
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
function deepEqual(a, b) {
|
|
2259
|
+
if (a === b) return true;
|
|
2260
|
+
if (typeof a !== typeof b) return false;
|
|
2261
|
+
if (a === null || b === null) return a === b;
|
|
2262
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
2263
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
2264
|
+
return a.every((x, i) => deepEqual(x, b[i]));
|
|
2265
|
+
}
|
|
2266
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
2267
|
+
const ka = Object.keys(a);
|
|
2268
|
+
const kb = Object.keys(b);
|
|
2269
|
+
if (ka.length !== kb.length) return false;
|
|
2270
|
+
return ka.every(
|
|
2271
|
+
(k) => deepEqual(a[k], b[k])
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/tools/widgets.ts
|
|
2278
|
+
var MutationResponseShape = {
|
|
2279
|
+
mode: z9.enum(["dry_run", "applied"]),
|
|
2280
|
+
page_id: z9.number(),
|
|
2281
|
+
confirmation_token: z9.string().optional(),
|
|
2282
|
+
backup_meta_key: z9.string().optional(),
|
|
2283
|
+
css_flush: z9.string().optional(),
|
|
2284
|
+
mutated: z9.boolean().optional(),
|
|
2285
|
+
warnings: z9.array(z9.string()).optional(),
|
|
2286
|
+
verification: VerificationSchema.optional()
|
|
2287
|
+
};
|
|
2288
|
+
async function fetchData(siteId, pageId) {
|
|
2289
|
+
const page = await wpRequest(
|
|
2290
|
+
`/wp/v2/pages/${pageId}?context=edit&_fields=meta`,
|
|
2291
|
+
{ siteId }
|
|
2292
|
+
);
|
|
2293
|
+
const v = page.meta?._elementor_data;
|
|
2294
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2295
|
+
return { raw, data: parseElementorData(raw) };
|
|
2296
|
+
}
|
|
2297
|
+
async function writeData(siteId, pageId, data) {
|
|
2298
|
+
const ser = serializeElementorData(data);
|
|
2299
|
+
const validation = validateElementorData(ser);
|
|
2300
|
+
if (!validation.valid) {
|
|
2301
|
+
throw new Error("Validation failed after edit: " + validation.errors.join("; "));
|
|
2302
|
+
}
|
|
2303
|
+
await wpRequest(`/wp/v2/pages/${pageId}`, {
|
|
2304
|
+
siteId,
|
|
2305
|
+
method: "PUT",
|
|
2306
|
+
body: { meta: { _elementor_data: ser } }
|
|
2307
|
+
});
|
|
2308
|
+
const flush = await flushCSS(siteId, pageId);
|
|
2309
|
+
return { method: flush.method, serialized: ser };
|
|
2310
|
+
}
|
|
2311
|
+
var readWidgetTool = defineTool({
|
|
2312
|
+
name: "read_widget",
|
|
2313
|
+
description: "Fetch a single widget's full settings by id. Use list_widgets_in_page to find the id first.",
|
|
2314
|
+
inputSchema: z9.object({
|
|
2315
|
+
site_id: z9.string().optional(),
|
|
2316
|
+
page_id: z9.number().int().positive(),
|
|
2317
|
+
widget_id: z9.string().min(1)
|
|
2318
|
+
}),
|
|
2319
|
+
outputSchema: z9.object({
|
|
2320
|
+
widget_id: z9.string(),
|
|
2321
|
+
widget_type: z9.string().optional(),
|
|
2322
|
+
settings: z9.record(z9.any())
|
|
2323
|
+
}),
|
|
2324
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2325
|
+
async handler(input) {
|
|
2326
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2327
|
+
const w = readWidget(data, input.widget_id);
|
|
2328
|
+
if (!w) throw new Error(`Widget ${input.widget_id} not found on page ${input.page_id}`);
|
|
2329
|
+
return { widget_id: w.id, widget_type: w.widgetType, settings: w.settings };
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
var updateWidgetSettingsTool = defineTool({
|
|
2333
|
+
name: "update_widget_settings",
|
|
2334
|
+
description: "Shallow-merge a partial settings object into one widget. Backs up the page first, validates the result, auto-flushes CSS, then re-reads the page and verifies the patch persisted (matches_requested in the response). Two-call confirmation.",
|
|
2335
|
+
inputSchema: z9.object({
|
|
2336
|
+
site_id: z9.string().optional(),
|
|
2337
|
+
page_id: z9.number().int().positive(),
|
|
2338
|
+
widget_id: z9.string().min(1),
|
|
2339
|
+
settings_patch: z9.record(z9.any()),
|
|
2340
|
+
confirmation: z9.string().optional()
|
|
2341
|
+
}),
|
|
2342
|
+
outputSchema: z9.object({
|
|
2343
|
+
...MutationResponseShape,
|
|
2344
|
+
widget_id: z9.string(),
|
|
2345
|
+
keys_changed: z9.array(z9.string())
|
|
2346
|
+
}),
|
|
2347
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2348
|
+
async handler(input) {
|
|
2349
|
+
if (!input.confirmation) {
|
|
2350
|
+
const token = issueConfirmation("update_widget_settings", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2351
|
+
return {
|
|
2352
|
+
mode: "dry_run",
|
|
2353
|
+
page_id: input.page_id,
|
|
2354
|
+
widget_id: input.widget_id,
|
|
2355
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2356
|
+
confirmation_token: token
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
const conf = consumeConfirmation(input.confirmation, "update_widget_settings");
|
|
2360
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2361
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2362
|
+
if (!updateWidgetSettings(data, input.widget_id, input.settings_patch)) {
|
|
2363
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2364
|
+
}
|
|
2365
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2366
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2367
|
+
const verification = await verifyWrite({
|
|
2368
|
+
siteId: input.site_id,
|
|
2369
|
+
pageId: input.page_id,
|
|
2370
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and check widget ${input.widget_id} settings include the requested patch`,
|
|
2371
|
+
predicate: (canonical) => {
|
|
2372
|
+
const widget = findElementById(canonical, input.widget_id);
|
|
2373
|
+
if (!widget) return { ok: false, notes: "Widget no longer present after write" };
|
|
2374
|
+
const persisted = widget.settings;
|
|
2375
|
+
const mismatches = [];
|
|
2376
|
+
for (const [k, want] of Object.entries(input.settings_patch)) {
|
|
2377
|
+
if (!deepEqual(persisted[k], want)) mismatches.push(k);
|
|
2378
|
+
}
|
|
2379
|
+
return {
|
|
2380
|
+
ok: mismatches.length === 0,
|
|
2381
|
+
persisted,
|
|
2382
|
+
notes: mismatches.length === 0 ? void 0 : `Persisted state diverges from requested patch on key(s): ${mismatches.join(", ")}`
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
return {
|
|
2387
|
+
mode: "applied",
|
|
2388
|
+
page_id: input.page_id,
|
|
2389
|
+
widget_id: input.widget_id,
|
|
2390
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2391
|
+
backup_meta_key: backup.meta_key,
|
|
2392
|
+
css_flush: w.method,
|
|
2393
|
+
mutated: rawBefore !== w.serialized,
|
|
2394
|
+
warnings: [],
|
|
2395
|
+
verification
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
var deleteWidgetTool = defineTool({
|
|
2400
|
+
name: "delete_widget",
|
|
2401
|
+
description: "Remove a widget from a page by id. Two-call confirmation. Backs up before deleting; re-reads to confirm the widget is gone.",
|
|
2402
|
+
inputSchema: z9.object({
|
|
2403
|
+
site_id: z9.string().optional(),
|
|
2404
|
+
page_id: z9.number().int().positive(),
|
|
2405
|
+
widget_id: z9.string().min(1),
|
|
2406
|
+
confirmation: z9.string().optional()
|
|
2407
|
+
}),
|
|
2408
|
+
outputSchema: z9.object({
|
|
2409
|
+
...MutationResponseShape,
|
|
2410
|
+
widget_id: z9.string()
|
|
2411
|
+
}),
|
|
2412
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2413
|
+
async handler(input) {
|
|
2414
|
+
if (!input.confirmation) {
|
|
2415
|
+
const token = issueConfirmation("delete_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2416
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, confirmation_token: token };
|
|
2417
|
+
}
|
|
2418
|
+
const conf = consumeConfirmation(input.confirmation, "delete_widget");
|
|
2419
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2420
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2421
|
+
if (!deleteWidget(data, input.widget_id)) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2422
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2423
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2424
|
+
const verification = await verifyWrite({
|
|
2425
|
+
siteId: input.site_id,
|
|
2426
|
+
pageId: input.page_id,
|
|
2427
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and assert widget ${input.widget_id} is gone`,
|
|
2428
|
+
predicate: (canonical) => {
|
|
2429
|
+
const found = findElementById(canonical, input.widget_id);
|
|
2430
|
+
return {
|
|
2431
|
+
ok: found === null,
|
|
2432
|
+
notes: found ? "Widget still present in canonical re-read \u2014 delete did not persist" : void 0
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
return {
|
|
2437
|
+
mode: "applied",
|
|
2438
|
+
page_id: input.page_id,
|
|
2439
|
+
widget_id: input.widget_id,
|
|
2440
|
+
backup_meta_key: backup.meta_key,
|
|
2441
|
+
css_flush: w.method,
|
|
2442
|
+
mutated: rawBefore !== w.serialized,
|
|
2443
|
+
warnings: [],
|
|
2444
|
+
verification
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
var duplicateWidgetTool = defineTool({
|
|
2449
|
+
name: "duplicate_widget",
|
|
2450
|
+
description: "Duplicate a widget in place (right after the original). The clone gets a new id. Re-reads to confirm the clone persisted. Two-call confirmation.",
|
|
2451
|
+
inputSchema: z9.object({
|
|
2452
|
+
site_id: z9.string().optional(),
|
|
2453
|
+
page_id: z9.number().int().positive(),
|
|
2454
|
+
widget_id: z9.string().min(1),
|
|
2455
|
+
confirmation: z9.string().optional()
|
|
2456
|
+
}),
|
|
2457
|
+
outputSchema: z9.object({
|
|
2458
|
+
...MutationResponseShape,
|
|
2459
|
+
source_widget_id: z9.string(),
|
|
2460
|
+
new_widget_id: z9.string().optional()
|
|
2461
|
+
}),
|
|
2462
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2463
|
+
async handler(input) {
|
|
2464
|
+
if (!input.confirmation) {
|
|
2465
|
+
const token = issueConfirmation("duplicate_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2466
|
+
return { mode: "dry_run", page_id: input.page_id, source_widget_id: input.widget_id, confirmation_token: token };
|
|
2467
|
+
}
|
|
2468
|
+
const conf = consumeConfirmation(input.confirmation, "duplicate_widget");
|
|
2469
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2470
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2471
|
+
const r = duplicateWidget(data, input.widget_id);
|
|
2472
|
+
if (!r.ok || !r.new_widget_id) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2473
|
+
const newId = r.new_widget_id;
|
|
2474
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2475
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2476
|
+
const verification = await verifyWrite({
|
|
2477
|
+
siteId: input.site_id,
|
|
2478
|
+
pageId: input.page_id,
|
|
2479
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and assert the clone (${newId}) exists alongside the source`,
|
|
2480
|
+
predicate: (canonical) => {
|
|
2481
|
+
const source = findElementById(canonical, input.widget_id);
|
|
2482
|
+
const clone = findElementById(canonical, newId);
|
|
2483
|
+
return {
|
|
2484
|
+
ok: source !== null && clone !== null,
|
|
2485
|
+
notes: !clone ? "Clone not present in canonical re-read" : !source ? "Original is missing after duplicate" : void 0
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
return {
|
|
2490
|
+
mode: "applied",
|
|
2491
|
+
page_id: input.page_id,
|
|
2492
|
+
source_widget_id: input.widget_id,
|
|
2493
|
+
new_widget_id: newId,
|
|
2494
|
+
backup_meta_key: backup.meta_key,
|
|
2495
|
+
css_flush: w.method,
|
|
2496
|
+
mutated: rawBefore !== w.serialized,
|
|
2497
|
+
warnings: [],
|
|
2498
|
+
verification
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
var swapWidgetTypeTool = defineTool({
|
|
2503
|
+
name: "swap_widget_type",
|
|
2504
|
+
description: "Replace a widget's type (e.g., heading \u2192 button) while preserving its id and position. Provide full new_settings \u2014 the old settings are NOT carried over (different widget types have incompatible schemas). Re-reads to confirm. Two-call confirmation.",
|
|
2505
|
+
inputSchema: z9.object({
|
|
2506
|
+
site_id: z9.string().optional(),
|
|
2507
|
+
page_id: z9.number().int().positive(),
|
|
2508
|
+
widget_id: z9.string().min(1),
|
|
2509
|
+
new_widget_type: z9.string().min(1),
|
|
2510
|
+
new_settings: z9.record(z9.any()).default({}),
|
|
2511
|
+
confirmation: z9.string().optional()
|
|
2512
|
+
}),
|
|
2513
|
+
outputSchema: z9.object({
|
|
2514
|
+
...MutationResponseShape,
|
|
2515
|
+
widget_id: z9.string(),
|
|
2516
|
+
new_widget_type: z9.string()
|
|
2517
|
+
}),
|
|
2518
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2519
|
+
async handler(input) {
|
|
2520
|
+
if (!input.confirmation) {
|
|
2521
|
+
const token = issueConfirmation("swap_widget_type", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2522
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_widget_type: input.new_widget_type, confirmation_token: token };
|
|
2523
|
+
}
|
|
2524
|
+
const conf = consumeConfirmation(input.confirmation, "swap_widget_type");
|
|
2525
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2526
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2527
|
+
if (!swapWidgetType(data, input.widget_id, input.new_widget_type, input.new_settings)) {
|
|
2528
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2529
|
+
}
|
|
2530
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2531
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2532
|
+
const verification = await verifyWrite({
|
|
2533
|
+
siteId: input.site_id,
|
|
2534
|
+
pageId: input.page_id,
|
|
2535
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and assert widget ${input.widget_id} now has widgetType="${input.new_widget_type}"`,
|
|
2536
|
+
predicate: (canonical) => {
|
|
2537
|
+
const widget = findElementById(canonical, input.widget_id);
|
|
2538
|
+
if (!widget) return { ok: false, notes: "Widget missing after swap" };
|
|
2539
|
+
return {
|
|
2540
|
+
ok: widget.widgetType === input.new_widget_type,
|
|
2541
|
+
persisted: { widgetType: widget.widgetType, settings: widget.settings },
|
|
2542
|
+
notes: widget.widgetType === input.new_widget_type ? void 0 : `Expected widgetType="${input.new_widget_type}", canonical state has "${widget.widgetType}"`
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
return {
|
|
2547
|
+
mode: "applied",
|
|
2548
|
+
page_id: input.page_id,
|
|
2549
|
+
widget_id: input.widget_id,
|
|
2550
|
+
new_widget_type: input.new_widget_type,
|
|
2551
|
+
backup_meta_key: backup.meta_key,
|
|
2552
|
+
css_flush: w.method,
|
|
2553
|
+
mutated: rawBefore !== w.serialized,
|
|
2554
|
+
warnings: [],
|
|
2555
|
+
verification
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
var addWidgetTool = defineTool({
|
|
2560
|
+
name: "add_widget",
|
|
2561
|
+
description: "Append a new widget to a parent container (section, column, or container) on a page. Re-reads to confirm the new widget exists under the parent. Two-call confirmation.",
|
|
2562
|
+
inputSchema: z9.object({
|
|
2563
|
+
site_id: z9.string().optional(),
|
|
2564
|
+
page_id: z9.number().int().positive(),
|
|
2565
|
+
parent_id: z9.string().min(1).describe("Id of the section/column/container that will receive the widget."),
|
|
2566
|
+
widget_type: z9.string().min(1).describe("e.g., 'heading', 'text-editor', 'button', 'image'."),
|
|
2567
|
+
settings: z9.record(z9.any()).default({}),
|
|
2568
|
+
confirmation: z9.string().optional()
|
|
2569
|
+
}),
|
|
2570
|
+
outputSchema: z9.object({
|
|
2571
|
+
...MutationResponseShape,
|
|
2572
|
+
parent_id: z9.string(),
|
|
2573
|
+
widget_type: z9.string(),
|
|
2574
|
+
new_widget_id: z9.string().optional()
|
|
2575
|
+
}),
|
|
2576
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2577
|
+
async handler(input) {
|
|
2578
|
+
if (!input.confirmation) {
|
|
2579
|
+
const token = issueConfirmation("add_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2580
|
+
return { mode: "dry_run", page_id: input.page_id, parent_id: input.parent_id, widget_type: input.widget_type, confirmation_token: token };
|
|
2581
|
+
}
|
|
2582
|
+
const conf = consumeConfirmation(input.confirmation, "add_widget");
|
|
2583
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2584
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2585
|
+
const r = addWidget(data, input.parent_id, input.widget_type, input.settings);
|
|
2586
|
+
if (!r.ok || !r.new_widget_id) throw new Error(`Parent ${input.parent_id} not found`);
|
|
2587
|
+
const newId = r.new_widget_id;
|
|
2588
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2589
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2590
|
+
const verification = await verifyWrite({
|
|
2591
|
+
siteId: input.site_id,
|
|
2592
|
+
pageId: input.page_id,
|
|
2593
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and assert new widget ${newId} exists under parent ${input.parent_id} with widgetType="${input.widget_type}"`,
|
|
2594
|
+
predicate: (canonical) => {
|
|
2595
|
+
const widget = findElementById(canonical, newId);
|
|
2596
|
+
if (!widget) return { ok: false, notes: "New widget not found after add" };
|
|
2597
|
+
const parent = findElementById(canonical, input.parent_id);
|
|
2598
|
+
const isUnderParent = parent?.elements?.some((e) => e.id === newId) === true;
|
|
2599
|
+
return {
|
|
2600
|
+
ok: widget.widgetType === input.widget_type && isUnderParent,
|
|
2601
|
+
notes: !isUnderParent ? `Widget exists but is not under expected parent ${input.parent_id}` : widget.widgetType !== input.widget_type ? `Widget exists with wrong widgetType "${widget.widgetType}"` : void 0
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
return {
|
|
2606
|
+
mode: "applied",
|
|
2607
|
+
page_id: input.page_id,
|
|
2608
|
+
parent_id: input.parent_id,
|
|
2609
|
+
widget_type: input.widget_type,
|
|
2610
|
+
new_widget_id: newId,
|
|
2611
|
+
backup_meta_key: backup.meta_key,
|
|
2612
|
+
css_flush: w.method,
|
|
2613
|
+
mutated: rawBefore !== w.serialized,
|
|
2614
|
+
warnings: [],
|
|
2615
|
+
verification
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
var moveWidgetTool = defineTool({
|
|
2620
|
+
name: "move_widget",
|
|
2621
|
+
description: "Move a widget to a different parent (or different position in the same parent). Re-reads to confirm new parent. Two-call confirmation.",
|
|
2622
|
+
inputSchema: z9.object({
|
|
2623
|
+
site_id: z9.string().optional(),
|
|
2624
|
+
page_id: z9.number().int().positive(),
|
|
2625
|
+
widget_id: z9.string().min(1),
|
|
2626
|
+
new_parent_id: z9.string().min(1),
|
|
2627
|
+
position: z9.number().int().default(-1).describe("0-based position in the new parent. -1 = append."),
|
|
2628
|
+
confirmation: z9.string().optional()
|
|
2629
|
+
}),
|
|
2630
|
+
outputSchema: z9.object({
|
|
2631
|
+
...MutationResponseShape,
|
|
2632
|
+
widget_id: z9.string(),
|
|
2633
|
+
new_parent_id: z9.string()
|
|
2634
|
+
}),
|
|
2635
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2636
|
+
async handler(input) {
|
|
2637
|
+
if (!input.confirmation) {
|
|
2638
|
+
const token = issueConfirmation("move_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2639
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_parent_id: input.new_parent_id, confirmation_token: token };
|
|
2640
|
+
}
|
|
2641
|
+
const conf = consumeConfirmation(input.confirmation, "move_widget");
|
|
2642
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2643
|
+
const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
|
|
2644
|
+
if (!moveWidget(data, input.widget_id, input.new_parent_id, input.position)) {
|
|
2645
|
+
throw new Error(`Widget or parent not found`);
|
|
2646
|
+
}
|
|
2647
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2648
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2649
|
+
const verification = await verifyWrite({
|
|
2650
|
+
siteId: input.site_id,
|
|
2651
|
+
pageId: input.page_id,
|
|
2652
|
+
description: `Re-read /wp/v2/pages/${input.page_id} and assert widget ${input.widget_id} is now a direct child of ${input.new_parent_id}`,
|
|
2653
|
+
predicate: (canonical) => {
|
|
2654
|
+
const parent = findElementById(canonical, input.new_parent_id);
|
|
2655
|
+
const isUnderNewParent = parent?.elements?.some((e) => e.id === input.widget_id) === true;
|
|
2656
|
+
return {
|
|
2657
|
+
ok: isUnderNewParent,
|
|
2658
|
+
notes: isUnderNewParent ? void 0 : `Widget ${input.widget_id} not found under ${input.new_parent_id} after move`
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
return {
|
|
2663
|
+
mode: "applied",
|
|
2664
|
+
page_id: input.page_id,
|
|
2665
|
+
widget_id: input.widget_id,
|
|
2666
|
+
new_parent_id: input.new_parent_id,
|
|
2667
|
+
backup_meta_key: backup.meta_key,
|
|
2668
|
+
css_flush: w.method,
|
|
2669
|
+
mutated: rawBefore !== w.serialized,
|
|
2670
|
+
warnings: [],
|
|
2671
|
+
verification
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
// src/tools/bulk.ts
|
|
2677
|
+
import { z as z10 } from "zod";
|
|
2678
|
+
init_wp_rest();
|
|
2679
|
+
init_config();
|
|
2680
|
+
init_backup();
|
|
2681
|
+
init_css_flush();
|
|
2682
|
+
init_policies();
|
|
2683
|
+
init_confirmation();
|
|
2684
|
+
async function listElementorPageIds(siteId) {
|
|
2685
|
+
const out = [];
|
|
2686
|
+
let page = 1;
|
|
2687
|
+
for (; ; ) {
|
|
2688
|
+
const items = await wpRequest("/wp/v2/pages", {
|
|
2689
|
+
siteId,
|
|
2690
|
+
query: {
|
|
2691
|
+
meta_key: "_elementor_edit_mode",
|
|
2692
|
+
meta_value: "builder",
|
|
2693
|
+
context: "edit",
|
|
2694
|
+
per_page: 100,
|
|
2695
|
+
page,
|
|
2696
|
+
_fields: "id,title"
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
if (items.length === 0) break;
|
|
2700
|
+
out.push(...items.map((p) => ({ id: p.id, title: p.title.rendered })));
|
|
2701
|
+
if (items.length < 100) break;
|
|
2702
|
+
page++;
|
|
2703
|
+
if (page > 50) break;
|
|
2704
|
+
}
|
|
2705
|
+
return out;
|
|
2706
|
+
}
|
|
2707
|
+
var bulkFindReplaceSiteTool = defineTool({
|
|
2708
|
+
name: "bulk_find_replace_site",
|
|
2709
|
+
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.",
|
|
2710
|
+
inputSchema: z10.object({
|
|
2711
|
+
site_id: z10.string().optional(),
|
|
2712
|
+
find: z10.string().min(1),
|
|
2713
|
+
replace: z10.string(),
|
|
2714
|
+
widget_type: z10.string().optional(),
|
|
2715
|
+
case_sensitive: z10.boolean().default(false),
|
|
2716
|
+
confirmation: z10.string().optional()
|
|
2717
|
+
}),
|
|
2718
|
+
outputSchema: z10.object({
|
|
2719
|
+
mode: z10.enum(["dry_run", "applied"]),
|
|
2720
|
+
site_id: z10.string(),
|
|
2721
|
+
pages_scanned: z10.number(),
|
|
2722
|
+
total_match_count: z10.number(),
|
|
2723
|
+
pages_with_matches: z10.array(z10.object({
|
|
2724
|
+
page_id: z10.number(),
|
|
2725
|
+
title: z10.string(),
|
|
2726
|
+
match_count: z10.number()
|
|
2727
|
+
})),
|
|
2728
|
+
pages_applied: z10.array(z10.object({
|
|
2729
|
+
page_id: z10.number(),
|
|
2730
|
+
backup_meta_key: z10.string().optional(),
|
|
2731
|
+
css_flush: z10.string().optional(),
|
|
2732
|
+
mode: z10.enum(["applied", "rolled_back", "skipped"]),
|
|
2733
|
+
error: z10.string().optional()
|
|
2734
|
+
})).optional(),
|
|
2735
|
+
confirmation_token: z10.string().optional(),
|
|
2736
|
+
expires_in_seconds: z10.number().optional()
|
|
2737
|
+
}),
|
|
2738
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2739
|
+
async handler(input) {
|
|
2740
|
+
const pages = await listElementorPageIds(input.site_id);
|
|
2741
|
+
const matches = [];
|
|
2742
|
+
for (const p of pages) {
|
|
2743
|
+
try {
|
|
2744
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2745
|
+
const v = page.meta?._elementor_data;
|
|
2746
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2747
|
+
const data = parseElementorData(raw);
|
|
2748
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2749
|
+
widgetType: input.widget_type,
|
|
2750
|
+
caseSensitive: input.case_sensitive
|
|
2751
|
+
});
|
|
2752
|
+
if (dry.replacementCount > 0) matches.push({ page_id: p.id, title: p.title, match_count: dry.replacementCount });
|
|
2753
|
+
} catch {
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
const total = matches.reduce((s, m) => s + m.match_count, 0);
|
|
2757
|
+
if (!input.confirmation) {
|
|
2758
|
+
if (total === 0) {
|
|
2759
|
+
return {
|
|
2760
|
+
mode: "dry_run",
|
|
2761
|
+
site_id: loadConfig().default_site_id ?? "default",
|
|
2762
|
+
pages_scanned: pages.length,
|
|
2763
|
+
total_match_count: 0,
|
|
2764
|
+
pages_with_matches: []
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
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);
|
|
2768
|
+
return {
|
|
2769
|
+
mode: "dry_run",
|
|
2770
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2771
|
+
pages_scanned: pages.length,
|
|
2772
|
+
total_match_count: total,
|
|
2773
|
+
pages_with_matches: matches,
|
|
2774
|
+
confirmation_token: token,
|
|
2775
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
const conf = consumeConfirmation(input.confirmation, "bulk_find_replace_site");
|
|
2779
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2780
|
+
const applied = [];
|
|
2781
|
+
for (const m of matches) {
|
|
2782
|
+
try {
|
|
2783
|
+
const page = await wpRequest(`/wp/v2/pages/${m.page_id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2784
|
+
const v = page.meta?._elementor_data;
|
|
2785
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2786
|
+
const data = parseElementorData(raw);
|
|
2787
|
+
findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2788
|
+
const ser = serializeElementorData(data);
|
|
2789
|
+
const validation = validateElementorData(ser);
|
|
2790
|
+
if (!validation.valid) {
|
|
2791
|
+
applied.push({ page_id: m.page_id, mode: "rolled_back", error: validation.errors.join("; ") });
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
const backup = await fullBackup(input.site_id, m.page_id);
|
|
2795
|
+
await wpRequest(`/wp/v2/pages/${m.page_id}`, {
|
|
2796
|
+
siteId: input.site_id,
|
|
2797
|
+
method: "PUT",
|
|
2798
|
+
body: { meta: { _elementor_data: ser } }
|
|
2799
|
+
});
|
|
2800
|
+
const flush = await flushCSS(input.site_id, m.page_id);
|
|
2801
|
+
applied.push({ page_id: m.page_id, backup_meta_key: backup.meta_key, css_flush: flush.method, mode: "applied" });
|
|
2802
|
+
} catch (e) {
|
|
2803
|
+
applied.push({ page_id: m.page_id, mode: "skipped", error: e.message });
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return {
|
|
2807
|
+
mode: "applied",
|
|
2808
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2809
|
+
pages_scanned: pages.length,
|
|
2810
|
+
total_match_count: total,
|
|
2811
|
+
pages_with_matches: matches,
|
|
2812
|
+
pages_applied: applied
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
var fleetFindReplaceTool = defineTool({
|
|
2817
|
+
name: "fleet_find_replace",
|
|
2818
|
+
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.",
|
|
2819
|
+
inputSchema: z10.object({
|
|
2820
|
+
find: z10.string().min(1),
|
|
2821
|
+
replace: z10.string(),
|
|
2822
|
+
site_ids: z10.array(z10.string()).optional().describe("Subset of sites to hit. Defaults to all."),
|
|
2823
|
+
widget_type: z10.string().optional(),
|
|
2824
|
+
case_sensitive: z10.boolean().default(false),
|
|
2825
|
+
confirmation: z10.string().optional()
|
|
2826
|
+
}),
|
|
2827
|
+
outputSchema: z10.object({
|
|
2828
|
+
mode: z10.enum(["dry_run", "applied"]),
|
|
2829
|
+
sites_scanned: z10.number(),
|
|
2830
|
+
total_match_count: z10.number(),
|
|
2831
|
+
by_site: z10.array(z10.object({
|
|
2832
|
+
site_id: z10.string(),
|
|
2833
|
+
url: z10.string(),
|
|
2834
|
+
pages_scanned: z10.number(),
|
|
2835
|
+
matches: z10.number(),
|
|
2836
|
+
error: z10.string().optional()
|
|
2837
|
+
})),
|
|
2838
|
+
confirmation_token: z10.string().optional(),
|
|
2839
|
+
expires_in_seconds: z10.number().optional()
|
|
2840
|
+
}),
|
|
2841
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2842
|
+
async handler(input) {
|
|
2843
|
+
const cfg = loadConfig();
|
|
2844
|
+
const targets = input.site_ids ? cfg.sites.filter((s) => input.site_ids?.includes(s.id)) : cfg.sites;
|
|
2845
|
+
const by_site = [];
|
|
2846
|
+
let total = 0;
|
|
2847
|
+
for (const site of targets) {
|
|
2848
|
+
try {
|
|
2849
|
+
const pages = await listElementorPageIds(site.id);
|
|
2850
|
+
let siteMatches = 0;
|
|
2851
|
+
for (const p of pages) {
|
|
2852
|
+
try {
|
|
2853
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2854
|
+
const v = page.meta?._elementor_data;
|
|
2855
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2856
|
+
const data = parseElementorData(raw);
|
|
2857
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2858
|
+
widgetType: input.widget_type,
|
|
2859
|
+
caseSensitive: input.case_sensitive
|
|
2860
|
+
});
|
|
2861
|
+
siteMatches += dry.replacementCount;
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: pages.length, matches: siteMatches });
|
|
2866
|
+
total += siteMatches;
|
|
2867
|
+
} catch (e) {
|
|
2868
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: 0, matches: 0, error: e.message });
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (!input.confirmation) {
|
|
2872
|
+
if (total === 0) {
|
|
2873
|
+
return {
|
|
2874
|
+
mode: "dry_run",
|
|
2875
|
+
sites_scanned: by_site.length,
|
|
2876
|
+
total_match_count: 0,
|
|
2877
|
+
by_site
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
const token = issueConfirmation("fleet_find_replace", { find: input.find, replace: input.replace }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2881
|
+
return {
|
|
2882
|
+
mode: "dry_run",
|
|
2883
|
+
sites_scanned: by_site.length,
|
|
2884
|
+
total_match_count: total,
|
|
2885
|
+
by_site,
|
|
2886
|
+
confirmation_token: token,
|
|
2887
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
const conf = consumeConfirmation(input.confirmation, "fleet_find_replace");
|
|
2891
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2892
|
+
for (const site of targets) {
|
|
2893
|
+
const pages = await listElementorPageIds(site.id);
|
|
2894
|
+
for (const p of pages) {
|
|
2895
|
+
try {
|
|
2896
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2897
|
+
const v = page.meta?._elementor_data;
|
|
2898
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2899
|
+
const data = parseElementorData(raw);
|
|
2900
|
+
const r = findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2901
|
+
if (r.replacementCount === 0) continue;
|
|
2902
|
+
const ser = serializeElementorData(data);
|
|
2903
|
+
const validation = validateElementorData(ser);
|
|
2904
|
+
if (!validation.valid) continue;
|
|
2905
|
+
await fullBackup(site.id, p.id);
|
|
2906
|
+
await wpRequest(`/wp/v2/pages/${p.id}`, {
|
|
2907
|
+
siteId: site.id,
|
|
2908
|
+
method: "PUT",
|
|
2909
|
+
body: { meta: { _elementor_data: ser } }
|
|
2910
|
+
});
|
|
2911
|
+
await flushCSS(site.id, p.id);
|
|
2912
|
+
} catch {
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return {
|
|
2917
|
+
mode: "applied",
|
|
2918
|
+
sites_scanned: by_site.length,
|
|
2919
|
+
total_match_count: total,
|
|
2920
|
+
by_site
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
var restoreFromFileTool = defineTool({
|
|
2925
|
+
name: "restore_from_file",
|
|
2926
|
+
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.",
|
|
2927
|
+
inputSchema: z10.object({
|
|
2928
|
+
site_id: z10.string().optional(),
|
|
2929
|
+
page_id: z10.number().int().positive(),
|
|
2930
|
+
file_path: z10.string().min(1),
|
|
2931
|
+
confirmation: z10.string().optional()
|
|
2932
|
+
}),
|
|
2933
|
+
outputSchema: z10.object({
|
|
2934
|
+
mode: z10.enum(["dry_run", "restored"]),
|
|
2935
|
+
page_id: z10.number(),
|
|
2936
|
+
file_path: z10.string(),
|
|
2937
|
+
method: z10.enum(["wp-cli", "rest"]).optional(),
|
|
2938
|
+
pre_restore_backup_meta_key: z10.string().optional(),
|
|
2939
|
+
css_flush: z10.string().optional(),
|
|
2940
|
+
confirmation_token: z10.string().optional()
|
|
2941
|
+
}),
|
|
2942
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2943
|
+
async handler(input) {
|
|
2944
|
+
if (!input.confirmation) {
|
|
2945
|
+
const token = issueConfirmation("restore_from_file", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2946
|
+
return {
|
|
2947
|
+
mode: "dry_run",
|
|
2948
|
+
page_id: input.page_id,
|
|
2949
|
+
file_path: input.file_path,
|
|
2950
|
+
confirmation_token: token
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
const conf = consumeConfirmation(input.confirmation, "restore_from_file");
|
|
2954
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2955
|
+
const pre = await fullBackup(input.site_id, input.page_id);
|
|
2956
|
+
const r = await restoreFromFile(input.site_id, input.page_id, input.file_path);
|
|
2957
|
+
const flush = await flushCSS(input.site_id, input.page_id);
|
|
2958
|
+
return {
|
|
2959
|
+
mode: "restored",
|
|
2960
|
+
page_id: input.page_id,
|
|
2961
|
+
file_path: input.file_path,
|
|
2962
|
+
method: r.method,
|
|
2963
|
+
pre_restore_backup_meta_key: pre.meta_key,
|
|
2964
|
+
css_flush: flush.method
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2118
2969
|
// src/tools/index.ts
|
|
2119
2970
|
var tools = [
|
|
2120
2971
|
// Sites & health
|
|
@@ -2131,11 +2982,23 @@ var tools = [
|
|
|
2131
2982
|
listElementorBackupsTool,
|
|
2132
2983
|
restoreElementorBackupTool,
|
|
2133
2984
|
duplicateElementorPageTool,
|
|
2985
|
+
// Widget-level CRUD (v1.1)
|
|
2986
|
+
readWidgetTool,
|
|
2987
|
+
updateWidgetSettingsTool,
|
|
2988
|
+
deleteWidgetTool,
|
|
2989
|
+
duplicateWidgetTool,
|
|
2990
|
+
swapWidgetTypeTool,
|
|
2991
|
+
addWidgetTool,
|
|
2992
|
+
moveWidgetTool,
|
|
2134
2993
|
// Templates
|
|
2135
2994
|
listTemplatesTool,
|
|
2136
2995
|
exportTemplateTool,
|
|
2137
2996
|
importTemplateTool,
|
|
2138
2997
|
applyTemplateToPageTool,
|
|
2998
|
+
// Bulk + fleet (v1.1)
|
|
2999
|
+
bulkFindReplaceSiteTool,
|
|
3000
|
+
fleetFindReplaceTool,
|
|
3001
|
+
restoreFromFileTool,
|
|
2139
3002
|
// WP-CLI escape
|
|
2140
3003
|
wpCliRunTool,
|
|
2141
3004
|
wpSearchReplaceTool,
|