elementor-mcp-agent 1.1.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/dist/server.js CHANGED
@@ -2122,7 +2122,7 @@ var checkElementorVersionsTool = defineTool({
2122
2122
  });
2123
2123
 
2124
2124
  // src/tools/widgets.ts
2125
- import { z as z8 } from "zod";
2125
+ import { z as z9 } from "zod";
2126
2126
  init_wp_rest();
2127
2127
 
2128
2128
  // src/elementor/widget-ops.ts
@@ -2213,6 +2213,78 @@ init_backup();
2213
2213
  init_css_flush();
2214
2214
  init_policies();
2215
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
+ };
2216
2288
  async function fetchData(siteId, pageId) {
2217
2289
  const page = await wpRequest(
2218
2290
  `/wp/v2/pages/${pageId}?context=edit&_fields=meta`,
@@ -2234,20 +2306,20 @@ async function writeData(siteId, pageId, data) {
2234
2306
  body: { meta: { _elementor_data: ser } }
2235
2307
  });
2236
2308
  const flush = await flushCSS(siteId, pageId);
2237
- return { method: flush.method };
2309
+ return { method: flush.method, serialized: ser };
2238
2310
  }
2239
2311
  var readWidgetTool = defineTool({
2240
2312
  name: "read_widget",
2241
2313
  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)
2314
+ inputSchema: z9.object({
2315
+ site_id: z9.string().optional(),
2316
+ page_id: z9.number().int().positive(),
2317
+ widget_id: z9.string().min(1)
2246
2318
  }),
2247
- outputSchema: z8.object({
2248
- widget_id: z8.string(),
2249
- widget_type: z8.string().optional(),
2250
- settings: z8.record(z8.any())
2319
+ outputSchema: z9.object({
2320
+ widget_id: z9.string(),
2321
+ widget_type: z9.string().optional(),
2322
+ settings: z9.record(z9.any())
2251
2323
  }),
2252
2324
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2253
2325
  async handler(input) {
@@ -2259,22 +2331,18 @@ var readWidgetTool = defineTool({
2259
2331
  });
2260
2332
  var updateWidgetSettingsTool = defineTool({
2261
2333
  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()
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())
2278
2346
  }),
2279
2347
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2280
2348
  async handler(input) {
@@ -2290,38 +2358,56 @@ var updateWidgetSettingsTool = defineTool({
2290
2358
  }
2291
2359
  const conf = consumeConfirmation(input.confirmation, "update_widget_settings");
2292
2360
  if (!conf) throw new Error("Invalid or expired confirmation token");
2293
- const { data } = await fetchData(input.site_id, input.page_id);
2361
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2294
2362
  if (!updateWidgetSettings(data, input.widget_id, input.settings_patch)) {
2295
2363
  throw new Error(`Widget ${input.widget_id} not found`);
2296
2364
  }
2297
2365
  const backup = await fullBackup(input.site_id, input.page_id);
2298
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
+ });
2299
2386
  return {
2300
2387
  mode: "applied",
2301
2388
  page_id: input.page_id,
2302
2389
  widget_id: input.widget_id,
2303
2390
  keys_changed: Object.keys(input.settings_patch),
2304
2391
  backup_meta_key: backup.meta_key,
2305
- css_flush: w.method
2392
+ css_flush: w.method,
2393
+ mutated: rawBefore !== w.serialized,
2394
+ warnings: [],
2395
+ verification
2306
2396
  };
2307
2397
  }
2308
2398
  });
2309
2399
  var deleteWidgetTool = defineTool({
2310
2400
  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()
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()
2325
2411
  }),
