seo 0.13.0 → 0.13.2

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/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-BfNQ2FiC.js";
2
+ import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-Dmtr8qrP.js";
3
3
  import { createHash, randomUUID, sign } from "node:crypto";
4
4
  import { ZodOptional, z } from "zod";
5
5
  import { arch, homedir, platform, tmpdir, totalmem } from "node:os";
@@ -6811,7 +6811,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6811
6811
  };
6812
6812
  }));
6813
6813
  //#endregion
6814
- //#region node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js
6814
+ //#region node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/utils.js
6815
6815
  var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6816
6816
  /** @type {(value: string) => boolean} */
6817
6817
  const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
@@ -7105,6 +7105,38 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7105
7105
  return output.join("");
7106
7106
  }
7107
7107
  /**
7108
+ * Removes dot segments from the path of a relative reference (a path that does
7109
+ * not start with "/") without changing what it resolves to against any base.
7110
+ * Unlike `removeDotSegments`, leading ".." segments that cannot be collapsed
7111
+ * are kept, the result never becomes root-relative, and a result that would be
7112
+ * empty or start with an empty segment is prefixed with "." (an empty path
7113
+ * means "same document" and a leading "//" would introduce an authority).
7114
+ *
7115
+ * @param {string} path
7116
+ * @returns {string}
7117
+ */
7118
+ function removeDotSegmentsRelative(path) {
7119
+ if (path === "" || path[0] === "/") return removeDotSegments(path);
7120
+ const segments = path.split("/");
7121
+ const last = segments.length - 1;
7122
+ /** @type {string[]} */
7123
+ const output = [];
7124
+ let trailingDirectory = false;
7125
+ for (let i = 0; i <= last; i++) {
7126
+ const segment = segments[i];
7127
+ if (segment === "." || segment === "..") {
7128
+ if (segment === "..") {
7129
+ if (output.length !== 0 && output[output.length - 1] !== "..") output.pop();
7130
+ else output.push("..");
7131
+ }
7132
+ trailingDirectory = i === last;
7133
+ } else output.push(segment);
7134
+ }
7135
+ if (trailingDirectory && output[output.length - 1] !== "..") output.push("");
7136
+ if (output.length === 0 || output[0] === "" || output[0].includes(":")) output.unshift(".");
7137
+ return output.join("/");
7138
+ }
7139
+ /**
7108
7140
  * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
7109
7141
  * After the URI regex parses, these characters cannot be literal in the host
7110
7142
  * field, so any that appear after decoding came from percent-encoding and
@@ -7408,6 +7440,7 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7408
7440
  encodeFragment,
7409
7441
  escapePreservingEscapes,
7410
7442
  removeDotSegments,
7443
+ removeDotSegmentsRelative,
7411
7444
  isIPv4,
7412
7445
  isUUID,
7413
7446
  normalizeIPv6,
@@ -7415,7 +7448,7 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7415
7448
  };
7416
7449
  }));
7417
7450
  //#endregion
7418
- //#region node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js
7451
+ //#region node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/schemes.js
7419
7452
  var require_schemes = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7420
7453
  const { isUUID } = require_utils();
7421
7454
  const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
@@ -7599,20 +7632,26 @@ var require_schemes = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7599
7632
  };
7600
7633
  }));
7601
7634
  //#endregion
7602
- //#region node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/index.js
7635
+ //#region node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/index.js
7603
7636
  var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7604
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
7637
+ const { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
7605
7638
  const { SCHEMES, getSchemeHandler } = require_schemes();
7606
7639
  const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
7607
7640
  const MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
7641
+ const MALFORMED_HOST_ERROR = "URI host is malformed.";
7642
+ const FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
7608
7643
  /**
7644
+ * RFC 3986 section 3.1 does not allow percent-encoding in a scheme, so the
7645
+ * scheme is validated as-is and never decoded. Decoding would let inert input
7646
+ * such as `%6Aavascript:` become a live `javascript:` scheme.
7647
+ *
7609
7648
  * @param {string} scheme
7610
7649
  * @returns {string}
7611
7650
  */
