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/cli.js +75 -75
- package/dist/cli.js.map +1 -1
- package/dist/dist-7TxFytBp.js +420 -0
- package/dist/dist-7TxFytBp.js.map +1 -0
- package/dist/{dist-CLV_HXUd.js → dist-BBszRLUG.js} +1 -1
- package/dist/{esm-BfNQ2FiC.js → esm-Dmtr8qrP.js} +2 -2
- package/dist/{esm-BfNQ2FiC.js.map → esm-Dmtr8qrP.js.map} +1 -1
- package/dist/index.d.ts +128 -126
- package/dist/index.js +207 -118
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +301 -142
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/dist-wzZsD_Fi.js +0 -420
- package/dist/dist-wzZsD_Fi.js.map +0 -1
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
7613
|
-
const
|
|
7614
|
-
if (!VALID_SCHEME.test(
|
|
7615
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (
|
|
7824
|
-
parsed.
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
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
|
-
|
|
14505
|
-
|
|
14506
|
-
let
|
|
14627
|
+
const value = site.trim();
|
|
14628
|
+
if (names.includes(value)) return value;
|
|
14629
|
+
let url;
|
|
14507
14630
|
try {
|
|
14508
|
-
|
|
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
|
|
14515
|
-
|
|
14516
|
-
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
39439
|
-
|
|
39440
|
-
|
|
39441
|
-
|
|
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
|
-
|
|
39570
|
-
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
|
|
39575
|
-
|
|
39576
|
-
|
|
39577
|
-
|
|
39578
|
-
|
|
39579
|
-
|
|
39580
|
-
|
|
39581
|
-
|
|
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
|
-
|
|
39644
|
-
|
|
39645
|
-
|
|
39646
|
-
|
|
39647
|
-
|
|
39648
|
-
|
|
39649
|
-
|
|
39650
|
-
|
|
39651
|
-
|
|
39652
|
-
|
|
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 (
|
|
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}
|
|
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;
|
|
43069
|
-
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional
|
|
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:
|
|
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
|
|
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
|
|
43678
|
-
|
|
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
|
-
|
|
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) {
|