2326
2412
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2327
2413
  async handler(input) {
@@ -2331,36 +2417,47 @@ var deleteWidgetTool = defineTool({
2331
2417
  }
2332
2418
  const conf = consumeConfirmation(input.confirmation, "delete_widget");
2333
2419
  if (!conf) throw new Error("Invalid or expired confirmation token");
2334
- const { data } = await fetchData(input.site_id, input.page_id);
2420
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2335
2421
  if (!deleteWidget(data, input.widget_id)) throw new Error(`Widget ${input.widget_id} not found`);
2336
2422
  const backup = await fullBackup(input.site_id, input.page_id);
2337
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
+ });
2338
2436
  return {
2339
2437
  mode: "applied",
2340
2438
  page_id: input.page_id,
2341
2439
  widget_id: input.widget_id,
2342
2440
  backup_meta_key: backup.meta_key,
2343
- css_flush: w.method
2441
+ css_flush: w.method,
2442
+ mutated: rawBefore !== w.serialized,
2443
+ warnings: [],
2444
+ verification
2344
2445
  };
2345
2446
  }
2346
2447
  });
2347
2448
  var duplicateWidgetTool = defineTool({
2348
2449
  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()
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()
2364
2461
  }),
2365
2462
  annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
2366
2463
  async handler(input) {
@@ -2370,40 +2467,53 @@ var duplicateWidgetTool = defineTool({
2370
2467
  }
2371
2468
  const conf = consumeConfirmation(input.confirmation, "duplicate_widget");
2372
2469
  if (!conf) throw new Error("Invalid or expired confirmation token");
2373
- const { data } = await fetchData(input.site_id, input.page_id);
2470
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2374
2471
  const r = duplicateWidget(data, input.widget_id);
2375
- if (!r.ok) throw new Error(`Widget ${input.widget_id} not found`);
2472
+ if (!r.ok || !r.new_widget_id) throw new Error(`Widget ${input.widget_id} not found`);
2473
+ const newId = r.new_widget_id;
2376
2474
  const backup = await fullBackup(input.site_id, input.page_id);
2377
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
+ });
2378
2489
  return {
2379
2490
  mode: "applied",
2380
2491
  page_id: input.page_id,
2381
2492
  source_widget_id: input.widget_id,
2382
- new_widget_id: r.new_widget_id,
2493
+ new_widget_id: newId,
2383
2494
  backup_meta_key: backup.meta_key,
2384
- css_flush: w.method
2495
+ css_flush: w.method,
2496
+ mutated: rawBefore !== w.serialized,
2497
+ warnings: [],
2498
+ verification
2385
2499
  };
2386
2500
  }
2387
2501
  });
2388
2502
  var swapWidgetTypeTool = defineTool({
2389
2503
  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()
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()
2407
2517
  }),
2408
2518
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2409
2519
  async handler(input) {
@@ -2413,42 +2523,55 @@ var swapWidgetTypeTool = defineTool({
2413
2523
  }
2414
2524
  const conf = consumeConfirmation(input.confirmation, "swap_widget_type");
2415
2525
  if (!conf) throw new Error("Invalid or expired confirmation token");
2416
- const { data } = await fetchData(input.site_id, input.page_id);
2526
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2417
2527
  if (!swapWidgetType(data, input.widget_id, input.new_widget_type, input.new_settings)) {
2418
2528
  throw new Error(`Widget ${input.widget_id} not found`);
2419
2529
  }
2420
2530
  const backup = await fullBackup(input.site_id, input.page_id);
2421
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
+ });
2422
2546
  return {
2423
2547
  mode: "applied",
2424
2548
  page_id: input.page_id,
2425
2549
  widget_id: input.widget_id,
2426
2550
  new_widget_type: input.new_widget_type,
2427
2551
  backup_meta_key: backup.meta_key,
2428
- css_flush: w.method
2552
+ css_flush: w.method,
2553
+ mutated: rawBefore !== w.serialized,
2554
+ warnings: [],
2555
+ verification
2429
2556
  };
2430
2557
  }
2431
2558
  });
2432
2559
  var addWidgetTool = defineTool({
2433
2560
  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()
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()
2452
2575
  }),
2453
2576
  annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