7612
- function decodeValidScheme(scheme) {
7613
- const decodedScheme = unescape(String(scheme));
7614
- if (!VALID_SCHEME.test(decodedScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
7615
- return decodedScheme;
7651
+ function validateScheme(scheme) {
7652
+ const stringScheme = String(scheme);
7653
+ if (!VALID_SCHEME.test(stringScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
7654
+ return stringScheme;
7616
7655
  }
7617
7656
  /**
7618
7657
  * @template {import('./types/index').URIComponent|string} T
@@ -7640,6 +7679,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7640
7679
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
7641
7680
  const resolvedHost = resolved.host;
7642
7681
  const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
7682
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) throw new Error(MALFORMED_HOST_ERROR);
7643
7683
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
7644
7684
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !/\P{ASCII}/u.test(resolvedHost);
7645
7685
  if (resolved.error && !encodedASCIIHost) throw new Error(resolved.error);
@@ -7683,10 +7723,11 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7683
7723
  } else {
7684
7724
  if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
7685
7725
  else {
7686
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
7726
+ const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
7727
+ if (baseHasAuthority && !base.path) target.path = "/" + relative.path;
7687
7728
  else if (!base.path) target.path = relative.path;
7688
7729
  else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
7689
- target.path = removeDotSegments(target.path);
7730
+ target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
7690
7731
  }
7691
7732
  target.query = relative.query;
7692
7733
  }
@@ -7734,17 +7775,18 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7734
7775
  };
7735
7776
  const options = Object.assign({}, opts);
7736
7777
  const uriTokens = [];
7737
- if (component.scheme) component.scheme = decodeValidScheme(component.scheme);
7778
+ if (component.scheme) component.scheme = validateScheme(component.scheme);
7738
7779
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
7739
7780
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
7740
7781
  const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
7741
- const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
7782
+ const schemelessReference = (component.scheme === void 0 || options.reference === "suffix") && !hasAuthority;
7783
+ const pathNoScheme = !options.skipEscape && schemelessReference;
7742
7784
  if (component.path !== void 0) {
7743
7785
  if (!options.skipEscape) component.path = serializePathEncoding(component.path, pathNoScheme);
7744
7786
  else component.path = normalizePercentEncoding(component.path);
7745
7787
  }
7746
7788
  if (options.reference !== "suffix" && component.scheme) {
7747
- component.scheme = decodeValidScheme(component.scheme);
7789
+ component.scheme = validateScheme(component.scheme);
7748
7790
  uriTokens.push(component.scheme, ":");
7749
7791
  }
7750
7792
  const authority = recomposeAuthority(component);
@@ -7755,8 +7797,8 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7755
7797
  }
7756
7798
  if (component.path !== void 0) {
7757
7799
  let s = component.path;
7758
- if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = removeDotSegments(s);
7759
- if (pathNoScheme) s = serializePathEncoding(s, true);
7800
+ if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
7801
+ if (schemelessReference) s = serializePathEncoding(s, true);
7760
7802
  if (authority === void 0 && s[0] === "/" && s[1] === "/") s = "/%2F" + s.slice(2);
7761
7803
  uriTokens.push(s);
7762
7804
  }
@@ -7820,15 +7862,50 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7820
7862
  * @returns {boolean} whether host conversion failed
7821
7863
  */
7822
7864
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
7823
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) try {
7824
- parsed.host = new URL("http://" + parsed.host).hostname;
7825
- } catch (e) {
7826
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
7865
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
7866
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
7827
7867
  return true;
7828
7868
  }
7869
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
7870
+ let url;
7871
+ try {
7872
+ url = new URL("http://" + parsed.host);
7873
+ } catch (e) {
7874
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
7875
+ return true;
7876
+ }
7877
+ if (!isHostOnlyURL(url)) {
7878
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
7879
+ return true;
7880
+ }
7881
+ parsed.host = url.hostname;
7882
+ }
7829
7883
  return false;
7830
7884
  }
7831
7885
  /**
7886
+ * Whether a domain host contains an ASCII character that RFC 3986 does not
7887
+ * allow in a reg-name (unreserved, sub-delims, or pct-encoded). Such a host is
7888
+ * rejected instead of being handed to the WHATWG URL parser, which would treat
7889
+ * "@" as a userinfo delimiter (silently dropping the data before it) and strip
7890
+ * TAB, LF, and CR. Non-ASCII characters remain allowed for IDNs.
7891
+ *
7892
+ * @param {import('./types/index').URIComponent} parsed
7893
+ * @param {import('./types/index').Options} options
7894
+ * @param {{ domainHost?: boolean }|undefined} schemeHandler
7895
+ * @param {boolean} isIP
7896
+ * @returns {boolean}
7897
+ */
7898
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
7899
+ return Boolean(parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host));
7900
+ }
7901
+ /**
7902
+ * @param {URL} url
7903
+ * @returns {boolean}
7904
+ */
7905
+ function isHostOnlyURL(url) {
7906
+ return url.username === "" && url.password === "" && url.port === "" && url.pathname === "/" && url.search === "" && url.hash === "" && url.hostname !== "";
7907
+ }
7908
+ /**
7832
7909
  * @param {string} uri
7833
7910
  * @param {import('./types/index').Options} [opts]
7834
7911
  * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
@@ -7885,8 +7962,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7885
7962
  parsed.query = matches[7];
7886
7963
  parsed.fragment = matches[8];
7887
7964
  if (parsed.scheme !== void 0) {
7888
- const decodedScheme = unescape(parsed.scheme);
7889
- if (VALID_SCHEME.test(decodedScheme)) parsed.scheme = decodedScheme.toLowerCase();
7965
+ if (VALID_SCHEME.test(parsed.scheme)) parsed.scheme = parsed.scheme.toLowerCase();
7890
7966
  else {
7891
7967
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
7892
7968
  malformedScheme = true;
@@ -7909,7 +7985,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7909
7985
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
7910
7986
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
7911
7987
  if (malformedIPLiteral) {
7912
- parsed.error = parsed.error || "URI host is malformed.";
7988
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
7913
7989
  malformedAuthorityOrPort = true;
7914
7990
  }
7915
7991
  } else isIP = true;
@@ -7924,7 +8000,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7924
8000
  if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
7925
8001
  let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
7926
8002
  if (!isIP) host = normalizePercentEncoding(host.toLowerCase());
7927
- parsed.host = reescapeHostDelimiters(host, isIP);
8003
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
7928
8004
  }
7929
8005
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
7930
8006
  if (parsed.path) parsed.path = normalizePathEncoding(parsed.path);
@@ -13560,6 +13636,44 @@ function agentActionsView(report, options = {}) {
13560
13636
  };
13561
13637
  }
13562
13638
  //#endregion
13639
+ //#region packages/core/dist/providers/errors.js
13640
+ const PROVIDER_ERROR_CODES = {
13641
+ configuration: "AUTH_CONFIG_REQUIRED",
13642
+ authentication: "AUTH_CONFIG_REQUIRED",
13643
+ "rate-limit": "RATE_LIMITED",
13644
+ timeout: "PROVIDER_UNAVAILABLE",
13645
+ "response-too-large": "PROVIDER_UNAVAILABLE",
13646
+ "invalid-response": "PROVIDER_UNAVAILABLE",
13647
+ "remote-error": "PROVIDER_UNAVAILABLE"
13648
+ };
13649
+ var ProviderError = class extends Error {
13650
+ provider;
13651
+ operation;
13652
+ code;
13653
+ status;
13654
+ retryable;
13655
+ constructor(input) {
13656
+ super(input.message, { cause: input.cause });
13657
+ this.name = "ProviderError";
13658
+ this.provider = input.provider;
13659
+ this.operation = input.operation;
13660
+ this.code = input.code;
13661
+ this.status = input.status ?? null;
13662
+ this.retryable = input.retryable ?? false;
13663
+ }
13664
+ toJSON() {
13665
+ return {
13666
+ name: this.name,
13667
+ provider: this.provider,
13668
+ operation: this.operation,
13669
+ code: this.code,
13670
+ message: this.message,
13671
+ status: this.status,
13672
+ retryable: this.retryable
13673
+ };
13674
+ }
13675
+ };
13676
+ //#endregion
13563
13677
  //#region packages/core/dist/errors.js
13564
13678
  const errorMetadata = {
13565
13679
  ACCESS_DENIED: {
@@ -13625,6 +13739,7 @@ function errorMessage$1(error) {
13625
13739
  function toSeoError(error) {
13626
13740
  if (error instanceof SeoError) return error;
13627
13741
  const message = errorMessage$1(error);
13742
+ if (error instanceof ProviderError) return new SeoError(PROVIDER_ERROR_CODES[error.code], message);
13628
13743
  if (/not logged in/i.test(message)) return new SeoError("AUTH_REQUIRED", message);
13629
13744
  if (/oauth client config.*missing|shared seo google app/i.test(message)) return new SeoError("AUTH_CONFIG_REQUIRED", message);
13630
13745
  if (/invalid_grant|refresh token.*no longer valid/i.test(message)) return new SeoError("AUTH_EXPIRED", message);
@@ -14408,15 +14523,18 @@ function factsOnly(value) {
14408
14523
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !ADVICE_KEYS.test(key)).map(([key, child]) => [key, factsOnly(child)]));
14409
14524
  return value;
14410
14525
  }
14526
+ function realIsoDate(value) {
14527
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) && (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().startsWith(value);
14528
+ }
14411
14529
  function dateWindow(input, fallbackDays) {
14412
- if (input.startDate || input.endDate) {
14413
- if (!input.startDate || !input.endDate || input.startDate > input.endDate) throw new CommandError("INVALID_INPUT", "Send a start date and an end date together, in order.");
14414
- return {
14415
- startDate: input.startDate,
14416
- endDate: input.endDate
14417
- };
14418
- }
14419
- return finalGscDateRange(fallbackDays);
14530
+ if (!input.startDate && !input.endDate) return finalGscDateRange(fallbackDays);
14531
+ const endDate = input.endDate ?? latestFinalGscDate();
14532
+ const startDate = input.startDate ?? shiftIsoDate(endDate, 1 - fallbackDays);
14533
+ if (!realIsoDate(startDate) || !realIsoDate(endDate) || startDate > endDate) throw new CommandError("INVALID_INPUT", `Send real YYYY-MM-DD dates with the start before the end (got ${startDate} to ${endDate}).`);
14534
+ return {
14535
+ startDate,
14536
+ endDate
14537
+ };
14420
14538
  }
14421
14539
  function earlierRange(range) {
14422
14540
  return decayComparisonRange(range, "previous-period");
@@ -14500,20 +14618,38 @@ function listOfUpdates(value, checked) {
14500
14618
  if (value.length === 0) return `Google updates ${where}: none.`;
14501
14619
  return `Google updates ${where} (timing only, not cause): ${value.map((update) => `${update.name} (${update.start.slice(0, 10)} to ${update.end?.slice(0, 10) ?? "ongoing"})`).join("; ")}.`;
14502
14620
  }
14621
+ function readable(names, who = "it") {
14622
+ if (!names.length) return `Search Console properties ${who} can read: none.`;
14623
+ const more = names.length > 20 ? `, and ${names.length - 20} more` : "";
14624
+ return `Search Console properties ${who} can read: ${names.slice(0, 20).join(", ")}${more}.`;
14625
+ }
14503
14626
  function matchProperty(names, site) {
14504
- if (names.includes(site)) return site;
14505
- let host;
14506
- let origin;
14627
+ const value = site.trim();
14628
+ if (names.includes(value)) return value;
14629
+ let url;
14507
14630
  try {
14508
- const url = new URL(site.includes("://") ? site : `https://${site.replace(/^sc-domain:/, "")}`);
14509
- host = url.hostname.replace(/^www\./, "");
14510
- origin = url.origin;
14631
+ url = new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/i, "")}`);
14511
14632
  } catch {
14512
14633
  return null;
14513
14634
  }
14514
- const domain = names.filter((name) => name.startsWith("sc-domain:")).map((name) => name.slice(10)).filter((name) => host === name || host.endsWith(`.${name}`)).sort((a, b) => b.length - a.length)[0];
14515
- if (domain) return `sc-domain:${domain}`;
14516
- return names.find((name) => name === `${origin}/` || name === `${origin.replace("://", "://www.")}/` || name === `${origin.replace("://www.", "://")}/`) ?? null;
14635
+ const bare = (name) => name.replace(/^www\./, "");
14636
+ const host = bare(url.hostname);
14637
+ const domain = names.filter((name) => name.startsWith("sc-domain:")).filter((name) => {
14638
+ const own = name.slice(10);
14639
+ return own.startsWith("www.") ? bare(own) === host : host === own || host.endsWith(`.${own}`);
14640
+ }).sort((a, b) => b.length - a.length || a.localeCompare(b))[0];
14641
+ if (domain) return domain;
14642
+ const schemes = value.includes("://") ? [url.protocol] : ["https:", "http:"];
14643
+ return names.map((name) => {
14644
+ try {
14645
+ return {
14646
+ name,
14647
+ url: new URL(name)
14648
+ };
14649
+ } catch {
14650
+ return null;
14651
+ }
14652
+ }).filter((entry) => entry !== null && schemes.includes(entry.url.protocol) && bare(entry.url.hostname) === host && `${url.pathname}/`.startsWith(entry.url.pathname)).sort((a, b) => b.url.pathname.length - a.url.pathname.length || schemes.indexOf(a.url.protocol) - schemes.indexOf(b.url.protocol) || Number(b.url.hostname === url.hostname) - Number(a.url.hostname === url.hostname) || a.name.localeCompare(b.name))[0]?.name ?? null;
14517
14653
  }
14518
14654
  //#endregion
14519
14655
  //#region packages/core/dist/commands/present.js
@@ -14935,9 +15071,10 @@ var LinkedAccounts = class {
14935
15071
  }
14936
15072
  async resolve(site) {
14937
15073
  if (this.google.length === 0) throw new CommandError("NOT_CONNECTED", `No Google account is connected. ${this.hint.signIn}`);
14938
- const found = matchProperty([...(await this.ownerMap()).keys()], site);
15074
+ const names = await this.properties();
15075
+ const found = matchProperty(names, site);
14939
15076
  if (found) return found;
14940
- throw new CommandError("NO_ACCESS", `None of the connected Google accounts (${this.google.map((linked) => linked.email).join(", ")}) has a Search Console property for ${site}. ${this.hint.addGoogle}`);
15077
+ throw new CommandError("NO_ACCESS", `None of the connected Google accounts (${this.google.map((linked) => linked.email).join(", ")}) has a Search Console property for ${site}. ${readable(names, "they")} ${this.hint.addGoogle}`);
14941
15078
  }
14942
15079
  async owner(site) {
14943
15080
  const owner = (await this.ownerMap()).get(await this.resolve(site));
@@ -15286,6 +15423,10 @@ function classify(error) {
15286
15423
  errorCategory: "crawl_timeout",
15287
15424
  failureReason: "crawl_timeout"
15288
15425
  };
15426
+ const provider = signals.find((signal) => signal.name === "ProviderError");
15427
+ const providerCode = PROVIDER_ERROR_CODES[provider?.code];
15428
+ const fromProvider = providerCode && PRODUCT_FAILURES[providerCode];
15429
+ if (fromProvider) return fromProvider;
15289
15430
  for (const code of codes) {
15290
15431
  const product = PRODUCT_FAILURES[code];
15291
15432
  if (product) return product;
@@ -20540,7 +20681,7 @@ async function queryPagesTopQueriesBatch(site, pageUrls, range = 28) {
20540
20681
  }
20541
20682
  //#endregion
20542
20683
  //#region packages/core/dist/gsc/client/sites.js
20543
- async function listSites(_refresh = false, accountEmail) {
20684
+ async function listSites$1(_refresh = false, accountEmail) {
20544
20685
  const entries = !accountEmail && cloudLinked() ? await allCloudSites() : await listSearchConsoleSites(googleFetchFor((await getAuthorized(accountEmail)).client), LOCAL_SIGN_IN);
20545
20686
  writeToCache((database) => {
20546
20687
  const insert = database.prepare("INSERT OR REPLACE INTO sites (site_url, display_name, permission, added_at, is_default) VALUES (?, ?, ?, ?, COALESCE((SELECT is_default FROM sites WHERE site_url = ?), 0))");
@@ -26618,35 +26759,6 @@ async function fetchSitemapUrls(input) {
26618
26759
  };
26619
26760
  }
26620
26761
  //#endregion
26621
- //#region packages/core/dist/providers/errors.js
26622
- var ProviderError = class extends Error {
26623
- provider;
26624
- operation;
26625
- code;
26626
- status;
26627
- retryable;
26628
- constructor(input) {
26629
- super(input.message, { cause: input.cause });
26630
- this.name = "ProviderError";
26631
- this.provider = input.provider;
26632
- this.operation = input.operation;
26633
- this.code = input.code;
26634
- this.status = input.status ?? null;
26635
- this.retryable = input.retryable ?? false;
26636
- }
26637
- toJSON() {
26638
- return {
26639
- name: this.name,
26640
- provider: this.provider,
26641
- operation: this.operation,
26642
- code: this.code,
26643
- message: this.message,
26644
- status: this.status,
26645
- retryable: this.retryable
26646
- };
26647
- }
26648
- };
26649
- //#endregion
26650
26762
  //#region packages/core/dist/providers/cache.js
26651
26763
  function stableValue(value) {
26652
26764
  if (Array.isArray(value)) return value.map(stableValue);
@@ -33081,7 +33193,7 @@ function analyzeTrafficAnomalyRows(input) {
33081
33193
  const comparisonStart = shiftIsoDate(input.endDate, -(input.recentDays - 1));
33082
33194
  const baselineEnd = shiftIsoDate(comparisonStart, -1);
33083
33195
  const baselineExpectedDays = calendarDays(input.startDate, baselineEnd);
33084
- if (comparisonStart < input.startDate || baselineExpectedDays < 14) throw new SeoError("INSUFFICIENT_DATA", "The requested calendar range does not contain at least 14 baseline days before the comparison window.");
33196
+ if (comparisonStart < input.startDate || baselineExpectedDays < 14) throw new SeoError("INSUFFICIENT_DATA", `The date range needs at least ${input.recentDays + 14} days: 14 baseline days before the last ${input.recentDays}. Send a longer range, or leave the dates out for the default.`);
33085
33197
  const aggregated = aggregateDailyRows({
33086
33198
  rows: input.rows,
33087
33199
  startDate: input.startDate,
@@ -33089,7 +33201,7 @@ function analyzeTrafficAnomalyRows(input) {
33089
33201
  });
33090
33202
  const baselineRows = aggregated.rows.filter((row) => (row.keys[0] ?? "") <= baselineEnd);
33091
33203
  const comparisonRows = aggregated.rows.filter((row) => (row.keys[0] ?? "") >= comparisonStart);
33092
- if (baselineRows.length < 14 || comparisonRows.length < 3) throw new SeoError("INSUFFICIENT_DATA", `Not enough observed daily GSC data for anomaly detection (${baselineRows.length} baseline days and ${comparisonRows.length} comparison days).`);
33204
+ if (baselineRows.length < 14 || comparisonRows.length < 3) throw new SeoError("INSUFFICIENT_DATA", `Not enough daily Search Console data to look for a drop: ${baselineRows.length} baseline days and ${comparisonRows.length} comparison days, and it needs 14 and 3. The site may be new or get too few clicks for daily rows.`);
33093
33205
  const metrics = ["clicks", "impressions"];
33094
33206
  const stepCaveats = [];
33095
33207
  const anomalies = metrics.map((metric) => {
@@ -39434,25 +39546,64 @@ function need(headers, provider) {
39434
39546
  if (values.some((value) => !value)) throw new CommandError("NO_KEY", `No ${provider} key. Hosted MCP: save it at https://seoskill.dev/account#keys. CLI and local MCP: run \`seo login\` to use the account's keys, or \`seo providers ${provider} connect\`.`);
39435
39547
  return values;
39436
39548
  }
39549
+ const TASK_TIMEOUT_MS = 6e4;
39550
+ const READ_TIMEOUT_MS = 3e4;
39551
+ const RETRY_DELAY_MS = 250;
39552
+ const retryStatus = (status) => status === 429 || status >= 500;
39437
39553
  async function call(provider, request) {
39438
- const response = await fetch(request);
39439
- if (response.ok) return response;
39440
- const text = (await response.text()).slice(0, 300);
39441
- throw new CommandError(response.status === 401 || response.status === 403 ? "NO_KEY" : "PROVIDER_ERROR", `${provider} answered ${response.status}. ${text}`);
39554
+ const read = request.method === "GET";
39555
+ const timeoutMs = read ? READ_TIMEOUT_MS : TASK_TIMEOUT_MS;
39556
+ for (let attempt = 1;; attempt += 1) {
39557
+ const last = attempt === 2;
39558
+ let response;
39559
+ try {
39560
+ response = await fetch(request.clone(), { signal: AbortSignal.timeout(timeoutMs) });
39561
+ } catch (error) {
39562
+ if (read && !last) {
39563
+ await wait(RETRY_DELAY_MS);
39564
+ continue;
39565
+ }
39566
+ throw new CommandError("PROVIDER_ERROR", error.name === "TimeoutError" ? `${provider} did not answer within ${timeoutMs / 1e3} seconds.` : `${provider} could not be reached: ${error.message}`);
39567
+ }
39568
+ if (response.ok) return response;
39569
+ if (retryStatus(response.status) && !last) {
39570
+ await response.body?.cancel();
39571
+ await wait(RETRY_DELAY_MS);
39572
+ continue;
39573
+ }
39574
+ const text = (await response.text()).slice(0, 300);
39575
+ throw new CommandError(response.status === 401 || response.status === 403 ? "NO_KEY" : response.status === 429 ? "RATE_LIMITED" : "PROVIDER_ERROR", `${provider} answered ${response.status}. ${text}`);
39576
+ }
39442
39577
  }
39578
+ const wait = (ms) => new Promise((done) => setTimeout(done, ms));
39443
39579
  function requireInput(input) {
39444
39580
  const needsKeywords = input.action === "keyword_metrics" || input.action === "serp";
39445
39581
  if (needsKeywords && input.keywords.length === 0) throw new CommandError("INVALID_INPUT", `${input.action} needs keywords.`);
39446
39582
  if (!needsKeywords && !input.target) throw new CommandError("INVALID_INPUT", `${input.action} needs a target domain or URL.`);
39447
39583
  }
39584
+ async function eachKeyword(keywords, missed, rowsFor) {
39585
+ const rows = [];
39586
+ let failure;
39587
+ for (const keyword of keywords) try {
39588
+ rows.push(...await rowsFor(keyword));
39589
+ } catch (error) {
39590
+ if (error instanceof CommandError && error.code === "NO_KEY") throw error;
39591
+ failure ??= error;
39592
+ missed.push(keyword);
39593
+ }
39594
+ if (missed.length === keywords.length) throw failure;
39595
+ return rows;
39596
+ }
39448
39597
  async function research(provider, headers, input) {
39449
39598
  requireInput(input);
39450
39599
  const keys = need(headers, provider);
39451
39600
  if (provider === "dataforseo") return dataForSeo(keys, input);
39601
+ const missed = [];
39452
39602
  return {
39453
- rows: await ahrefs(keys[0] ?? "", input),
39603
+ rows: await ahrefs(keys[0] ?? "", input, missed),
39454
39604
  costUsd: null,
39455
- partial: false
39605
+ partial: false,
39606
+ missed
39456
39607
  };
39457
39608
  }
39458
39609
  function countryName(code) {
@@ -39518,12 +39669,14 @@ const PARTIAL = 40106;
39518
39669
  async function dataForSeo(keys, input) {
39519
39670
  const spent = {
39520
39671
  cost: 0,
39521
- partial: false
39672
+ partial: false,
39673
+ missed: []
39522
39674
  };
39523
39675
  return {
39524
39676
  rows: await dataForSeoRows(keys, input, spent),
39525
39677
  costUsd: Math.round(spent.cost * 1e4) / 1e4,
39526
- partial: spent.partial
39678
+ partial: spent.partial,
39679
+ missed: spent.missed
39527
39680
  };
39528
39681
  }
39529
39682
  async function dataForSeoRows(keys, input, spent) {
@@ -39565,28 +39718,20 @@ async function dataForSeoRows(keys, input, spent) {
39565
39718
  keyword_difficulty: "keyword_data.keyword_properties.keyword_difficulty",
39566
39719
  main_intent: "keyword_data.search_intent_info.main_intent"
39567
39720
  }));
39568
- if (input.action === "serp") {
39569
- const rows = [];
39570
- for (const keyword of input.keywords.slice(0, 5)) {
39571
- const items = await post("v3/serp/google/organic/live/advanced", {
39572
- keyword,
39573
- depth: Math.min(input.limit, 100)
39574
- });
39575
- for (const item of items) {
39576
- if (item.type !== "organic") continue;
39577
- rows.push({
39578
- keyword,
39579
- ...pick(item, {
39580
- position: "rank_absolute",
39581
- url: "url",
39582
- domain: "domain",
39583
- title: "title"
39584
- })
39585
- });
39586
- }
39587
- }
39588
- return rows;
39589
- }
39721
+ if (input.action === "serp") return eachKeyword(input.keywords.slice(0, 5), spent.missed, async (keyword) => {
39722
+ return (await post("v3/serp/google/organic/live/advanced", {
39723
+ keyword,
39724
+ depth: Math.min(input.limit, 100)
39725
+ })).filter((item) => item.type === "organic").map((item) => ({
39726
+ keyword,
39727
+ ...pick(item, {
39728
+ position: "rank_absolute",
39729
+ url: "url",
39730
+ domain: "domain",
39731
+ title: "title"
39732
+ })
39733
+ }));
39734
+ });
39590
39735
  if (input.action === "backlinks") return (await post("v3/backlinks/referring_domains/live", {
39591
39736
  target: bareTarget(input.target),
39592
39737
  limit: input.limit,
@@ -39615,7 +39760,7 @@ function isPage(target) {
39615
39760
  function bareTarget(target) {
39616
39761
  return target.replace(/^https?:\/\//, "").replace(/\/$/, "");
39617
39762
  }
39618
- async function ahrefs(key, input) {
39763
+ async function ahrefs(key, input, missed) {
39619
39764
  const get = async (path, params) => {
39620
39765
  const url = new URL(`https://api.ahrefs.com/v3/${path}`);
39621
39766
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
@@ -39639,22 +39784,17 @@ async function ahrefs(key, input) {
39639
39784
  limit: String(input.limit),
39640
39785
  order_by: "sum_traffic:desc"
39641
39786
  })).keywords ?? [];
39642
- if (input.action === "serp") {
39643
- const rows = [];
39644
- for (const keyword of input.keywords.slice(0, 5)) {
39645
- const data = await get("serp-overview/serp-overview", {
39646
- select: "position,type,url,title,domain_rating,traffic,keywords",
39647
- keyword,
39648
- country: input.country,
39649
- top_positions: String(Math.min(input.limit, 100))
39650
- });
39651
- for (const row of data.positions ?? []) rows.push({
39652
- keyword,
39653
- ...row
39654
- });
39655
- }
39656
- return rows;
39657
- }
39787
+ if (input.action === "serp") return eachKeyword(input.keywords.slice(0, 5), missed, async (keyword) => {
39788
+ return ((await get("serp-overview/serp-overview", {
39789
+ select: "position,type,url,title,domain_rating,traffic,keywords",
39790
+ keyword,
39791
+ country: input.country,
39792
+ top_positions: String(Math.min(input.limit, 100))
39793
+ })).positions ?? []).map((row) => ({
39794
+ keyword,
39795
+ ...row
39796
+ }));
39797
+ });
39658
39798
  if (input.action === "backlinks") return (await get("site-explorer/refdomains", {
39659
39799
  select: "domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen",
39660
39800
  target: input.target,
@@ -42376,7 +42516,7 @@ async function runResearch(input, keys, options) {
42376
42516
  const offset = input.offset ?? 0;
42377
42517
  const language = input.provider === "dataforseo" ? dataForSeoLanguage(country) : null;
42378
42518
  if (input.action === "ai_answers") throw new CommandError("INVALID_INPUT", "ai_answers runs on its own.");
42379
- const { rows: found, costUsd, partial } = await research(input.provider, keys, {
42519
+ const { rows: found, costUsd, partial, missed } = await research(input.provider, keys, {
42380
42520
  action: input.action,
42381
42521
  keywords: input.keywords ?? [],
42382
42522
  target,
@@ -42402,6 +42542,7 @@ async function runResearch(input, keys, options) {
42402
42542
  `${input.provider} estimates, in the provider’s own units.`,
42403
42543
  ...costUsd !== null ? [`DataForSEO cost: $${costUsd}.`] : [],
42404
42544
  ...partial ? ["Partial: DataForSEO could not fetch some pages of results, so rows may be missing."] : [],
42545
+ ...missed.length ? [`Partial: ${input.provider} failed for ${missed.join(", ")}, so those keywords have no rows. Ask for them again.`] : [],
42405
42546
  ...note ? [note] : [],
42406
42547
  ...hasTargetDr ? [`${bareDomain(target)} domain_rating: ${targetDr}.`] : [],
42407
42548
  ...rated.added || hasTargetDr ? [DR_ATTRIBUTION] : []
@@ -42419,6 +42560,7 @@ async function runResearch(input, keys, options) {
42419
42560
  leftOut: note,
42420
42561
  costUsd,
42421
42562
  partial,
42563
+ missedKeywords: missed,
42422
42564
  total: rated.rows.length,
42423
42565
  offset,
42424
42566
  rows: rated.rows.slice(offset, offset + 50)
@@ -42673,10 +42815,10 @@ function markdownText(markdown, url) {
42673
42815
  return `Main content as Markdown (live page${markdown.truncated ? `, cut to ${markdown.markdown.length.toLocaleString("en-US")} of ${markdown.characters.toLocaleString("en-US")} characters` : ""}):\n\n${markdown.markdown}`;
42674
42816
  }
42675
42817
  async function runCompare(input, deps) {
42676
- if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) throw new CommandError("INVALID_INPUT", "The date must be YYYY-MM-DD.");
42818
+ if (!realIsoDate(input.date)) throw new CommandError("INVALID_INPUT", "The date must be a real day as YYYY-MM-DD.");
42677
42819
  const last = latestFinalGscDate(deps.now?.() ?? /* @__PURE__ */ new Date());
