@respira/wordpress-mcp-server 7.6.1 → 8.0.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.
Files changed (34) hide show
  1. package/README.md +59 -31
  2. package/TOOL_CATALOG.md +288 -0
  3. package/dist/__tests__/pagespeed-error-classification.test.d.ts +2 -0
  4. package/dist/__tests__/pagespeed-error-classification.test.d.ts.map +1 -0
  5. package/dist/__tests__/pagespeed-error-classification.test.js +52 -0
  6. package/dist/__tests__/pagespeed-error-classification.test.js.map +1 -0
  7. package/dist/__tests__/rest-route-fallback.test.js +0 -64
  8. package/dist/__tests__/rest-route-fallback.test.js.map +1 -1
  9. package/dist/__tests__/site-token-401-error-classification.test.d.ts +0 -19
  10. package/dist/__tests__/site-token-401-error-classification.test.d.ts.map +1 -1
  11. package/dist/__tests__/site-token-401-error-classification.test.js +20 -60
  12. package/dist/__tests__/site-token-401-error-classification.test.js.map +1 -1
  13. package/dist/__tests__/tools-list-protocol-smoke.test.d.ts +2 -0
  14. package/dist/__tests__/tools-list-protocol-smoke.test.d.ts.map +1 -0
  15. package/dist/__tests__/tools-list-protocol-smoke.test.js +78 -0
  16. package/dist/__tests__/tools-list-protocol-smoke.test.js.map +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +20 -7
  19. package/dist/config.js.map +1 -1
  20. package/dist/elementor-tools.js +2 -2
  21. package/dist/elementor-tools.js.map +1 -1
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js +758 -19
  24. package/dist/server.js.map +1 -1
  25. package/dist/usage-emitter.d.ts +7 -0
  26. package/dist/usage-emitter.d.ts.map +1 -1
  27. package/dist/usage-emitter.js +41 -1
  28. package/dist/usage-emitter.js.map +1 -1
  29. package/dist/wordpress-client.d.ts +59 -42
  30. package/dist/wordpress-client.d.ts.map +1 -1
  31. package/dist/wordpress-client.js +149 -135
  32. package/dist/wordpress-client.js.map +1 -1
  33. package/package.json +5 -8
  34. package/tool-capabilities.json +4122 -0
@@ -13,6 +13,63 @@ import { randomUUID } from 'node:crypto';
13
13
  import { getRespiraAgentHeaders } from './agent-signature.js';
14
14
  import { CONFIG_FILE, isTlsInsecureEnv } from './config.js';
15
15
  import { makeHttpsAgent } from './system-ca.js';
