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/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 writeData = await sshWpCli(site, `post meta update ${postId} _elementor_data ${shellQuote(data_value)}`, { timeout_ms: 3e4 });
452
- if (writeData.exitCode !== 0) throw new Error(`Restore write failed: ${writeData.stderr}`);
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,