42678
42820
  const daysAfter = Math.round((Date.parse(last) - Date.parse(input.date)) / 864e5) + 1;
42679
- if (daysAfter < 3) throw new CommandError("INVALID_INPUT", `Search Console has final data to ${last}. Use a date at least 3 days before that.`);
42821
+ if (daysAfter < 3) throw new CommandError("INVALID_INPUT", `Search Console has final data to ${last}, and a comparison needs 3 days after the change. The latest date that works is ${shiftIsoDate(last, -2)}.`);
42680
42822
  const days = Math.min(28, daysAfter);
42681
42823
  const after = {
42682
42824
  startDate: input.date,
@@ -43048,8 +43190,15 @@ function weekMarkdown(data) {
43048
43190
  out.push("", "## Ask your agent", "", "Paste this into your agent:", "", "```", prompt, "```");
43049
43191
  return out.join("\n");
43050
43192
  }
43051
- async function runAllSites(deps) {
43193
+ async function runAllSites(deps, sites) {
43052
43194
  const data = deps.week ? await deps.week() : null;
43195
+ if (!data && sites) {
43196
+ const list = await sites();
43197
+ return {
43198
+ text: `No site given. Pick one below and send it: ${deps.call("report", { site: "<site>" })}. The summary of every site is for accounts in the weekly site audit beta.\n\n${list.text}`,
43199
+ data: list.data
43200
+ };
43201
+ }
43053
43202
  if (!data) throw new CommandError("INVALID_INPUT", `No site given. Name one: ${deps.call("report", { site: "<site>" })}. The summary of every site is for accounts in the weekly site audit beta.`);
43054
43203
  const startMs = Date.parse(`${data.start}T00:00:00Z`);
43055
43204
  const endMs = Date.parse(`${data.end}T00:00:00Z`) + DAY_MS;
@@ -43065,8 +43214,8 @@ async function runAllSites(deps) {
43065
43214
  //#region packages/core/dist/commands/tools.js
43066
43215
  const site = z.string().min(1).describe("Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.");
43067
43216
  const dates = {
43068
- start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; send with end_date."),
43069
- end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; send with start_date.")
43217
+ start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; without end_date it runs to the latest final day."),
43218
+ end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional.")
43070
43219
  };
43071
43220
  const detail = {
43072
43221
  detail: z.enum([
@@ -43085,6 +43234,14 @@ const DATA_SOURCES = [
43085
43234
  function tool(spec) {
43086
43235
  return spec;
43087
43236
  }
43237
+ const listSites = async (surface) => runSites({
43238
+ accounts: await surface.accounts(),
43239
+ keys: await surface.keys(),
43240
+ clicky: clickySites(await surface.keys()).map(clickyLabel),
43241
+ signIn: surface.signIn,
43242
+ bing: surface.deps.bing ? await surface.deps.bing() : void 0,
43243
+ connect: surface.connect
43244
+ });
43088
43245
  const SHARED_TOOLS = [
43089
43246
  tool({
43090
43247
  name: "seo_sites",
@@ -43092,20 +43249,13 @@ const SHARED_TOOLS = [
43092
43249
  title: "List sites",
43093
43250
  description: "Lists the Search Console and Google Analytics properties the connected Google accounts can read, the Bing Webmaster site for each, and which research keys are set. Call it first.",
43094
43251
  input: z.object({}),
43095
- run: async (surface) => runSites({
43096
- accounts: await surface.accounts(),
43097
- keys: await surface.keys(),
43098
- clicky: clickySites(await surface.keys()).map(clickyLabel),
43099
- signIn: surface.signIn,
43100
- bing: surface.deps.bing ? await surface.deps.bing() : void 0,
43101
- connect: surface.connect
43102
- })
43252
+ run: listSites
43103
43253
  }),
43104
43254
  tool({
43105
43255
  name: "seo_report",
43106
43256
  command: "report",
43107
43257
  title: "Search Console report",
43108
- description: `Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with what needs a look and the next step for each, ending with the site to ask about next; an account outside that beta must send site. With site: the main report for that Search Console property, the overview plus each section's row count and top rows. With site and section: that section's rows; page sections also check their top 20 URLs live. Sections:\n${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}`,
43258
+ description: `Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with what needs a look and the next step for each, ending with the site to ask about next; an account outside that beta gets its site list to pick from. With site: the main report for that Search Console property, the overview plus each section's row count and top rows. With site and section: that section's rows; page sections also check their top 20 URLs live. Sections:\n${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}`,
43109
43259
  input: z.object({
43110
43260
  site: site.optional().describe("Leave it out for the summary of every audited site (weekly site audit beta). Otherwise a Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com."),
43111
43261
  section: z.enum(SECTION_IDS).optional().describe("Optional, needs site. Leave it out first; then ask for one section."),
@@ -43118,7 +43268,7 @@ const SHARED_TOOLS = [
43118
43268
  site: "<site>",
43119
43269
  section: input.section
43120
43270
  })}.`);
43121
- return runAllSites(surface.deps);
43271
+ return runAllSites(surface.deps, () => listSites(surface));
43122
43272
  }
43123
43273
  const target = input.site;
43124
43274
  const resolved = await surface.resolveSite(target).catch((error) => error instanceof Error ? error : new Error(String(error)));
@@ -43674,8 +43824,9 @@ async function localRefreshClicky() {
43674
43824
  return localResearchKeys();
43675
43825
  }
43676
43826
  async function resolveLocalSite(site, options = {}) {
43677
- const found = matchProperty((await listSites(options.refresh, options.account)).map((entry) => entry.siteUrl), site);
43678
- if (!found) throw new CommandError("NO_ACCESS", `This Google login has no Search Console property for ${site}. Run \`seo sites\` to list the properties it can read.`);
43827
+ const names = (await listSites$1(options.refresh, options.account)).map((entry) => entry.siteUrl);
43828
+ const found = matchProperty(names, site);
43829
+ if (!found) throw new CommandError("NO_ACCESS", `This Google login has no Search Console property for ${site}. ${readable(names)}`);
43679
43830
  return found;
43680
43831
  }
43681
43832
  const ERROR_CODES = {
@@ -43683,6 +43834,7 @@ const ERROR_CODES = {
43683
43834
  NOT_CONNECTED: "AUTH_REQUIRED",
43684
43835
  NO_ACCESS: "PROPERTY_NOT_FOUND",
43685
43836
  NO_KEY: "AUTH_CONFIG_REQUIRED",
43837
+ NO_DATA: "INSUFFICIENT_DATA",
43686
43838
  RATE_LIMITED: "RATE_LIMITED",
43687
43839
  GOOGLE_ERROR: "PROVIDER_UNAVAILABLE",
43688
43840
  PROVIDER_ERROR: "PROVIDER_UNAVAILABLE"
@@ -46607,13 +46759,20 @@ const REMOVED_REPORT_IDS = {
46607
46759
  "server-log-analysis": null,
46608
46760
  "technical-watch": "seo crawl"
46609
46761
  };
46610
- function activeReport(id) {
46762
+ const REPORT_ALIASES = {
46763
+ cannibalization: "cannibalisation",
46764
+ "content-decay": "decaying-pages",
46765
+ "low-ctr": "ctr-underperformers",
46766
+ "traffic-drop": "traffic-anomaly"
46767
+ };
46768
+ function activeReport(name) {
46769
+ const id = REPORT_ALIASES[name] ?? name;
46611
46770
  if (Object.hasOwn(REMOVED_REPORT_IDS, id)) {
46612
46771
  const replacement = REMOVED_REPORT_IDS[id];
46613
46772
  throw new SeoError("INVALID_INPUT", replacement ? `The ${id} report was removed. Use \`${replacement}\` instead.` : `The ${id} report was removed. Run \`seo help\` for the commands.`);
46614
46773
  }
46615
46774
  const report = getReportDefinition(id);
46616
- if (!report) throw new SeoError("INVALID_INPUT", `Unknown report: ${id}.`);
46775
+ if (!report) throw new SeoError("INVALID_INPUT", `Unknown report: ${id}. Reports: ${listReportDefinitions().map((item) => item.id).join(", ")}.`);
46617
46776
  return report;
46618
46777
  }
46619
46778
  function listReports(category) {