16
+ /** `C:\...` or `C:/...`. Windows drive-letter absolute path. */
17
+ const WINDOWS_DRIVE_PATH_RE = /^[a-zA-Z]:[\\/]/;
18
+ /**
19
+ * Does this string name a file on the caller's own disk, as opposed to a URL,
20
+ * a data: URI, or a bare base64 blob?
21
+ *
22
+ * Matches POSIX absolute (`/home/...`), relative (`./`, `../`), home-relative
23
+ * (`~/...`), Windows drive-letter (`C:\...`, `C:/...`), and `file://` URIs
24
+ * (`file:///C:/Users/...` on Windows, `file:///home/...` on Unix). The caller
25
+ * converts `file://` to a real path with `resolveLocalFilePath()` below.
26
+ *
27
+ * The Windows and `file://` arms are why this is a named helper rather than an
28
+ * inline condition. Without them those inputs fall through to the base64
29
+ * branch, where they do not error: they decode to garbage and upload as a
30
+ * corrupt file. Silent, not loud.
31
+ *
32
+ * Not handled: Windows UNC paths (`\\server\share\file.jpg`). They do not
33
+ * reproduce the same way, since backslashes are not valid base64 characters,
34
+ * but they are not read as a local file either. Out of scope until a UNC
35
+ * report comes in.
36
+ */
37
+ export function isLocalFilePath(file) {
38
+ return (file.startsWith('/') ||
39
+ file.startsWith('./') ||
40
+ file.startsWith('../') ||
41
+ file.startsWith('~') ||
42
+ WINDOWS_DRIVE_PATH_RE.test(file) ||
43
+ file.startsWith('file://'));
44
+ }
45
+ /**
46
+ * Turns an `isLocalFilePath()`-matched string into a real filesystem path.
47
+ * Handles `file://` URIs (unwrapping via `fileURLToPath`, forcing Windows
48
+ * semantics when the URI's own path looks like a drive letter, so the
49
+ * conversion is correct even when the MCP server process itself is running on
50
+ * a non-Windows host), `~` home-dir expansion, and relative-path resolution
51
+ * against cwd.
52
+ *
53
+ * Windows drive-letter absolute paths are returned as-is rather than run
54
+ * through `resolve()`: they are already absolute, and POSIX `resolve()` does
55
+ * not recognise a drive letter as an absolute-path marker, so calling it on a
56
+ * non-Windows host would wrongly prefix the string with cwd. On the Windows
57
+ * machine where such a path is real, `fs` accepts it verbatim.
58
+ */
59
+ export function resolveLocalFilePath(file) {
60
+ let input = file;
61
+ if (file.startsWith('file://')) {
62
+ const looksWindows = WINDOWS_DRIVE_PATH_RE.test(new URL(file).pathname.replace(/^\//, ''));
63
+ input = looksWindows ? fileURLToPath(file, { windows: true }) : fileURLToPath(file);
64
+ }
65
+ if (input.startsWith('~')) {
66
+ return input.replace(/^~/, process.env.HOME || '');
67
+ }
68
+ if (WINDOWS_DRIVE_PATH_RE.test(input)) {
69
+ return input;
70
+ }
71
+ return resolve(input);
72
+ }
16
73
  const MCP_CLIENT_VERSION = (() => {
17
74
  try {
18
75
  const currentDir = dirname(fileURLToPath(import.meta.url));
@@ -60,67 +117,6 @@ export function isReplaySafeRetry(args) {
60
117
  }
61
118
  return safe && !!args.connectionCode && RETRYABLE_CONNECTION_CODES.has(args.connectionCode);
62
119
  }
63
- // Windows drive-letter absolute path, e.g. `C:\Users\bob\photo.jpg` or `C:/Users/bob/photo.jpg`.
64
- const WINDOWS_DRIVE_PATH_RE = /^[A-Za-z]:[\\/]/;
65
- /**
66
- * True when `file` (the `file` argument to `uploadMedia`) looks like a local
67
- * filesystem path rather than a base64 blob or a `data:`/`http(s):` URL.
68
- *
69
- * Historically this only matched POSIX-style paths (`/`, `./`, `../`, `~`),
70
- * so a Windows absolute path like `C:/Users/bob/photo.jpg` — which starts
71
- * with none of those — fell through to the base64 branch. `Buffer.from(file,
72
- * 'base64')` doesn't throw on non-base64 input; it silently decodes whatever
73
- * valid base64 characters happen to appear (mostly none, for a path like
74
- * that) and produces a few garbage bytes instead of an error, so the upload
75
- * "succeeds" with a corrupt file. See ticket 5a19d816.
76
- *
77
- * Also matches `file://` URIs (`file:///C:/Users/...` on Windows,
78
- * `file:///home/...` on Unix); the caller converts these to a real path with
79
- * `fileURLToPath` before reuse of the existing local-file-read logic below.
80
- *
81
- * Not handled: Windows UNC paths (`\\server\share\file.jpg`). They don't
82
- * reproduce this bug — backslashes aren't valid base64 characters, so they'd
83
- * still fall into the base64 branch but wouldn't "succeed" quietly the same
84
- * way — but they also aren't read as a local file today. Out of scope for
85
- * this fix; revisit if a UNC-specific report comes in.
86
- */
87
- export function isLocalFilePath(file) {
88
- return (file.startsWith('/') ||
89
- file.startsWith('./') ||
90
- file.startsWith('../') ||
91
- file.startsWith('~') ||
92
- WINDOWS_DRIVE_PATH_RE.test(file) ||
93
- file.startsWith('file://'));
94
- }
95
- /**
96
- * Turns an `isLocalFilePath()`-matched string into a real filesystem path.
97
- * Handles `file://` URIs (unwrapping via `fileURLToPath`, forcing Windows
98
- * semantics when the URI's own path looks like a drive letter so this
99
- * converts correctly even when the MCP server process itself is running on
100
- * a non-Windows host), `~` home-dir expansion, and relative-path resolution
101
- * against cwd.
102
- *
103
- * Windows drive-letter absolute paths (`C:\...`, `C:/...`) are returned
104
- * as-is rather than run through `resolve()`: they're already absolute, and
105
- * POSIX `resolve()` does not recognize a drive letter as an absolute-path
106
- * marker, so calling it on a non-Windows host (e.g. this test suite) would
107
- * wrongly prefix the string with `cwd`. On the Windows machine where such a
108
- * path is actually real, `fs` calls accept it verbatim.
109
- */
110
- export function resolveLocalFilePath(file) {
111
- let input = file;
112
- if (file.startsWith('file://')) {
113
- const looksWindows = WINDOWS_DRIVE_PATH_RE.test(new URL(file).pathname.replace(/^\//, ''));
114
- input = looksWindows ? fileURLToPath(file, { windows: true }) : fileURLToPath(file);
115
- }
116
- if (input.startsWith('~')) {
117
- return input.replace(/^~/, process.env.HOME || '');
118
- }
119
- if (WINDOWS_DRIVE_PATH_RE.test(input)) {
120
- return input;
121
- }
122
- return resolve(input);
123
- }
124
120
  export class WordPressClient {
125
121
  client;
126
122
  rootClient;
@@ -872,10 +868,10 @@ export class WordPressClient {
872
868
  : 'The site returned an HTML error page instead of a WordPress REST API response. ' +
873
869
  'A firewall, CDN, or maintenance mode may be blocking API access. ' +
874
870
  'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.';
875
- return new Error(`Site not reachable: ${this.siteConfig.url} returned a ${status} response from the web server (not WordPress).\n\n${hint}`);
871
+ return this.codedError(`Site not reachable: ${this.siteConfig.url} returned a ${status} response from the web server (not WordPress).\n\n${hint}`, 'http_401_non_wordpress');
876
872
  }
877
873
  const reason = data?.message || data?.code || 'Invalid API key';
878
- const authError = new Error(`Authentication failed: ${reason}`);
874
+ const authError = this.codedError(`Authentication failed: ${reason}`, typeof data?.code === 'string' && data.code ? data.code : 'http_401', data);
879
875
  // Same rationale as in formatErrorWithInstructions / the generic
880
876
  // catch-all below: stamp the WP_Error code (respira_invalid_site_token,
881
877
  // respira_token_stale, license_inactive, ...) onto error.name so
@@ -887,56 +883,47 @@ export class WordPressClient {
887
883
  // its machine-readable code the moment it crossed into the npm
888
884
  // server — telemetry only recorded the tool name, not why it failed.
889
885
  // Ticket fc4fa49e.
890
- if (typeof data?.code === 'string' && data.code) {
891
- authError.name = data.code.slice(0, 80);
892
- }
893
886
  return authError;
894
887
  }
895
888
  else if (status === 429) {
896
- return new Error(`Rate limit exceeded. Please wait before making more requests.`);
889
+ return this.codedError('Rate limit exceeded. Please wait before making more requests.', typeof data?.code === 'string' && data.code ? data.code : 'http_429', data);
897
890
  }
898
891
  else if (status === 403) {
899
892
  if (isNonWordPressResponse) {
900
- return new Error(`Site blocked: ${this.siteConfig.url} returned a 403 Forbidden from the web server (not WordPress).\n\n` +
893
+ return this.codedError(`Site blocked: ${this.siteConfig.url} returned a 403 Forbidden from the web server (not WordPress).\n\n` +
901
894
  'A firewall, security plugin (e.g. Wordfence, Sucuri), or CDN rule is blocking REST API requests. ' +
902
- 'Whitelist the /wp-json/respira/* path or the MCP server\'s User-Agent in your security settings.');
895
+ 'Whitelist the /wp-json/respira/* path or the MCP server\'s User-Agent in your security settings.', 'http_403_non_wordpress');
903
896
  }
904
897
  // Format error with instructions if available
905
898
  return this.formatErrorWithInstructions(data);
906
899
  }
907
900
  else if (status >= 500) {
908
901
  if (isNonWordPressResponse) {
909
- return new Error(`Server error: ${this.siteConfig.url} returned a ${status} error from the web server (not WordPress).\n\n` +
902
+ return this.codedError(`Server error: ${this.siteConfig.url} returned a ${status} error from the web server (not WordPress).\n\n` +
910
903
  'The site may be down, misconfigured, or behind a proxy that is failing. ' +
911
- 'Check that the site loads normally in a browser first.');
904
+ 'Check that the site loads normally in a browser first.', `http_${status}_non_wordpress`);
912
905
  }
913
906
  const serverDetails = this.extractServerErrorDetails(data);
914
907
  const message = data?.message || 'Internal server error';
915
908
  const detailsSuffix = serverDetails ? `\n\nDebug details: ${serverDetails}` : '';
916
- return new Error(`WordPress server error (${status}): ${message}${detailsSuffix}`);
909
+ return this.codedError(`WordPress server error (${status}): ${message}${detailsSuffix}`, typeof data?.code === 'string' && data.code ? data.code : `http_${status}`, data);
917
910
  }
918
911
  // Check if error has instructions data
919
912
  if (data?.data && (data.data.instructions || data.data.links)) {
920
913
  return this.formatErrorWithInstructions(data);
921
914
  }
922
915
  if (isNonWordPressResponse) {
923
- return new Error(`Unexpected response: ${this.siteConfig.url} returned HTTP ${status} with an HTML page instead of a WordPress REST API response.\n\n` +
916
+ return this.codedError(`Unexpected response: ${this.siteConfig.url} returned HTTP ${status} with an HTML page instead of a WordPress REST API response.\n\n` +
924
917
  'The Respira plugin may not be active, the REST API may be disabled, or a proxy/firewall is intercepting requests. ' +
925
- 'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.');
918
+ 'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.', `http_${status}_non_wordpress`);
926
919
  }
927
- const apiError = new Error(`API error (${status}): ${data?.message || error.message}`);
920
+ const apiError = this.codedError(`API error (${status}): ${data?.message || error.message}`, typeof data?.code === 'string' && data.code ? data.code : `http_${status}`, data);
928
921
  // Promote the WP_Error code (e.g. 'respira_no_builder',
929
922
  // 'respira_element_not_found') to error.name so the MCP usage emitter
930
923
  // records something diagnostic instead of the literal 'Error'. Without
931
924
  // this, every WP-side failure collapses to error_code='Error' in
932
925
  // mcp_tool_events and the admin/mcp-quality dashboard cannot tell
933
926
  // 'wrong target' from 'no builder detected' from 'permission denied'.
934
- if (typeof data?.code === 'string' && data.code) {
935
- apiError.name = data.code.slice(0, 80);
936
- }
937
- else {
938
- apiError.name = `http_${status}`;
939
- }
940
927
  // v6.21.2: preserve the structured WP_Error envelope so the agent sees
941
928
  // the rich fields (missing_args, required_path_keys, hint, instructions,
942
929
  // registered_abilities_sample, destructive_tools, available_types, ...).
@@ -948,26 +935,6 @@ export class WordPressClient {
948
935
  // Attach the WP wire envelope as a non-enumerable property on the
949
936
  // Error so server.ts can surface it in the tool result without
950
937
  // accidentally duplicating fields the message already carries.
951
- const errorEnvelope = {};
952
- if (typeof data?.code === 'string')
953
- errorEnvelope.code = data.code;
954
- if (data?.data && typeof data.data === 'object') {
955
- // Spread the structured fields. `status` would duplicate the HTTP code,
956
- // skip it.
957
- for (const [k, v] of Object.entries(data.data)) {
958
- if (k === 'status')
959
- continue;
960
- errorEnvelope[k] = v;
961
- }
962
- }
963
- if (Object.keys(errorEnvelope).length > 0) {
964
- Object.defineProperty(apiError, 'respiraErrorEnvelope', {
965
- value: errorEnvelope,
966
- enumerable: false,
967
- configurable: false,
968
- writable: false,
969
- });
970
- }
971
938
  // v6.19.2: when WordPress reports rest_no_route, enrich the message
972
939
  // with the attempted HTTP method and request path. Without this,
973
940
  // the /mcp-quality dashboard sees the generic WP string "No route
@@ -1061,7 +1028,39 @@ export class WordPressClient {
1061
1028
  netError.name = 'respira_write_outcome_unknown';
1062
1029
  return netError;
1063
1030
  }
1064
- return new Error(`Unknown error: ${error.message}`);
1031
+ return this.codedError(`Unknown error: ${error.message}`, 'unknown_error');
1032
+ }
1033
+ /**
1034
+ * Create a telemetry-safe Error while preserving the structured WP_Error
1035
+ * envelope. Every HTTP/proxy/upstream branch must use this helper so useful
1036
+ * PageSpeed and hosting failures never collapse into `<tool>_unclassified`.
1037
+ */
1038
+ codedError(message, code, data) {
1039
+ const coded = new Error(message);
1040
+ const safeCode = String(code || 'unknown_error')
1041
+ .toLowerCase()
1042
+ .replace(/[^a-z0-9_:-]/g, '_')
1043
+ .slice(0, 80);
1044
+ coded.name = safeCode || 'unknown_error';
1045
+ const errorEnvelope = {};
1046
+ if (typeof data?.code === 'string' && data.code) {
1047
+ errorEnvelope.code = data.code;
1048
+ }
1049
+ if (data?.data && typeof data.data === 'object') {
1050
+ for (const [key, value] of Object.entries(data.data)) {
1051
+ if (key !== 'status')
1052
+ errorEnvelope[key] = value;
1053
+ }
1054
+ }
1055
+ if (Object.keys(errorEnvelope).length > 0) {
1056
+ Object.defineProperty(coded, 'respiraErrorEnvelope', {
1057
+ value: errorEnvelope,
1058
+ enumerable: false,
1059
+ configurable: false,
1060
+ writable: false,
1061
+ });
1062
+ }
1063
+ return coded;
1065
1064
  }
1066
1065
  /**
1067
1066
  * Extract concise server-side diagnostics from a failed WP response.
@@ -1279,6 +1278,10 @@ export class WordPressClient {
1279
1278
  const response = await this.client.get('/abilities/gap-report');
1280
1279
  return response.data;
1281
1280
  }
1281
+ async searchAbilities(params) {
1282
+ const response = await this.client.get('/abilities/search', { params: params || {} });
1283
+ return response.data;
1284
+ }
1282
1285
  /**
1283
1286
  * Invoke an inhaled ability via the Respira safety proxy.
1284
1287
  *
@@ -1550,8 +1553,22 @@ export class WordPressClient {
1550
1553
  /**
1551
1554
  * Replace the content of an existing Divi Theme Builder layout.
1552
1555
  */
1556
+ /**
1557
+ * `confirm_live_edit` is required by the plugin on the first call: a global
1558
+ * layout rewrites every page that uses it and goes live with no staging step,
1559
+ * so the first attempt is refused with `respira_tb_live_edit_confirmation_required`
1560
+ * and the caller has to come back having acknowledged the blast radius.
1561
+ *
1562
+ * Threaded through only when the caller actually set it. Sending `false`
1563
+ * explicitly is not the same as not answering, and the plugin reads both.
1564
+ */
1553
1565
  async updateThemeBuilderTemplate(args) {
1554
- const response = await this.client.post(`/divi/theme-builder/template/${args.layout_id}`, { structure: args.structure });
1566
+ const response = await this.client.post(`/divi/theme-builder/template/${args.layout_id}`, {
1567
+ structure: args.structure,
1568
+ ...(args.confirm_live_edit === undefined
1569
+ ? {}
1570
+ : { confirm_live_edit: args.confirm_live_edit }),
1571
+ });
1555
1572
  return response.data;
1556
1573
  }
1557
1574
  /**
@@ -2690,32 +2707,11 @@ export class WordPressClient {
2690
2707
  }
2691
2708
  if (restRouteFallbackWorked) {
2692
2709
  this.lastRestRouteFallbackWorked = true;
2693
- // Ticket 73b64e82: these probes run over a bare `axios.request()` (see
2694
- // the `probe()` helper above), not through `this.client`/`this.rootClient`,
2695
- // so they never pass through the response interceptor that normally
2696
- // trips the sticky `useRestRouteFallback` flag on a real tool call
2697
- // (see the success interceptor installed in the constructor, a few
2698
- // hundred lines up). Without this, diagnoseConnection would *report*
2699
- // `rest_route_fallback_worked: true` while every subsequent normal tool
2700
- // call (get_builder_info, list_pages, ...) kept retrying the doomed
2701
- // pretty path from scratch on this client instance — forcing the user
2702
- // to hand-set `forceRestRoute: true` even though the diagnostic had
2703
- // already proven the fallback works. Flip the same flag a real
2704
- // tool-call fallback would, so the diagnostic's finding actually
2705
- // sticks for the rest of the session instead of just being reported.
2706
- if (!this.useRestRouteFallback) {
2707
- this.useRestRouteFallback = true;
2708
- if (!this.restRouteFallbackWarned) {
2709
- this.restRouteFallbackWarned = true;
2710
- process.stderr.write(`[respira-mcp] Site ${this.siteConfig.name} has REST rewrite shadowing; ` +
2711
- `falling back to ?rest_route= for this session (confirmed by wordpress_diagnose_connection).\n`);
2712
- }
2713
- }
2714
2710
  recommendations.push('Pretty `/wp-json/respira/...` path returned HTML but `?rest_route=/respira/...` returned JSON. ' +
2715
2711
  'A plugin or theme rewrite rule is shadowing `/wp-json/[anything]` and triggering ' +
2716
2712
  'WordPress `redirect_canonical()` (look for `x-redirect-by: WordPress` on the 301). ' +
2717
- 'This session has switched to `?rest_route=` automatically — set ' +
2718
- '`forceRestRoute: true` in the site config to make that permanent and skip this probe on future connects.');
2713
+ 'The MCP server will auto-fall-back to `?rest_route=` for this session — set ' +
2714
+ '`forceRestRoute: true` in the site config to skip the pretty-permalink probe entirely.');
2719
2715
  }
2720
2716
  return {
2721
2717
  success: true,
@@ -2847,8 +2843,12 @@ export class WordPressClient {
2847
2843
  * Activate a plugin (EXPERIMENTAL). Approval-gated; echo approval_token
2848
2844
  * back on the second call with the same slug to complete.
2849
2845
  */
2850
- async activatePlugin(slug, approvalToken) {
2851
- const body = approvalToken ? { approval_token: approvalToken } : undefined;
2846
+ async activatePlugin(slug, approvalToken, forceWithoutProbe) {
2847
+ const body = {};
2848
+ if (approvalToken)
2849
+ body.approval_token = approvalToken;
2850
+ if (forceWithoutProbe)
2851
+ body.force_without_probe = true;
2852
2852
  const response = await this.client.post(`/plugins/${slug}/activate`, body);
2853
2853
  return response.data;
2854
2854
  }
@@ -3106,13 +3106,14 @@ export class WordPressClient {
3106
3106
  return response.data;
3107
3107
  }
3108
3108
  /**
3109
- * Delete a media attachment. Approval-gated since v7.1.0-beta.1: the
3110
- * first call comes back with `code: respira_approval_required` and a
3111
- * fresh `approval_token`; the caller re-sends the same id with that
3112
- * token to confirm. Ticket 927aa7ed — this method used to only forward
3113
- * `id`, silently dropping `force` and `approvalToken`, so the WP side
3114
- * never saw a token on the "confirm" call and looped forever minting
3115
- * new ones. Mirrors `deletePage`'s params shape (see comment above it).
3109
+ * `force` skips the trash and deletes permanently. `approvalToken` carries a
3110
+ * governance approval back to the plugin on the retry after a first call was
3111
+ * refused.
3112
+ *
3113
+ * Both are optional and, as of 8.0, no caller in server.ts passes either:
3114
+ * the delete_media tool schema does not expose them. The parameters are kept
3115
+ * because they are the shipped signature and the plugin route accepts them.
3116
+ * Wiring the tool schema is a separate change, not a release-eve one.
3116
3117
  */
3117
3118
  async deleteMedia(id, force, approvalToken) {
3118
3119
  const params = {};
@@ -3348,6 +3349,15 @@ export class WordPressClient {
3348
3349
  const response = await this.client.post('/woocommerce/storefront/update-checkout-layout', data);
3349
3350
  return response.data;
3350
3351
  }
3352
+ // WooCommerce Add-on 8.0: product-card field addressing
3353
+ async woocommerceAnalyzeStorefrontCard(params) {
3354
+ const response = await this.client.get('/woocommerce/storefront/analyze-card', { params: params || {} });
3355
+ return response.data;
3356
+ }
3357
+ async woocommerceUpdateStorefrontCardField(data) {
3358
+ const response = await this.client.post('/woocommerce/storefront/update-card-field', data);
3359
+ return response.data;
3360
+ }
3351
3361
  // WooCommerce Add-on v3.0 — variations and attributes
3352
3362
  async woocommerceSetProductAttributes(id, data) {
3353
3363
  const response = await this.client.post(`/woocommerce/products/${id}/attributes`, data);
@@ -3524,6 +3534,10 @@ export class WordPressClient {
3524
3534
  const response = await this.client.get('/woocommerce/feeds/status');
3525
3535
  return response.data;
3526
3536
  }
3537
+ async woocommerceRepairFeedScheduler(data) {
3538
+ const response = await this.client.post('/woocommerce/feeds/repair', data || {});
3539
+ return response.data;
3540
+ }
3527
3541
  async woocommerceValidateFeed(params) {
3528
3542
  const response = await this.client.get('/woocommerce/feeds/validate', { params: params || {} });
3529
3543
  return response.data;