seo 0.13.0 → 0.13.1

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-BIXGSxFJ.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) => {
@@ -42673,10 +42785,10 @@ function markdownText(markdown, url) {
42673
42785
  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
42786
  }
42675
42787
  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.");
42788
+ if (!realIsoDate(input.date)) throw new CommandError("INVALID_INPUT", "The date must be a real day as YYYY-MM-DD.");
42677
42789
  const last = latestFinalGscDate(deps.now?.() ?? /* @__PURE__ */ new Date());
42678
42790
  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.`);
42791
+ 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
42792
  const days = Math.min(28, daysAfter);
42681
42793
  const after = {
42682
42794
  startDate: input.date,
@@ -43048,8 +43160,15 @@ function weekMarkdown(data) {
43048
43160
  out.push("", "## Ask your agent", "", "Paste this into your agent:", "", "```", prompt, "```");
43049
43161
  return out.join("\n");
43050
43162
  }
43051
- async function runAllSites(deps) {
43163
+ async function runAllSites(deps, sites) {
43052
43164
  const data = deps.week ? await deps.week() : null;
43165
+ if (!data && sites) {
43166
+ const list = await sites();
43167
+ return {
43168
+ 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}`,
43169
+ data: list.data
43170
+ };
43171
+ }
43053
43172
  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
43173
  const startMs = Date.parse(`${data.start}T00:00:00Z`);
43055
43174
  const endMs = Date.parse(`${data.end}T00:00:00Z`) + DAY_MS;
@@ -43065,8 +43184,8 @@ async function runAllSites(deps) {
43065
43184
  //#region packages/core/dist/commands/tools.js
43066
43185
  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
43186
  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.")
43187
+ 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."),
43188
+ end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional.")
43070
43189
  };
43071
43190
  const detail = {
43072
43191
  detail: z.enum([
@@ -43085,6 +43204,14 @@ const DATA_SOURCES = [
43085
43204
  function tool(spec) {
43086
43205
  return spec;
43087
43206
  }
43207
+ const listSites = async (surface) => runSites({
43208
+ accounts: await surface.accounts(),
43209
+ keys: await surface.keys(),
43210
+ clicky: clickySites(await surface.keys()).map(clickyLabel),
43211
+ signIn: surface.signIn,
43212
+ bing: surface.deps.bing ? await surface.deps.bing() : void 0,
43213
+ connect: surface.connect
43214
+ });
43088
43215
  const SHARED_TOOLS = [
43089
43216
  tool({
43090
43217
  name: "seo_sites",
@@ -43092,20 +43219,13 @@ const SHARED_TOOLS = [
43092
43219
  title: "List sites",
43093
43220
  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
43221
  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
- })
43222
+ run: listSites
43103
43223
  }),
43104
43224
  tool({
43105
43225
  name: "seo_report",
43106
43226
  command: "report",
43107
43227
  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")}`,
43228
+ 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
43229
  input: z.object({
43110
43230
  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
43231
  section: z.enum(SECTION_IDS).optional().describe("Optional, needs site. Leave it out first; then ask for one section."),
@@ -43118,7 +43238,7 @@ const SHARED_TOOLS = [
43118
43238
  site: "<site>",
43119
43239
  section: input.section
43120
43240
  })}.`);
43121
- return runAllSites(surface.deps);
43241
+ return runAllSites(surface.deps, () => listSites(surface));
43122
43242
  }
43123
43243
  const target = input.site;
43124
43244
  const resolved = await surface.resolveSite(target).catch((error) => error instanceof Error ? error : new Error(String(error)));
@@ -43674,8 +43794,9 @@ async function localRefreshClicky() {
43674
43794
  return localResearchKeys();
43675
43795
  }
43676
43796
  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.`);
43797
+ const names = (await listSites$1(options.refresh, options.account)).map((entry) => entry.siteUrl);
43798
+ const found = matchProperty(names, site);
43799
+ if (!found) throw new CommandError("NO_ACCESS", `This Google login has no Search Console property for ${site}. ${readable(names)}`);
43679
43800
  return found;
43680
43801
  }
43681
43802
  const ERROR_CODES = {
@@ -43683,6 +43804,7 @@ const ERROR_CODES = {
43683
43804
  NOT_CONNECTED: "AUTH_REQUIRED",
43684
43805
  NO_ACCESS: "PROPERTY_NOT_FOUND",
43685
43806
  NO_KEY: "AUTH_CONFIG_REQUIRED",
43807
+ NO_DATA: "INSUFFICIENT_DATA",
43686
43808
  RATE_LIMITED: "RATE_LIMITED",
43687
43809
  GOOGLE_ERROR: "PROVIDER_UNAVAILABLE",
43688
43810
  PROVIDER_ERROR: "PROVIDER_UNAVAILABLE"
@@ -46607,13 +46729,20 @@ const REMOVED_REPORT_IDS = {
46607
46729
  "server-log-analysis": null,
46608
46730
  "technical-watch": "seo crawl"
46609
46731
  };
46610
- function activeReport(id) {
46732
+ const REPORT_ALIASES = {
46733
+ cannibalization: "cannibalisation",
46734
+ "content-decay": "decaying-pages",
46735
+ "low-ctr": "ctr-underperformers",
46736
+ "traffic-drop": "traffic-anomaly"
46737
+ };
46738
+ function activeReport(name) {
46739
+ const id = REPORT_ALIASES[name] ?? name;
46611
46740
  if (Object.hasOwn(REMOVED_REPORT_IDS, id)) {
46612
46741
  const replacement = REMOVED_REPORT_IDS[id];
46613
46742
  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
46743
  }
46615
46744
  const report = getReportDefinition(id);
46616
- if (!report) throw new SeoError("INVALID_INPUT", `Unknown report: ${id}.`);
46745
+ if (!report) throw new SeoError("INVALID_INPUT", `Unknown report: ${id}. Reports: ${listReportDefinitions().map((item) => item.id).join(", ")}.`);
46617
46746
  return report;
46618
46747
  }
46619
46748
  function listReports(category) {