2454
2577
  async handler(input) {
@@ -2458,41 +2581,56 @@ var addWidgetTool = defineTool({
2458
2581
  }
2459
2582
  const conf = consumeConfirmation(input.confirmation, "add_widget");
2460
2583
  if (!conf) throw new Error("Invalid or expired confirmation token");
2461
- const { data } = await fetchData(input.site_id, input.page_id);
2584
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2462
2585
  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`);
2586
+ if (!r.ok || !r.new_widget_id) throw new Error(`Parent ${input.parent_id} not found`);
2587
+ const newId = r.new_widget_id;
2464
2588
  const backup = await fullBackup(input.site_id, input.page_id);
2465
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
+ });
2466
2605
  return {
2467
2606
  mode: "applied",
2468
2607
  page_id: input.page_id,
2469
2608
  parent_id: input.parent_id,
2470
2609
  widget_type: input.widget_type,
2471
- new_widget_id: r.new_widget_id,
2610
+ new_widget_id: newId,
2472
2611
  backup_meta_key: backup.meta_key,
2473
- css_flush: w.method
2612
+ css_flush: w.method,
2613
+ mutated: rawBefore !== w.serialized,
2614
+ warnings: [],
2615
+ verification
2474
2616
  };
2475
2617
  }
2476
2618
  });
2477
2619
  var moveWidgetTool = defineTool({
2478
2620
  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()
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()
2496
2634
  }),
2497
2635
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2498
2636
  async handler(input) {
@@ -2502,25 +2640,41 @@ var moveWidgetTool = defineTool({
2502
2640
  }
2503
2641
  const conf = consumeConfirmation(input.confirmation, "move_widget");
2504
2642
  if (!conf) throw new Error("Invalid or expired confirmation token");
2505
- const { data } = await fetchData(input.site_id, input.page_id);
2643
+ const { raw: rawBefore, data } = await fetchData(input.site_id, input.page_id);
2506
2644
  if (!moveWidget(data, input.widget_id, input.new_parent_id, input.position)) {
2507
2645
  throw new Error(`Widget or parent not found`);
2508
2646
  }
2509
2647
  const backup = await fullBackup(input.site_id, input.page_id);
2510
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
+ });
2511
2662
  return {
2512
2663
  mode: "applied",
2513
2664
  page_id: input.page_id,
2514
2665
  widget_id: input.widget_id,
2515
2666
  new_parent_id: input.new_parent_id,
2516
2667
  backup_meta_key: backup.meta_key,
2517
- css_flush: w.method
2668
+ css_flush: w.method,
2669
+ mutated: rawBefore !== w.serialized,
2670
+ warnings: [],
2671
+ verification
2518
2672
  };
2519
2673
  }
2520
2674
  });
2521
2675
 
2522
2676
  // src/tools/bulk.ts
2523
- import { z as z9 } from "zod";
2677
+ import { z as z10 } from "zod";
2524
2678
  init_wp_rest();
2525
2679
  init_config();
2526
2680
  init_backup();
@@ -2553,33 +2707,33 @@ async function listElementorPageIds(siteId) {
2553
2707
  var bulkFindReplaceSiteTool = defineTool({
2554
2708
  name: "bulk_find_replace_site",
2555
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.",
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()
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()
2573
2727
  })),
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()
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()
2580
2734
  })).optional(),
2581
- confirmation_token: z9.string().optional(),
2582
- expires_in_seconds: z9.number().optional()
2735
+ confirmation_token: z10.string().optional(),
2736
+ expires_in_seconds: z10.number().optional()
2583
2737
  }),
2584
2738
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2585
2739
  async handler(input) {
@@ -2662,27 +2816,27 @@ var bulkFindReplaceSiteTool = defineTool({
2662
2816
  var fleetFindReplaceTool = defineTool({
2663
2817
  name: "fleet_find_replace",
2664
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.",
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()
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()
2683
2837
  })),
2684
- confirmation_token: z9.string().optional(),
2685
- expires_in_seconds: z9.number().optional()
2838
+ confirmation_token: z10.string().optional(),
2839
+ expires_in_seconds: z10.number().optional()
2686
2840
  }),
2687
2841
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2688
2842
  async handler(input) {
@@ -2770,20 +2924,20 @@ var fleetFindReplaceTool = defineTool({
2770
2924
  var restoreFromFileTool = defineTool({
2771
2925
  name: "restore_from_file",
2772
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.",
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()
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()
2787
2941
  }),
2788
2942
  annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
2789
2943
  async handler(input) {