seo 0.17.1 → 0.17.3

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as version, t as load } from "./esm-G8B5TPzB.js";
2
+ import { n as version, t as load } from "./esm-HKeFfXOD.js";
3
3
  import { SEO_PROVIDER_API_VERSION } from "./provider-sdk.js";
4
4
  import crypto$1, { createHash, randomUUID, sign } from "node:crypto";
5
5
  import { z } from "zod";
@@ -453,13 +453,20 @@ const RULE_DEFINITIONS = [
453
453
  title: "Large HTML response",
454
454
  category: "performance",
455
455
  defaultSeverity: "low",
456
- whyItMatters: "Large HTML takes longer to download and parse before the browser can even start the rest of the page.",
457
- howToFix: "Trim duplicated markup, remove heavy inline data, split non-critical content, and move large scripts or styles out of the HTML response.",
458
- impactIfIgnored: "Pages can feel slow on mobile connections and crawlers spend more time processing each URL.",
459
- howToVerify: "Re-run the crawl and confirm sizeBytes is below the large HTML threshold for normal pages.",
456
+ whyItMatters: "Googlebot reads only the first 2MB of uncompressed HTML for Google Search and indexes the page as if the file ended there. When the h1 or main element, or most of the visible text, sits past that cut, Google may index the page without its main content. Large HTML is also slow to download and parse.",
457
+ howToFix: "Move the main content earlier and trim what comes before it: large inline CSS, inline SVG, JSON data and duplicated markup. Load non-critical styles and scripts as separate files.",
458
+ impactIfIgnored: "Content past the 2MB cut is not indexed, and every page of the template carries the same weight on slow connections.",
459
+ howToVerify: "Re-run the crawl and confirm sizeBytes is under 2,097,152, or that mainContentStartBytes is well before it and few visible words sit after it. The word split is a heuristic from the raw HTML.",
460
460
  agentHints: {
461
- evidenceFields: ["page.sizeBytes", "issue.evidence.thresholdBytes"],
462
- suggestedCommands: ["seo crawl <url> --json"]
461
+ evidenceFields: [
462
+ "page.sizeBytes",
463
+ "issue.evidence.thresholdBytes",
464
+ "issue.evidence.mainContentStartBytes",
465
+ "issue.evidence.bytesAfterLimit",
466
+ "issue.evidence.visibleWordsAfterLimit",
467
+ "issue.evidence.indexingRisk"
468
+ ],
469
+ suggestedCommands: ["seo crawl <url> --mode page --json"]
463
470
  }
464
471
  },
465
472
  {
@@ -1097,17 +1104,17 @@ function isAgentFindings(value) {
1097
1104
  const counts = record(candidate?.counts);
1098
1105
  return candidate?.schemaVersion === 1 && candidate.scope === "returned-report" && typeof counts?.total === "number" && typeof counts.returned === "number" && Array.isArray(candidate.items) && candidate.items.every((value) => {
1099
1106
  const item = record(value);
1100
- if (!text(item?.id) || item?.status !== "open" || !text(item.title) || !text(item.sourcePath) || !Array.isArray(item.allowedOutcomes)) return false;
1101
- if (item.type === "fix") return Boolean(text(record(item.fix)?.instruction));
1107
+ if (!text$1(item?.id) || item?.status !== "open" || !text$1(item.title) || !text$1(item.sourcePath) || !Array.isArray(item.allowedOutcomes)) return false;
1108
+ if (item.type === "fix") return Boolean(text$1(record(item.fix)?.instruction));
1102
1109
  if (item.type === "review") {
1103
1110
  const review = record(item.review);
1104
1111
  if (!review) return false;
1105
- return Boolean(text(review.question) && text(review.changeOnlyIf) && text(review.ifConfirmed) && text(review.ifNotNeeded) && Array.isArray(review.doNot));
1112
+ return Boolean(text$1(review.question) && text$1(review.changeOnlyIf) && text$1(review.ifConfirmed) && text$1(review.ifNotNeeded) && Array.isArray(review.doNot));
1106
1113
  }
1107
1114
  return false;
1108
1115
  });
1109
1116
  }
1110
- function text(value) {
1117
+ function text$1(value) {
1111
1118
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1112
1119
  }
1113
1120
  function stableId(value) {
@@ -1130,7 +1137,7 @@ function findingId(item, path, action) {
1130
1137
  "actionId",
1131
1138
  "id"
1132
1139
  ]) {
1133
- const id = text(item[key]);
1140
+ const id = text$1(item[key]);
1134
1141
  if (id) return id;
1135
1142
  }
1136
1143
  return `action-${stableId(`${path}|${action}`)}`;
@@ -1140,9 +1147,9 @@ function findingType(path, item) {
1140
1147
  }
1141
1148
  function findingVerification(item, fallback) {
1142
1149
  const supplied = record(item.verification);
1143
- const expected = text(supplied?.expected ?? item.howToVerify ?? fallback);
1150
+ const expected = text$1(supplied?.expected ?? item.howToVerify ?? fallback);
1144
1151
  if (!expected) return void 0;
1145
- const command = text(supplied?.command);
1152
+ const command = text$1(supplied?.command);
1146
1153
  return {
1147
1154
  ...command ? { command } : {},
1148
1155
  expected
@@ -1150,14 +1157,14 @@ function findingVerification(item, fallback) {
1150
1157
  }
1151
1158
  function stringArray(value) {
1152
1159
  if (!Array.isArray(value)) return [];
1153
- return value.flatMap((item) => text(item) ? [text(item)] : []);
1160
+ return value.flatMap((item) => text$1(item) ? [text$1(item)] : []);
1154
1161
  }
1155
1162
  function affectedUrls(item, affectedCount) {
1156
1163
  const items = stringArray(item.sampleUrls);
1157
1164
  const details = record(item.affectedUrlsReport);
1158
1165
  const params = record(details?.params);
1159
- const reportId = details?.id === "affected-urls" ? text(params?.reportId) : void 0;
1160
- const ruleId = details?.id === "affected-urls" ? text(params?.ruleId) : void 0;
1166
+ const reportId = details?.id === "affected-urls" ? text$1(params?.reportId) : void 0;
1167
+ const ruleId = details?.id === "affected-urls" ? text$1(params?.ruleId) : void 0;
1161
1168
  if (!items.length && !reportId) return void 0;
1162
1169
  const total = affectedCount ?? items.length;
1163
1170
  return {
@@ -1177,13 +1184,13 @@ function affectedUrls(item, affectedCount) {
1177
1184
  function reviewGuidance(item, title, action) {
1178
1185
  const supplied = record(item.review);
1179
1186
  const evidence = record(item.evidence);
1180
- const ruleId = text(item.ruleId ?? evidence?.ruleId);
1187
+ const ruleId = text$1(item.ruleId ?? evidence?.ruleId);
1181
1188
  const defined = (ruleId ? explainRule(ruleId) : void 0)?.review;
1182
1189
  return {
1183
- question: text(supplied?.question) ?? defined?.question ?? `Does the retained evidence and confirmed intent show that "${title}" needs a change?`,
1184
- changeOnlyIf: text(supplied?.changeOnlyIf) ?? defined?.changeOnlyIf ?? "Make a change only when the retained evidence and required intent support it.",
1185
- ifConfirmed: text(supplied?.ifConfirmed) ?? action,
1186
- ifNotNeeded: text(supplied?.ifNotNeeded) ?? defined?.ifNotNeeded ?? "Record no change and cite the evidence or confirmed intent that makes a change unnecessary.",
1190
+ question: text$1(supplied?.question) ?? defined?.question ?? `Does the retained evidence and confirmed intent show that "${title}" needs a change?`,
1191
+ changeOnlyIf: text$1(supplied?.changeOnlyIf) ?? defined?.changeOnlyIf ?? "Make a change only when the retained evidence and required intent support it.",
1192
+ ifConfirmed: text$1(supplied?.ifConfirmed) ?? action,
1193
+ ifNotNeeded: text$1(supplied?.ifNotNeeded) ?? defined?.ifNotNeeded ?? "Record no change and cite the evidence or confirmed intent that makes a change unnecessary.",
1187
1194
  doNot: stringArray(supplied?.doNot).length ? stringArray(supplied?.doNot) : [...defined?.doNot ?? []]
1188
1195
  };
1189
1196
  }
@@ -1202,7 +1209,7 @@ function findingEvidence(item) {
1202
1209
  }
1203
1210
  function normalizeFinding(value, path, index, options) {
1204
1211
  if (typeof value === "string") {
1205
- const action = text(value);
1212
+ const action = text$1(value);
1206
1213
  if (!action) return void 0;
1207
1214
  const common = {
1208
1215
  id: `action-${stableId(`${options.reportId ?? ""}|${path}|${action}`)}`,
@@ -1234,8 +1241,8 @@ function normalizeFinding(value, path, index, options) {
1234
1241
  }
1235
1242
  const item = record(value);
1236
1243
  if (!item) return void 0;
1237
- const action = text(item.action) ?? text(item.howToFix) ?? (item.recommendation === "fix" || item.recommendation === "review" ? void 0 : text(item.recommendation)) ?? text(item.nextStep);
1238
- const title = text(item.title) ?? text(item.headline) ?? text(item.name) ?? action;
1244
+ const action = text$1(item.action) ?? text$1(item.howToFix) ?? (item.recommendation === "fix" || item.recommendation === "review" ? void 0 : text$1(item.recommendation)) ?? text$1(item.nextStep);
1245
+ const title = text$1(item.title) ?? text$1(item.headline) ?? text$1(item.name) ?? action;
1239
1246
  if (!action || !title) return void 0;
1240
1247
  const confidence = [
1241
1248
  "high",
@@ -1253,7 +1260,7 @@ function normalizeFinding(value, path, index, options) {
1253
1260
  status: "open",
1254
1261
  title,
1255
1262
  ...confidence ? { confidence } : {},
1256
- ...text(item.severity) ? { severity: text(item.severity) } : {},
1263
+ ...text$1(item.severity) ? { severity: text$1(item.severity) } : {},
1257
1264
  ...typeof affectedCount === "number" && Number.isFinite(affectedCount) ? { affectedCount } : {},
1258
1265
  ...urls ? { affectedUrls: urls } : {},
1259
1266
  sourcePath: `${path}[${index}]`,
@@ -1315,7 +1322,7 @@ function inventory(value, path, reportId) {
1315
1322
  const totalItems = typeof (source.totalPages ?? source.totalItems) === "number" ? Number(source.totalPages ?? source.totalItems) : source.rows.length;
1316
1323
  const items = source.rows.map((value, index) => {
1317
1324
  const row = record(value) ?? { value };
1318
- const identity = text(row.id) ?? text(row.url) ?? text(row.path) ?? `${path}[${index}]`;
1325
+ const identity = text$1(row.id) ?? text$1(row.url) ?? text$1(row.path) ?? `${path}[${index}]`;
1319
1326
  return {
1320
1327
  ...row,
1321
1328
  id: `inventory-item-${stableId(`${reportId ?? ""}|${path}|${identity}`)}`,
@@ -1334,7 +1341,7 @@ function inventory(value, path, reportId) {
1334
1341
  returnedItems: items.length,
1335
1342
  complete: source.capped === false && items.length >= totalItems,
1336
1343
  ...source.criteria !== void 0 ? { criteria: source.criteria } : {},
1337
- ...text(source.note) ? { note: text(source.note) } : {},
1344
+ ...text$1(source.note) ? { note: text$1(source.note) } : {},
1338
1345
  ...pageCount !== void 0 ? { pagination: {
1339
1346
  page,
1340
1347
  pageCount,
@@ -2802,7 +2809,7 @@ async function readableSites(google, signIn) {
2802
2809
  permissionLevel: entry.permissionLevel ?? ""
2803
2810
  })).sort((a, b) => a.siteUrl.localeCompare(b.siteUrl));
2804
2811
  }
2805
- const hostOf$3 = (value) => {
2812
+ const hostOf$4 = (value) => {
2806
2813
  try {
2807
2814
  return new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/, "")}`).hostname.replace(/^www\./, "");
2808
2815
  } catch {
@@ -2820,14 +2827,14 @@ async function runSites(input) {
2820
2827
  };
2821
2828
  })), input.bing ? listBingSites(input.bing).catch((error) => error instanceof Error ? error : new Error(String(error))) : Promise.resolve(null)]);
2822
2829
  const bingSites = Array.isArray(bing) ? bing.filter((site) => site.verified) : [];
2823
- const bingByHost = new Map(bingSites.map((site) => [hostOf$3(site.url), site]));
2830
+ const bingByHost = new Map(bingSites.map((site) => [hostOf$4(site.url), site]));
2824
2831
  const properties = google.flatMap((account) => account.properties instanceof Error ? [] : account.properties.map((property) => ({
2825
2832
  ...property,
2826
2833
  ...named ? { account: account.email ?? "" } : {},
2827
- ...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$3(property.siteUrl))?.url ?? "" } : {}
2834
+ ...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$4(property.siteUrl))?.url ?? "" } : {}
2828
2835
  })));
2829
- const googleHosts = new Set(properties.map((property) => hostOf$3(property.siteUrl)));
2830
- const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$3(site.url)));
2836
+ const googleHosts = new Set(properties.map((property) => hostOf$4(property.siteUrl)));
2837
+ const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$4(site.url)));
2831
2838
  const failed = google.filter((account) => account.properties instanceof Error);
2832
2839
  const analytics = google.flatMap((account) => (account.analytics ?? []).flatMap((summary) => summary.propertySummaries.map((property) => ({
2833
2840
  property: property.property.replace("properties/", ""),
@@ -11477,6 +11484,98 @@ function contentOptimizationFromPageOpportunities(sourceReport) {
11477
11484
  async function contentOptimizationReport(input) {
11478
11485
  return contentOptimizationFromPageOpportunities(await pageOpportunitiesReport(input));
11479
11486
  }
11487
+ // packages/core/src/analyze/crawler/html-size.ts
11488
+ const GOOGLEBOT_HTML_LIMIT_BYTES = 2097152;
11489
+ const NEAR_LIMIT_SHARE = .9;
11490
+ const HIDDEN_BLOCKS = /<(script|style|svg|noscript|template)\b[\s\S]*?(?:<\/\1\s*>|$)|<!--[\s\S]*?(?:-->|$)/gi;
11491
+ function wordCount(text) {
11492
+ return text.match(/[^\s&;]+/g)?.length ?? 0;
11493
+ }
11494
+ /** Where a page over Googlebot's limit puts its main content. */
11495
+ function googlebotCutEvidence(html) {
11496
+ const bytes = Buffer.from(html, "utf8");
11497
+ if (bytes.byteLength <= 2097152) return void 0;
11498
+ const marker = /<(h1|main)[\s>/]/i.exec(html);
11499
+ const text = html.replace(HIDDEN_BLOCKS, (match) => " ".repeat(match.length)).replace(/<[^>]*>?/g, (match) => " ".repeat(match.length));
11500
+ const limitIndex = bytes.subarray(0, GOOGLEBOT_HTML_LIMIT_BYTES).toString("utf8").length;
11501
+ const startBytes = marker ? Buffer.byteLength(html.slice(0, marker.index)) : void 0;
11502
+ return {
11503
+ limitBytes: GOOGLEBOT_HTML_LIMIT_BYTES,
11504
+ bytesAfterLimit: bytes.byteLength - GOOGLEBOT_HTML_LIMIT_BYTES,
11505
+ ...marker && startBytes !== void 0 ? {
11506
+ mainContentStartBytes: startBytes,
11507
+ mainContentMarker: marker[1]?.toLowerCase()
11508
+ } : {},
11509
+ mainContentStart: startBytes === void 0 ? "not-found" : startBytes >= 2097152 ? "after-limit" : startBytes >= 2097152 * NEAR_LIMIT_SHARE ? "near-limit" : "before-limit",
11510
+ visibleWordsBeforeLimit: wordCount(text.slice(0, limitIndex)),
11511
+ visibleWordsAfterLimit: wordCount(text.slice(limitIndex))
11512
+ };
11513
+ }
11514
+ const formatBytes = (value) => value.toLocaleString("en-US");
11515
+ function largeHtmlIssue(rule, page) {
11516
+ const sizeBytes = page.sizeBytes ?? 0;
11517
+ const cut = page.googlebotCut;
11518
+ const htmlSource = page.usedJs ? "rendered" : "raw";
11519
+ const base = {
11520
+ ...rule,
11521
+ detail: `${Math.round(sizeBytes / 1024)} KB`,
11522
+ evidence: {
11523
+ sizeBytes,
11524
+ thresholdBytes: GOOGLEBOT_HTML_LIMIT_BYTES,
11525
+ htmlSource
11526
+ }
11527
+ };
11528
+ if (!cut) return base;
11529
+ const words = cut.visibleWordsBeforeLimit + cut.visibleWordsAfterLimit;
11530
+ const wordsAfterShare = words ? Math.round(cut.visibleWordsAfterLimit / words * 100) / 100 : void 0;
11531
+ const indexingRisk = htmlSource === "rendered" ? "unknown" : cut.mainContentStart === "after-limit" || cut.mainContentStart === "near-limit" || (wordsAfterShare ?? 0) > .5 ? "main-content-past-limit" : "none-found";
11532
+ const start = cut.mainContentStartBytes === void 0 ? "no <h1> or <main> found" : `main content (<${cut.mainContentMarker}>) starts at byte ${formatBytes(cut.mainContentStartBytes)}, ${cut.mainContentStart === "after-limit" ? "after" : cut.mainContentStart === "near-limit" ? "just before" : "before"} the cut`;
11533
+ const wordsNote = wordsAfterShare === void 0 ? "" : `, with about ${Math.round(wordsAfterShare * 100)}% of the visible words`;
11534
+ return {
11535
+ ...base,
11536
+ severity: indexingRisk === "main-content-past-limit" ? "high" : base.severity,
11537
+ detail: `${Math.round(sizeBytes / 1024)} KB; ${start}; ${formatBytes(cut.bytesAfterLimit)} bytes sit past Googlebot's ${formatBytes(cut.limitBytes)}-byte cut${wordsNote}.${indexingRisk === "main-content-past-limit" ? " Googlebot stops reading at the cut and indexes only what came before it (the word split is a heuristic)." : ""}`,
11538
+ evidence: {
11539
+ ...base.evidence,
11540
+ googlebotLimitBytes: cut.limitBytes,
11541
+ bytesAfterLimit: cut.bytesAfterLimit,
11542
+ mainContentStartBytes: cut.mainContentStartBytes ?? null,
11543
+ mainContentMarker: cut.mainContentMarker ?? null,
11544
+ mainContentStart: cut.mainContentStart,
11545
+ visibleWordsBeforeLimit: cut.visibleWordsBeforeLimit,
11546
+ visibleWordsAfterLimit: cut.visibleWordsAfterLimit,
11547
+ visibleWordsAfterLimitShare: wordsAfterShare ?? null,
11548
+ indexingRisk
11549
+ }
11550
+ };
11551
+ }
11552
+ const RESPONSE_CAP_ERROR = /^Page response exceeds the (\d+)-byte response limit/;
11553
+ function responseCapIssue(request) {
11554
+ if (request.outcome !== "failure") return void 0;
11555
+ const capBytes = Number(RESPONSE_CAP_ERROR.exec(request.error)?.[1]);
11556
+ if (!capBytes) return void 0;
11557
+ const rule = explainRule("large_html");
11558
+ if (!rule) throw new Error("Missing rule guidance for large_html");
11559
+ return {
11560
+ ruleId: rule.id,
11561
+ title: rule.title,
11562
+ category: rule.category,
11563
+ severity: rule.defaultSeverity,
11564
+ url: request.requestedUrl,
11565
+ detail: `Over ${Math.round(capBytes / 1024)} KB; the crawler stopped reading at its ${formatBytes(capBytes)}-byte cap, so where the main content starts is unknown.`,
11566
+ evidence: {
11567
+ dataStatus: "partial",
11568
+ sizeBytes: null,
11569
+ sizeBytesAtLeast: capBytes,
11570
+ thresholdBytes: GOOGLEBOT_HTML_LIMIT_BYTES,
11571
+ googlebotLimitBytes: GOOGLEBOT_HTML_LIMIT_BYTES,
11572
+ bytesAfterLimitAtLeast: capBytes - GOOGLEBOT_HTML_LIMIT_BYTES,
11573
+ mainContentStartBytes: null,
11574
+ mainContentStart: "unknown",
11575
+ indexingRisk: "unknown"
11576
+ }
11577
+ };
11578
+ }
11480
11579
  // packages/core/src/analyze/crawler/page-eligibility.ts
11481
11580
  function comparableUrl(value) {
11482
11581
  if (!value) return void 0;
@@ -11515,6 +11614,21 @@ function isCdnUtilityUrl(value) {
11515
11614
  return false;
11516
11615
  }
11517
11616
  }
11617
+ function globMatch(pattern, value) {
11618
+ if (!pattern.includes("*")) return value.includes(pattern);
11619
+ const parts = pattern.split("*");
11620
+ let position = 0;
11621
+ for (const part of parts) {
11622
+ if (!part) continue;
11623
+ const found = value.slice(position).indexOf(part);
11624
+ if (found === -1) return false;
11625
+ position += found + part.length;
11626
+ }
11627
+ return true;
11628
+ }
11629
+ function passesFilters(url, include, exclude) {
11630
+ return (!include.length || include.some((pattern) => globMatch(pattern, url))) && !exclude.some((pattern) => globMatch(pattern, url));
11631
+ }
11518
11632
  // packages/core/src/analyze/crawler/audit.ts
11519
11633
  function sameUrl(a, b) {
11520
11634
  if (!a || !b) return false;
@@ -11557,7 +11671,7 @@ function urlKey(value) {
11557
11671
  }
11558
11672
  }
11559
11673
  const SLOW_RESPONSE_MS = 2e3;
11560
- const LARGE_HTML_BYTES = 2097152;
11674
+ const LARGE_HTML_BYTES = GOOGLEBOT_HTML_LIMIT_BYTES;
11561
11675
  const COMPRESSION_MIN_BYTES = 4096;
11562
11676
  const DEEP_PAGE_DEPTH = 4;
11563
11677
  const WEAK_VALUABLE_INLINKS = 1;
@@ -11729,6 +11843,11 @@ function auditCrawlRequests(requests) {
11729
11843
  for (const request of requests) {
11730
11844
  if (request.outcome === "failure") {
11731
11845
  if (request.failureKind === "aborted") continue;
11846
+ const overCap = responseCapIssue(request);
11847
+ if (overCap) {
11848
+ issues.push(overCap);
11849
+ continue;
11850
+ }
11732
11851
  issues.push(requestIssue("connection_error", request, request.error, {
11733
11852
  outcome: request.outcome,
11734
11853
  failureKind: request.failureKind,
@@ -11841,10 +11960,7 @@ function auditCrawlPages(pages, opts = {}) {
11841
11960
  outgoingInternalCount: page.outgoingInternalCount,
11842
11961
  rendering: page.fetchDiagnostics?.rendering
11843
11962
  }));
11844
- if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue$1("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
11845
- sizeBytes: page.sizeBytes,
11846
- thresholdBytes: LARGE_HTML_BYTES
11847
- }));
11963
+ if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(largeHtmlIssue(issue$1("large_html", page), page));
11848
11964
  if (isHtmlPage(page) && !page.compression && (page.sizeBytes ?? 0) > COMPRESSION_MIN_BYTES) issues.push(issue$1("no_compression", page, void 0, {
11849
11965
  sizeBytes: page.sizeBytes,
11850
11966
  thresholdBytes: COMPRESSION_MIN_BYTES,
@@ -12661,7 +12777,11 @@ function refreshPrioritiesCsvFiles(report) {
12661
12777
  }
12662
12778
  // packages/core/src/analyze/crawler/crawl-url-queue.ts
12663
12779
  function before(left, right) {
12664
- return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
12780
+ if (left.depth !== right.depth) return left.depth < right.depth;
12781
+ const leftRank = left.rank ?? 0;
12782
+ const rightRank = right.rank ?? 0;
12783
+ if (leftRank !== rightRank) return leftRank < rightRank;
12784
+ return left.url < right.url;
12665
12785
  }
12666
12786
  var CrawlUrlQueue = class {
12667
12787
  #items = [];
@@ -13191,14 +13311,20 @@ function rateLimitRerunCommand(config, intervalMs) {
13191
13311
  return `seo crawl ${config.url}${scope}${sitemap} --max-pages ${config.maxPages} --concurrency 1 --fetch-interval-cap 1 --fetch-interval-ms ${slowMs}`;
13192
13312
  }
13193
13313
  /** Set when the sitemaps list more URLs than the page limit let the crawl read. */
13194
- function sitemapLimitCaveatInput(config, discovery, defaulted) {
13195
- if (!discovery || discovery.urlsReturned < config.maxPages || !discovery.roots.some((root) => root.possiblyTruncated)) return;
13314
+ function sitemapLimitCaveatInput(config, discovery, defaulted, sample) {
13315
+ if (!discovery) return void 0;
13316
+ const truncated = discovery.roots.some((root) => root.possiblyTruncated);
13317
+ if (!sample && (discovery.urlsReturned < config.maxPages || !truncated)) return;
13196
13318
  return {
13197
13319
  maxPages: config.maxPages,
13198
13320
  defaulted,
13199
13321
  ceiling: MAX_CRAWL_PAGES,
13200
13322
  health: config.strategy === "health",
13201
- url: config.url
13323
+ url: config.url,
13324
+ ...sample ? { sample: {
13325
+ ...sample,
13326
+ readCapped: truncated
13327
+ } } : {}
13202
13328
  };
13203
13329
  }
13204
13330
  function crawlCaveats(input) {
@@ -13210,8 +13336,9 @@ function crawlCaveats(input) {
13210
13336
  caveats.push(`${CRAWL_COVERAGE_CAVEAT_PREFIX} read ${readPages} of ${readPages + skippedUrls} pages because the origin kept rate limiting the crawl (HTTP 429) after it backed off. Rerun slower: ${rerunCommand}`);
13211
13337
  }
13212
13338
  if (input.sitemapLimit) {
13213
- const { maxPages, defaulted, ceiling, health, url } = input.sitemapLimit;
13214
- caveats.push(`Checked only the first ${maxPages} sitemap URLs because the page limit is ${maxPages}${defaulted ? " by default" : ""}, and the sitemaps list more. Raise --max-pages, up to ${ceiling.toLocaleString("en-US")}, to check more: seo crawl ${url}${health ? " --health" : ""} --max-pages ${ceiling}`);
13339
+ const { maxPages, defaulted, ceiling, health, url, sample } = input.sitemapLimit;
13340
+ const rerun = `Raise --max-pages, up to ${ceiling.toLocaleString("en-US")}, to check more: seo crawl ${url}${health ? " --health" : ""} --max-pages ${ceiling}`;
13341
+ caveats.push(sample ? `Checked a sample of ${sample.checked.toLocaleString("en-US")} of the ${sample.read.toLocaleString("en-US")}${sample.readCapped ? "+" : ""} sitemap URLs read because the page limit is ${maxPages}${defaulted ? " by default" : ""}. The sample takes URLs in turn from each of ${sample.sections} sections (the first path segment), shortest paths first, so no one section fills the crawl.${sample.readCapped ? " The sitemaps list more URLs than the crawl read." : ""} ${rerun}` : `Checked only the first ${maxPages} sitemap URLs because the page limit is ${maxPages}${defaulted ? " by default" : ""}, and the sitemaps list more. ${rerun}`);
13215
13342
  }
13216
13343
  if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
13217
13344
  const renderingFallbackUrls = input.renderingFallbackUrls ?? 0;
@@ -14057,6 +14184,20 @@ const crawlPageSnapshotSchema = z.object({
14057
14184
  responseHeaders: z.record(z.string(), z.string()).optional(),
14058
14185
  responseTimeMs: z.number().optional(),
14059
14186
  sizeBytes: z.number().int().optional(),
14187
+ googlebotCut: z.object({
14188
+ limitBytes: z.number().int().positive(),
14189
+ bytesAfterLimit: z.number().int().nonnegative(),
14190
+ mainContentStartBytes: z.number().int().nonnegative().optional(),
14191
+ mainContentMarker: z.enum(["h1", "main"]).optional(),
14192
+ mainContentStart: z.enum([
14193
+ "before-limit",
14194
+ "near-limit",
14195
+ "after-limit",
14196
+ "not-found"
14197
+ ]),
14198
+ visibleWordsBeforeLimit: z.number().int().nonnegative(),
14199
+ visibleWordsAfterLimit: z.number().int().nonnegative()
14200
+ }).optional(),
14060
14201
  usedJs: z.boolean().optional(),
14061
14202
  fetchSource: z.enum([
14062
14203
  "cache",
@@ -15551,6 +15692,7 @@ async function snapshotFetchedPage(url, fetched) {
15551
15692
  responseTimeMs: fetched.diagnostics.durationMs,
15552
15693
  sizeBytes: Buffer.byteLength(fetched.html),
15553
15694
  usedJs: fetched.usedJs,
15695
+ googlebotCut: googlebotCutEvidence(fetched.html),
15554
15696
  fetchSource: fetched.diagnostics.source,
15555
15697
  cacheState: fetched.diagnostics.cache,
15556
15698
  fetchDiagnostics: fetched.diagnostics,
@@ -17996,6 +18138,74 @@ function sitemapDiscoveryStatus(roots) {
17996
18138
  if (!roots.some((root) => root.dataStatus !== "unavailable")) return "unavailable";
17997
18139
  return roots.every((root) => root.dataStatus === "complete") ? "complete" : "partial";
17998
18140
  }
18141
+ /** How many sitemap URLs a crawl reads to pick its sample from. */
18142
+ function sitemapReadLimit(maxPages) {
18143
+ return Math.min(5e4, Math.max(1e4, maxPages * 10));
18144
+ }
18145
+ function sectionOf(url) {
18146
+ try {
18147
+ return new URL(url).pathname.split("/")[1]?.toLowerCase() ?? "";
18148
+ } catch {
18149
+ return "";
18150
+ }
18151
+ }
18152
+ function pathDepth(url) {
18153
+ try {
18154
+ return new URL(url).pathname.split("/").filter(Boolean).length;
18155
+ } catch {
18156
+ return 0;
18157
+ }
18158
+ }
18159
+ /**
18160
+ * Picks up to `limit` URLs round-robin across sections (the first path
18161
+ * segment), so one large section such as a forum cannot fill a capped crawl.
18162
+ * Within a section, shallower paths come first, then URL order.
18163
+ */
18164
+ function spreadSitemapSample(urls, limit) {
18165
+ const groups = /* @__PURE__ */ new Map();
18166
+ for (const url of urls) {
18167
+ const key = sectionOf(url);
18168
+ const group = groups.get(key);
18169
+ if (group) group.push(url);
18170
+ else groups.set(key, [url]);
18171
+ }
18172
+ const ordered = [...groups.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, group]) => group.map((url) => ({
18173
+ url,
18174
+ depth: pathDepth(url)
18175
+ })).sort((left, right) => left.depth - right.depth || (left.url < right.url ? -1 : left.url > right.url ? 1 : 0)).map((entry) => entry.url));
18176
+ const picked = [];
18177
+ for (let index = 0; picked.length < limit; index += 1) {
18178
+ let added = false;
18179
+ for (const group of ordered) {
18180
+ const url = group[index];
18181
+ if (url === void 0) continue;
18182
+ added = true;
18183
+ picked.push(url);
18184
+ if (picked.length >= limit) break;
18185
+ }
18186
+ if (!added) break;
18187
+ }
18188
+ return {
18189
+ urls: picked,
18190
+ sections: groups.size
18191
+ };
18192
+ }
18193
+ /**
18194
+ * The sitemap URLs a capped crawl checks, leaving room for the seed URLs, and
18195
+ * how they were picked when the sitemaps listed more.
18196
+ */
18197
+ function sitemapCrawlSample(urls, maxPages, seedUrls, normalize) {
18198
+ const candidates = urls.filter((url) => !seedUrls.has(normalize(url) ?? url));
18199
+ const sample = spreadSitemapSample(candidates, Math.max(0, maxPages - seedUrls.size));
18200
+ return candidates.length > sample.urls.length ? {
18201
+ urls: sample.urls,
18202
+ caveat: {
18203
+ checked: sample.urls.length,
18204
+ read: candidates.length,
18205
+ sections: sample.sections
18206
+ }
18207
+ } : { urls: sample.urls };
18208
+ }
17999
18209
  async function sitemapSeeds(input) {
18000
18210
  const roots = [];
18001
18211
  const urls = /* @__PURE__ */ new Set();
@@ -18075,21 +18285,6 @@ function isLikelyAsset(url) {
18075
18285
  const path = new URL(url).pathname.toLowerCase();
18076
18286
  return /\.(avif|bmp|css|csv|dmg|eot|gif|gz|ico|jpeg|jpg|js|json|map|mov|mp3|mp4|ogg|otf|pdf|png|svg|tar|ttf|wav|webm|webp|woff|woff2|zip)$/.test(path);
18077
18287
  }
18078
- function globMatch(pattern, value) {
18079
- if (!pattern.includes("*")) return value.includes(pattern);
18080
- const parts = pattern.split("*");
18081
- let position = 0;
18082
- for (const part of parts) {
18083
- if (!part) continue;
18084
- const found = value.slice(position).indexOf(part);
18085
- if (found === -1) return false;
18086
- position += found + part.length;
18087
- }
18088
- return true;
18089
- }
18090
- function passesFilters(url, include, exclude) {
18091
- return (!include.length || include.some((pattern) => globMatch(pattern, url))) && !exclude.some((pattern) => globMatch(pattern, url));
18092
- }
18093
18288
  async function crawlSite(input, dependencies) {
18094
18289
  const deps = resolveCrawlSiteDependencies(dependencies);
18095
18290
  const config = assertCrawlConfigLimits(normalizeCrawlConfig(input));
@@ -18144,6 +18339,7 @@ async function crawlSite(input, dependencies) {
18144
18339
  });
18145
18340
  };
18146
18341
  let sitemapDiscovery;
18342
+ let sitemapSample;
18147
18343
  let externalLinkVerification;
18148
18344
  let statusEventChain = Promise.resolve();
18149
18345
  const emitStatus = (phase, event = {}) => {
@@ -18191,7 +18387,7 @@ async function crawlSite(input, dependencies) {
18191
18387
  const llmsTxt = aiSignals?.signals.llmsTxt;
18192
18388
  const healthSitemapUrls = healthStrategy && !config.sitemapUrl && !isCancelled() ? await robotsSession?.sitemapUrls(origin) : void 0;
18193
18389
  cancelled = isCancelled();
18194
- const enqueue = (value, depth) => {
18390
+ const enqueue = (value, depth, rank) => {
18195
18391
  const normalized = normalizeUrl$1(value, config.url);
18196
18392
  if (!normalized) {
18197
18393
  recordSkip("invalid-url");
@@ -18277,7 +18473,8 @@ async function crawlSite(input, dependencies) {
18277
18473
  queuedUrls += 1;
18278
18474
  queue.push({
18279
18475
  url: normalized,
18280
- depth
18476
+ depth,
18477
+ rank
18281
18478
  });
18282
18479
  emitStatus("url_queued", {
18283
18480
  url: normalized,
@@ -18297,12 +18494,16 @@ async function crawlSite(input, dependencies) {
18297
18494
  const sitemap = await sitemapSeeds({
18298
18495
  url: config.url,
18299
18496
  sitemapUrl: config.sitemapUrl,
18300
- maxPages: config.maxPages,
18497
+ maxPages: sitemapReadLimit(config.maxPages),
18301
18498
  declaredSitemapUrls: aiSignals?.signals.robotsTxt?.sitemapUrls ?? healthSitemapUrls ?? [],
18302
18499
  warnings,
18303
18500
  fetchSitemapUrls: deps.fetchSitemapUrls
18304
18501
  });
18305
- for (const url of sitemap.urls) enqueue(url, 0);
18502
+ const sample = sitemapCrawlSample(sitemap.urls, config.maxPages, seedUrls, (url) => normalizeUrl$1(url, config.url));
18503
+ sitemapSample = sample.caveat;
18504
+ sample.urls.forEach((url, index) => {
18505
+ enqueue(url, 0, index + 1);
18506
+ });
18306
18507
  sitemapDiscovery = sitemap.discovery;
18307
18508
  }
18308
18509
  checkMemoryPressure();
@@ -18463,7 +18664,7 @@ async function crawlSite(input, dependencies) {
18463
18664
  });
18464
18665
  continue;
18465
18666
  }
18466
- const page = result.page;
18667
+ const page = structuredClone(result.page);
18467
18668
  const rendering = page.fetchDiagnostics?.rendering;
18468
18669
  if (rendering?.status === "unavailable" && config.js === "on") throw new SeoError("INVALID_INPUT", `--rendering on needs a local Chrome or Chromium browser, and none could start. ${rendering.error ?? ""} Install Chromium, or set SEO_BROWSER_EXECUTABLE_PATH to a Chromium executable, then rerun. Use --rendering auto to accept raw HTML.`.replace(/\s+/g, " ").trim());
18469
18670
  if (rendering?.status === "unavailable" || rendering?.status === "failed") renderingFallbackUrls += 1;
@@ -18733,7 +18934,7 @@ async function crawlSite(input, dependencies) {
18733
18934
  queueSafetyInventoryCapped,
18734
18935
  originBackpressureSkippedUrls,
18735
18936
  rateLimit: rateLimit.caveatInput(config, pages.length),
18736
- sitemapLimit: sitemapLimitCaveatInput(config, sitemapDiscovery, input.maxPages === void 0),
18937
+ sitemapLimit: sitemapLimitCaveatInput(config, sitemapDiscovery, input.maxPages === void 0, sitemapSample),
18737
18938
  memoryPressureSkippedUrls,
18738
18939
  queryVariantSkippedUrls: queryVariantSkipped.size,
18739
18940
  renderingFallbackUrls,
@@ -31198,6 +31399,12 @@ function plainFor(word) {
31198
31399
  for (const [key, value] of Object.entries(PLAIN)) if (lower.startsWith(key.slice(0, Math.max(4, key.length - 2)))) return value;
31199
31400
  return "a plain word";
31200
31401
  }
31402
+ function swapFix(word) {
31403
+ const plain = plainFor(word).replace(/\s*\([^)]*\)/g, "");
31404
+ if (plain === "a plain word") return "Use a plain word.";
31405
+ if (/^(?:say|name|give)\b/.test(plain)) return `Cut "${word}", or swap it for a detail the text already gives.`;
31406
+ return `Use ${plain.split(", ").map((swap) => `"${swap}"`).join(" or ")}.`;
31407
+ }
31201
31408
  const PHRASE_RULES = [
31202
31409
  {
31203
31410
  id: "chatbot-residue",
@@ -31356,7 +31563,7 @@ const PHRASE_RULES = [
31356
31563
  /\bstunning(?:ly)?\b|\benchanting\b/gi
31357
31564
  ],
31358
31565
  message: (match) => `"${match.text}" is far more common in AI text than in human writing.`,
31359
- fix: (match) => `Use ${plainFor(match.text.replace(/^(?:rich|a) /i, ""))}.`
31566
+ fix: (match) => swapFix(match.text.replace(/^(?:rich|a) /i, ""))
31360
31567
  },
31361
31568
  {
31362
31569
  id: "banned-word",
@@ -31376,7 +31583,7 @@ const PHRASE_RULES = [
31376
31583
  /\bgets? you to the next fix\b/gi
31377
31584
  ],
31378
31585
  message: (match) => `"${match.text}" is on the house list of words to cut.`,
31379
- fix: (match) => /source of truth/i.test(match.text) ? "Name the actual file, field or result." : /hype|everything you need|next fix/i.test(match.text) ? "Say what the page or tool actually gives the reader." : `Use ${plainFor(match.text)}.`
31586
+ fix: (match) => /source of truth/i.test(match.text) ? "Name the actual file, field or result." : /hype|everything you need|next fix/i.test(match.text) ? "Say what the page or tool actually gives the reader." : swapFix(match.text)
31380
31587
  },
31381
31588
  {
31382
31589
  id: "buzzword",
@@ -31621,7 +31828,7 @@ const PHRASE_RULES = [
31621
31828
  /(?:^|[.!?:]\s+)(?:But|And) only (?:part|half|some|partly)\.(?=\s|$)/g
31622
31829
  ],
31623
31830
  message: "A two-word denial used for rhythm. It makes the reader wait.",
31624
- fix: "Join it to the sentence before, such as \"Nothing technical was holding these pages back.\""
31831
+ fix: "Join it to the sentence before, so the point is stated once."
31625
31832
  },
31626
31833
  {
31627
31834
  id: "strawman",
@@ -32419,10 +32626,10 @@ function stockCloser(ctx) {
32419
32626
  for (const block of ctx.blocks(RHYTHM_KINDS)) for (const sentence of ctx.sentences(block)) {
32420
32627
  const text = sentence.text;
32421
32628
  if (CLOSERS.some((re) => re.test(text))) {
32422
- ctx.hit(rule, block, sentence.start, sentence.end, "A stock kicker that tells the reader how to feel about the last point.", "Cut it. If the point needs support, give the number or fact that makes it true.");
32629
+ ctx.hit(rule, block, sentence.start, sentence.end, "A stock kicker that tells the reader how to feel about the last point.", "Cut it. If the text already has the number or fact that makes the point, end on that.");
32423
32630
  continue;
32424
32631
  }
32425
- if (/^(?:but |and )?(?:that|this)(?:'s| is) not (?:how|what|why|where|a|an|the)\b/i.test(text) && words$2(text).length <= 10) ctx.hit(reframe, block, sentence.start, sentence.end, "A short \"That's not ...\" line dismisses a view instead of stating one.", "Say what is true instead, such as \"Agents can do this work in a day.\"");
32632
+ if (/^(?:but |and )?(?:that|this)(?:'s| is) not (?:how|what|why|where|a|an|the)\b/i.test(text) && words$2(text).length <= 10) ctx.hit(reframe, block, sentence.start, sentence.end, "A short \"That's not ...\" line dismisses a view instead of stating one.", "Cut it, or say what is true with a fact the text already gives.");
32426
32633
  }
32427
32634
  }
32428
32635
  const FIGURATIVE_NOUNS = "fights?|battles?|hills?|wars?|bets?|gambles?|races?|risks?|horses?|roads?|bridges?|doors?|games?|ships?|cards?";
@@ -33212,15 +33419,24 @@ async function ahrefs(key, input, missed, totals) {
33212
33419
  order_by: "refdomains_target:desc"
33213
33420
  })).pages ?? [];
33214
33421
  if (input.action === "serp") return eachKeyword(input.keywords.slice(0, serpKeywords(input)), missed, async (keyword) => {
33215
- return ((await get("serp-overview/serp-overview", {
33422
+ const data = await get("serp-overview/serp-overview", {
33216
33423
  select: "position,type,url,title,domain_rating,traffic,keywords",
33217
33424
  keyword,
33218
33425
  country: input.country,
33219
33426
  top_positions: String(serpDepth(input))
33220
- })).positions ?? []).map((row) => ({
33221
- keyword,
33222
- ...row
33223
- }));
33427
+ });
33428
+ let organic = 0;
33429
+ return (data.positions ?? []).map((row) => {
33430
+ const { position, type, ...rest } = row;
33431
+ const kinds = Array.isArray(type) ? type.map(String) : [String(type ?? "")];
33432
+ return {
33433
+ keyword,
33434
+ position: kinds.includes("organic") ? ++organic : null,
33435
+ ahrefs_position: position ?? null,
33436
+ type: kinds.join(",") || null,
33437
+ ...rest
33438
+ };
33439
+ });
33224
33440
  });
33225
33441
  if (input.action === "backlinks") return (await get("site-explorer/refdomains", {
33226
33442
  select: "domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen",
@@ -33248,7 +33464,55 @@ function flattenIntents(row) {
33248
33464
  intents: names.join(" ") || null
33249
33465
  };
33250
33466
  }
33467
+ // packages/core/src/commands/research-serp.ts
33468
+ const GOTO = /^https?:\/\/(www\.)?google\.[a-z.]+\/goto\b/;
33469
+ const hostOf$3 = (url) => {
33470
+ try {
33471
+ return new URL(url).hostname;
33472
+ } catch {
33473
+ return "";
33474
+ }
33475
+ };
33476
+ const text = (value) => typeof value === "string" && value ? value : null;
33477
+ function dataForSeoSerp(keyword, items) {
33478
+ const found = items.find((item) => item.type === "ai_overview");
33479
+ const overview = found ? {
33480
+ keyword,
33481
+ references: (found.references ?? []).slice(0, 20).map((ref) => {
33482
+ const url = String(ref.url ?? "");
33483
+ const goto = GOTO.test(url);
33484
+ return {
33485
+ domain: goto ? null : text(ref.domain) ?? (hostOf$3(url) || null),
33486
+ url: goto || !url ? null : url,
33487
+ title: text(ref.title)
33488
+ };
33489
+ })
33490
+ } : null;
33491
+ return {
33492
+ rows: items.filter((item) => item.type === "organic" || item.type === "featured_snippet").map((item) => {
33493
+ const row = {
33494
+ keyword,
33495
+ position: item.type === "organic" ? Number(item.rank_group) : null,
33496
+ rank_absolute: Number(item.rank_absolute),
33497
+ type: String(item.type),
33498
+ url: text(item.url),
33499
+ domain: text(item.domain),
33500
+ title: text(item.title)
33501
+ };
33502
+ if (GOTO.test(String(row.url))) {
33503
+ const crumb = String(item.breadcrumb ?? "");
33504
+ const site = /^https?:\/\/([^/\s›]+)/.exec(crumb);
33505
+ row.url = null;
33506
+ row.domain = site?.[1] ?? null;
33507
+ row.url_hint = site ? crumb.replace(/^https?:\/\//, "") : null;
33508
+ }
33509
+ return row;
33510
+ }),
33511
+ overview
33512
+ };
33513
+ }
33251
33514
  // packages/core/src/commands/research-providers.ts
33515
+ const RESEARCH_DEVICES = ["desktop", "mobile"];
33252
33516
  const RESEARCH_SCOPES = [
33253
33517
  "domain",
33254
33518
  "prefix",
@@ -33372,7 +33636,25 @@ async function dataForSeoList([login, password], path) {
33372
33636
  if (!task || task.status_code >= 4e4) throw new CommandError("PROVIDER_ERROR", `dataforseo: no list at ${path}`);
33373
33637
  return task.result ?? [];
33374
33638
  }
33375
- async function dataForSeoTask([login, password], path, task) {
33639
+ async function dataForSeoTask(keys, path, task) {
33640
+ let spent = 0;
33641
+ for (let attempt = 1;; attempt += 1) try {
33642
+ const done = await dataForSeoTaskOnce(keys, path, task);
33643
+ return {
33644
+ ...done,
33645
+ cost: done.cost + spent
33646
+ };
33647
+ } catch (error) {
33648
+ const failed = error;
33649
+ spent += failed.cost ?? 0;
33650
+ if (!SE_ERRORS.has(failed.status ?? 0) || attempt >= SE_TRIES) throw Object.assign(error, {
33651
+ cost: spent,
33652
+ ...attempt > 1 ? { tries: attempt } : {}
33653
+ });
33654
+ await wait(SE_RETRY_MS[attempt - 1] ?? 3e3);
33655
+ }
33656
+ }
33657
+ async function dataForSeoTaskOnce([login, password], path, task) {
33376
33658
  const result = (await readJson(await call("dataforseo", new Request(`https://api.dataforseo.com/${path}`, {
33377
33659
  method: "POST",
33378
33660
  headers: {
@@ -33387,7 +33669,10 @@ async function dataForSeoTask([login, password], path, task) {
33387
33669
  partial: false,
33388
33670
  noResults: true
33389
33671
  };
33390
- if (!result || result.status_code >= 4e4 && result.status_code !== PARTIAL) throw Object.assign(new CommandError("PROVIDER_ERROR", `dataforseo: ${result?.status_message ?? "no task result"}`), { cost: result?.cost ?? 0 });
33672
+ if (!result || result.status_code >= 4e4 && result.status_code !== PARTIAL) throw Object.assign(new CommandError("PROVIDER_ERROR", `dataforseo: ${result?.status_message ?? "no task result"}`), {
33673
+ cost: result?.cost ?? 0,
33674
+ status: result?.status_code ?? 0
33675
+ });
33391
33676
  return {
33392
33677
  cost: result.cost ?? 0,
33393
33678
  result: result.result?.[0] ?? null,
@@ -33396,6 +33681,9 @@ async function dataForSeoTask([login, password], path, task) {
33396
33681
  }
33397
33682
  const NO_RESULTS = 40102;
33398
33683
  const PARTIAL = 40106;
33684
+ const SE_ERRORS = /* @__PURE__ */ new Set([40101, 5e4]);
33685
+ const SE_TRIES = 3;
33686
+ const SE_RETRY_MS = [1e3, 3e3];
33399
33687
  const queued = (task) => ({
33400
33688
  id: String(task.id ?? ""),
33401
33689
  status_code: Number(task.status_code ?? 0),
@@ -33435,6 +33723,7 @@ async function dataForSeo(keys, input) {
33435
33723
  noData: withoutData(input, rows, spent.missed, 700),
33436
33724
  ...spent.available !== void 0 ? { available: spent.available } : {},
33437
33725
  ...spent.siteTotal ? { siteTotal: spent.siteTotal } : {},
33726
+ ...spent.aiOverviews ? { aiOverviews: spent.aiOverviews } : {},
33438
33727
  ...spent.rankFailed ? { rankFailed: true } : {}
33439
33728
  };
33440
33729
  }
@@ -33523,7 +33812,9 @@ async function dataForSeoRows(keys, input, spent) {
33523
33812
  }
33524
33813
  return items.map((item) => pick(item, {
33525
33814
  keyword: "keyword_data.keyword",
33526
- position: "ranked_serp_element.serp_item.rank_absolute",
33815
+ position: "ranked_serp_element.serp_item.rank_group",
33816
+ rank_absolute: "ranked_serp_element.serp_item.rank_absolute",
33817
+ type: "ranked_serp_element.serp_item.type",
33527
33818
  url: "ranked_serp_element.serp_item.url",
33528
33819
  search_volume: "keyword_data.keyword_info.search_volume",
33529
33820
  etv: "ranked_serp_element.serp_item.etv",
@@ -33533,28 +33824,13 @@ async function dataForSeoRows(keys, input, spent) {
33533
33824
  }
33534
33825
  if (input.action === "serp") {
33535
33826
  const rows = await eachKeyword(input.keywords.slice(0, serpKeywords(input)), spent.missed, async (keyword) => {
33536
- return (await post("v3/serp/google/organic/live/advanced", {
33827
+ const { rows, overview } = dataForSeoSerp(keyword, await post("v3/serp/google/organic/live/advanced", {
33537
33828
  keyword,
33538
- depth: serpDepth(input)
33539
- }, 2)).filter((item) => item.type === "organic").map((item) => {
33540
- const row = pick(item, {
33541
- position: "rank_absolute",
33542
- url: "url",
33543
- domain: "domain",
33544
- title: "title"
33545
- });
33546
- if (/^https?:\/\/(www\.)?google\.[a-z.]+\/goto\b/.test(String(row.url))) {
33547
- const crumb = String(item.breadcrumb ?? "");
33548
- const site = /^https?:\/\/([^/\s›]+)/.exec(crumb);
33549
- row.url = null;
33550
- row.domain = site?.[1] ?? null;
33551
- row.url_hint = site ? crumb.replace(/^https?:\/\//, "") : null;
33552
- }
33553
- return {
33554
- keyword,
33555
- ...row
33556
- };
33557
- });
33829
+ depth: serpDepth(input),
33830
+ device: input.device ?? "desktop"
33831
+ }, 2));
33832
+ if (overview) spent.aiOverviews = [...spent.aiOverviews ?? [], overview];
33833
+ return rows;
33558
33834
  });
33559
33835
  return input.noRank ? rows : withDomainRank(keys, rows, spent);
33560
33836
  }
@@ -33672,7 +33948,7 @@ async function serpOverlap(provider, headers, input) {
33672
33948
  const keywords = [...new Set(input.keywords.slice(0, OVERLAP_KEYWORDS))].filter((keyword) => !serps.missed.includes(keyword) && !serps.noData.includes(keyword));
33673
33949
  const host = (value) => String(value ?? "").toLowerCase().replace(/^www\./, "");
33674
33950
  const top = new Map(keywords.map((keyword) => {
33675
- const rows = serps.rows.filter((row) => row.keyword === keyword).slice(0, OVERLAP_DEPTH);
33951
+ const rows = serps.rows.filter((row) => row.keyword === keyword && row.type !== "featured_snippet").slice(0, OVERLAP_DEPTH);
33676
33952
  const pages = new Set(rows.map((row) => row.url).filter((url) => typeof url === "string" && url !== "").map((url) => url.replace(/\/$/, "").toLowerCase()));
33677
33953
  const sites = new Set(rows.map((row) => host(row.domain ?? siteOf(row.url))).filter(Boolean));
33678
33954
  return [keyword, {
@@ -37779,6 +38055,121 @@ async function runBingReport(input, deps) {
37779
38055
  data: report
37780
38056
  };
37781
38057
  }
38058
+ const DEVICES = ["desktop", "mobile"];
38059
+ const DEPTH = 10;
38060
+ const bare = (host) => host.toLowerCase().replace(/^www\./, "");
38061
+ function key(url) {
38062
+ try {
38063
+ const parsed = new URL(url);
38064
+ return `${bare(parsed.hostname)}${parsed.pathname.replace(/\/+$/, "")}`.toLowerCase();
38065
+ } catch {
38066
+ return "";
38067
+ }
38068
+ }
38069
+ function hintKey(hint) {
38070
+ const parts = hint.split("›").map((part) => part.trim());
38071
+ if (!parts[0] || parts.some((part) => /^\.\.\.$|…/.test(part))) return null;
38072
+ return `${bare(parts[0])}${parts.length > 1 ? `/${parts.slice(1).join("/")}` : ""}`.toLowerCase();
38073
+ }
38074
+ function phraseFrom(text, words = 2) {
38075
+ const cleaned = (text ?? "").split(/\s+[|\u2013\u2014-]\s+/)[0]?.replace(/["“”]/g, "").trim();
38076
+ if (!cleaned || cleaned.split(/\s+/).length < words) return null;
38077
+ return cleaned.split(/\s+/).slice(0, 12).join(" ");
38078
+ }
38079
+ async function runIndexCheck(input, headers) {
38080
+ let target;
38081
+ try {
38082
+ target = new URL(/^https?:\/\//i.test(input.url) ? input.url : `https://${input.url}`);
38083
+ } catch {
38084
+ throw new CommandError("INVALID_INPUT", "index_check needs a page URL as target.");
38085
+ }
38086
+ const keys = dataForSeoKeys(headers);
38087
+ const country = input.country ?? "us";
38088
+ const market = dataForSeoMarket(country, dataForSeoLanguage(country));
38089
+ const page = key(target.href);
38090
+ const host = bare(target.hostname);
38091
+ let phrases = (input.phrases ?? []).slice(0, 2);
38092
+ let phraseSource = phrases.length ? "given" : null;
38093
+ if (!phrases.length) {
38094
+ const live = await liveCheck(target.href).catch(() => null);
38095
+ const sentence = (live?.last?.description ?? "").split(/(?<=[.!?])\s/)[0] ?? "";
38096
+ const found = [phraseFrom(live?.last?.h1 ?? null) ?? phraseFrom(live?.last?.title ?? null), phraseFrom(sentence.replace(/[.!?]$/, ""), 5)].filter((phrase) => Boolean(phrase));
38097
+ phrases = [...new Set(found)].slice(0, 2);
38098
+ phraseSource = phrases.length ? "the page’s H1 or title and its meta description" : null;
38099
+ }
38100
+ const queries = [`site:${host}${target.pathname === "/" ? "" : target.pathname.replace(/\/+$/, "")}`, ...phrases.map((phrase) => `"${phrase.replace(/"/g, "")}"`)];
38101
+ const rows = [];
38102
+ let cost = 0;
38103
+ for (const query of queries) for (const device of DEVICES) {
38104
+ const row = {
38105
+ query,
38106
+ device,
38107
+ result: "empty",
38108
+ position: null,
38109
+ rank_absolute: null,
38110
+ results: 0,
38111
+ top_domains: "",
38112
+ error: null
38113
+ };
38114
+ try {
38115
+ const done = await dataForSeoTask(keys, "v3/serp/google/organic/live/advanced", {
38116
+ ...market,
38117
+ keyword: query,
38118
+ depth: DEPTH,
38119
+ device
38120
+ });
38121
+ cost += done.cost;
38122
+ const results = dataForSeoSerp(query, done.result?.items ?? []).rows;
38123
+ row.results = results.length;
38124
+ row.top_domains = [...new Set(results.map((r) => String(r.domain ?? "?")))].slice(0, 3).join(", ");
38125
+ const onSite = results.filter((r) => {
38126
+ const domain = bare(String(r.domain ?? ""));
38127
+ return domain === host || domain.endsWith(`.${host}`);
38128
+ });
38129
+ const match = onSite.find((r) => typeof r.url === "string" && key(r.url) === page || typeof r.url_hint === "string" && hintKey(r.url_hint) === page);
38130
+ if (match) {
38131
+ row.result = "page";
38132
+ row.position = typeof match.position === "number" ? match.position : null;
38133
+ row.rank_absolute = Number(match.rank_absolute) || null;
38134
+ } else if (onSite.some((r) => r.url === null && (typeof r.url_hint !== "string" || hintKey(r.url_hint) === null || hintKey(r.url_hint) === host))) row.result = "unresolved";
38135
+ else if (onSite.length) row.result = "other_pages";
38136
+ else if (results.length) row.result = query.startsWith("site:") ? "fallback" : "other_sites";
38137
+ } catch (error) {
38138
+ if (error instanceof CommandError && error.code === "NO_KEY") throw error;
38139
+ cost += error.cost ?? 0;
38140
+ row.result = "error";
38141
+ row.error = error.message;
38142
+ }
38143
+ rows.push(row);
38144
+ }
38145
+ const has = (result) => rows.some((row) => row.result === result);
38146
+ const verdict = has("page") ? "indexed" : has("error") || has("unresolved") ? "unknown" : "not_found";
38147
+ const reason = verdict === "indexed" ? `Google returned the page itself for ${rows.filter((row) => row.result === "page").length} of ${rows.length} searches.` : verdict === "unknown" ? has("error") ? `${rows.filter((row) => row.result === "error").length} of ${rows.length} searches failed after retries, and none of the others returned the page. Ask again later.` : "Google returned results from the site, but DataForSEO gave Google redirect links whose page it could not resolve, so they may or may not be this page." : `None of ${rows.length} searches returned the page.${has("fallback") ? " Where the site: search showed other sites, Google had dropped the filter because nothing matched; those are not rankings." : ""} It may be missing from the index, or indexed without this text. Check it with URL Inspection (seo page or seo index) if the site is yours.`;
38148
+ const usd = Math.round(cost * 1e4) / 1e4;
38149
+ return {
38150
+ text: [
38151
+ `Index check for ${target.href} (${country}, desktop and mobile): ${verdict.replace("_", " ")}.`,
38152
+ reason,
38153
+ phraseSource ? `Exact phrase from ${phraseSource}.` : "No exact phrase: none was given and the page’s H1 and title could not be read, so only site: was asked.",
38154
+ "",
38155
+ ...rows.map((row) => `${row.device.padEnd(7)} ${row.result}${row.position !== null ? ` (organic ${row.position}, absolute ${row.rank_absolute})` : ""} ${row.query}${row.results ? ` [${row.results} results: ${row.top_domains}]` : ""}${row.error ? ` ${row.error}` : ""}`),
38156
+ "",
38157
+ "This asks Google search, not Search Console. Google can index a page and still not show it for a site: or phrase search, so not found is evidence, not proof.",
38158
+ `DataForSEO cost: $${usd}.`
38159
+ ].join("\n"),
38160
+ data: {
38161
+ provider: "dataforseo",
38162
+ action: "index_check",
38163
+ url: target.href,
38164
+ country,
38165
+ verdict,
38166
+ reason,
38167
+ phrases,
38168
+ checks: rows,
38169
+ costUsd: usd
38170
+ }
38171
+ };
38172
+ }
37782
38173
  // packages/core/src/commands/research-cache.ts
37783
38174
  const RESEARCH_TTL_SECONDS = 86400;
37784
38175
  function workersResearchStore() {
@@ -37811,6 +38202,46 @@ async function saveResearch(store, key, saved) {
37811
38202
  if (!store) return;
37812
38203
  await store.put(key, JSON.stringify(saved), RESEARCH_TTL_SECONDS).catch(() => void 0);
37813
38204
  }
38205
+ // packages/core/src/commands/research-installed.ts
38206
+ async function installedSerpResearch(provider, input, serp) {
38207
+ if (!serp) throw new CommandError("INVALID_INPUT", `${provider} is not a built-in provider. Here the providers are dataforseo and ahrefs; installed provider packages run only in the seo CLI and the local MCP server (seo mcp serve).`);
38208
+ if (input.keywords.length === 0) throw new CommandError("INVALID_INPUT", "serp needs keywords.");
38209
+ const keywords = input.keywords.slice(0, serpKeywords(input));
38210
+ const language = dataForSeoMarket(input.country, null).language_code;
38211
+ const missed = [];
38212
+ const warnings = [];
38213
+ let costMicros = 0;
38214
+ let partial = false;
38215
+ let depth = serpDepth(input);
38216
+ const rows = await eachKeyword(keywords, missed, async (keyword) => {
38217
+ const got = await serp({
38218
+ provider,
38219
+ keyword,
38220
+ country: input.country,
38221
+ language,
38222
+ device: input.device ?? "desktop",
38223
+ depth: serpDepth(input)
38224
+ });
38225
+ depth = Math.min(depth, got.depth);
38226
+ costMicros = costMicros === null || got.costMicros === null ? null : costMicros + got.costMicros;
38227
+ partial ||= got.partial;
38228
+ warnings.push(...got.warnings.map((warning) => `${keyword}: ${warning}`));
38229
+ return got.rows.map((row) => ({
38230
+ keyword,
38231
+ ...row
38232
+ }));
38233
+ });
38234
+ const answered = new Set(rows.map((row) => row.keyword));
38235
+ if (depth < serpDepth(input)) warnings.unshift(`The package reads at most ${depth} results a keyword, so this read ${depth} of the ${serpDepth(input)} asked for.`);
38236
+ return {
38237
+ rows,
38238
+ costUsd: costMicros === null ? null : Number((costMicros / 1e6).toFixed(6)),
38239
+ partial,
38240
+ missed,
38241
+ noData: keywords.filter((keyword) => !missed.includes(keyword) && !answered.has(keyword)),
38242
+ warnings
38243
+ };
38244
+ }
37814
38245
  // packages/core/src/commands/research.ts
37815
38246
  const RESEARCH_ACTIONS = [
37816
38247
  "keyword_metrics",
@@ -37823,10 +38254,11 @@ const RESEARCH_ACTIONS = [
37823
38254
  "top_pages",
37824
38255
  "pages_by_backlinks",
37825
38256
  "keyword_ideas",
37826
- "serp_overlap"
38257
+ "serp_overlap",
38258
+ "index_check"
37827
38259
  ];
37828
38260
  const RESEARCH_PROVIDERS = ["dataforseo", "ahrefs"];
37829
- const RESEARCH_HELP = `provider is optional: without it the call uses the saved key, DataForSEO when both are saved. The target is a domain (the whole site), a path ending in / such as example.com/blog/ (every URL under it), or one page; scope overrides that. keyword_metrics (keywords: volume, difficulty, CPC), keyword_ideas (up to 5 seed keywords: searches that contain each seed, by volume), ranked_keywords (target: keywords a site, subfolder or page ranks for, with the provider's traffic total for the whole target), top_pages (target: the pages with the most estimated organic traffic), serp (keywords, up to 5: the ranking pages, each with its domain's authority: DataForSEO domain_rank 0 to 100, Ahrefs domain_rating; depth sets results per keyword, default 20; rows run position by position across the keywords), serp_overlap (2 to 10 keywords: for each pair, how many of their top 10 pages and sites are the same, to decide one page or several; results without a real page are counted, not compared), competitors (target: domains that share keywords), backlinks (target: referring domains), pages_by_backlinks (target: the pages with the most referring domains), link_gap (target and 2 to 5 competitors: referring domains that link to two or more competitors and not the target, highest authority first, from each site's top 1,000 referring domains; costs one backlinks call per site), ai_answers (DataForSEO only; target: the site. Without keywords it takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself. With up to 10 questions in keywords it asks those instead, and the target can be any domain, such as a competitor's. Each answer row also lists the other sites it cites, and the notes rank them. Answers come from DataForSEO's queue: answers not ready after about 40 seconds are returned as tasks, already paid for; call again with the same target and those tasks to read them at no cost. It asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s queue prices are about $0.0072 a question across the three engines, so about $0.07 a run: tell the person before running it). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.`;
38261
+ const RESEARCH_HELP = `provider is optional: without it the call uses the saved key, DataForSEO when both are saved. In the CLI and the local MCP server, serp can also use a provider package installed on this machine that supplies live search results: pass its id as provider. The target is a domain (the whole site), a path ending in / such as example.com/blog/ (every URL under it), or one page; scope overrides that. keyword_metrics (keywords: volume, difficulty, CPC), keyword_ideas (up to 5 seed keywords: searches that contain each seed, by volume), ranked_keywords (target: keywords a site, subfolder or page ranks for, with the provider's traffic total for the whole target), top_pages (target: the pages with the most estimated organic traffic), serp (keywords, up to 5: the ranking pages with their organic position beside DataForSEO's rank_absolute, which also counts AI Overviews and other blocks, plus the AI Overview's sources; device desktop or mobile, DataForSEO only; each with its domain's authority: DataForSEO domain_rank 0 to 100, Ahrefs domain_rating; depth sets results per keyword, default 20; rows run position by position across the keywords), index_check (DataForSEO only; target: one page URL; keywords: up to 2 exact phrases from the page, default its H1 or title. Searches Google for site:<url> and each "<phrase>" on desktop and mobile, retries failed searches, treats Google's fallback to unrelated sites as no match, and says indexed, not found or unknown with each search's evidence; about $0.01 a page), serp_overlap (2 to 10 keywords: for each pair, how many of their top 10 pages and sites are the same, to decide one page or several; results without a real page are counted, not compared), competitors (target: domains that share keywords), backlinks (target: referring domains), pages_by_backlinks (target: the pages with the most referring domains), link_gap (target and 2 to 5 competitors: referring domains that link to two or more competitors and not the target, highest authority first, from each site's top 1,000 referring domains; costs one backlinks call per site), ai_answers (DataForSEO only; target: the site. Without keywords it takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself. With up to 10 questions in keywords it asks those instead, and the target can be any domain, such as a competitor's. Each answer row also lists the other sites it cites, and the notes rank them. Answers come from DataForSEO's queue: answers not ready after about 40 seconds are returned as tasks, already paid for; call again with the same target and those tasks to read them at no cost. It asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s queue prices are about $0.0072 a question across the three engines, so about $0.07 a run: tell the person before running it). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.`;
37830
38262
  const GENERIC_PLATFORMS = [
37831
38263
  "youtube.com",
37832
38264
  "facebook.com",
@@ -37888,6 +38320,19 @@ async function runResearch(input, keys, options) {
37888
38320
  const offset = input.offset ?? 0;
37889
38321
  if (input.action === "ai_answers") throw new CommandError("INVALID_INPUT", "ai_answers runs on its own.");
37890
38322
  const provider = chooseProvider(input.provider, keys);
38323
+ const builtIn = RESEARCH_PROVIDERS.includes(provider) ? provider : null;
38324
+ if (!builtIn && input.action !== "serp") throw new CommandError("INVALID_INPUT", `An installed provider answers serp only; ${input.action} needs a built-in provider.`);
38325
+ if (input.action === "index_check") {
38326
+ if (provider === "ahrefs") throw new CommandError("INVALID_INPUT", "index_check uses DataForSEO: it runs live Google searches, which Ahrefs does not offer.");
38327
+ return runIndexCheck({
38328
+ url: target,
38329
+ phrases: input.keywords,
38330
+ country
38331
+ }, keys);
38332
+ }
38333
+ if (input.device && input.action !== "serp") throw new CommandError("INVALID_INPUT", "device works with serp only. ranked_keywords and the other database actions have one ranking per keyword, from desktop results.");
38334
+ if (input.device === "mobile" && provider === "ahrefs") throw new CommandError("INVALID_INPUT", "Ahrefs' SERP overview has no device choice, so it cannot show mobile results. Use provider dataforseo for device mobile.");
38335
+ const device = input.action === "serp" ? input.device ?? "desktop" : null;
37891
38336
  const language = provider === "dataforseo" ? dataForSeoLanguage(country) : null;
37892
38337
  const scope = [
37893
38338
  "ranked_keywords",
@@ -37903,28 +38348,32 @@ async function runResearch(input, keys, options) {
37903
38348
  ...scope ? { scope } : {},
37904
38349
  competitors: input.competitors ?? [],
37905
38350
  country,
37906
- ...input.action === "serp" ? { depth: input.depth ?? 20 } : {}
38351
+ ...input.action === "serp" ? { depth: input.depth ?? 20 } : {},
38352
+ ...device === "mobile" ? { device } : {}
37907
38353
  };
37908
38354
  const store = options.store ?? workersResearchStore();
37909
38355
  const key = await researchKey([
37910
38356
  provider,
37911
- provider === "dataforseo" ? keys.get("x-dataforseo-login") : keys.get("x-ahrefs-key"),
38357
+ builtIn ? keys.get(KEY_HEADER[builtIn]) : null,
37912
38358
  request
37913
38359
  ]);
37914
38360
  const saved = await readResearch(store, key, limit);
37915
- const result = saved?.result ?? await research(provider, keys, {
38361
+ const result = saved?.result ?? (builtIn ? await research(builtIn, keys, {
37916
38362
  ...request,
37917
38363
  limit
37918
- });
38364
+ }) : await installedSerpResearch(provider, {
38365
+ ...request,
38366
+ limit
38367
+ }, options.installedSerp));
37919
38368
  if (!saved) await saveResearch(store, key, {
37920
38369
  limit,
37921
38370
  savedAt: (/* @__PURE__ */ new Date()).toISOString(),
37922
38371
  result
37923
38372
  });
37924
- const { rows: fetched, costUsd: paid, partial, missed, noData, floor, leftOut, available, siteTotal, rankFailed, compared } = result;
38373
+ const { rows: fetched, costUsd: paid, partial, missed, noData, floor, leftOut, available, siteTotal, rankFailed, compared, aiOverviews, warnings } = result;
37925
38374
  const costUsd = saved ? 0 : paid;
37926
38375
  const order = new Map(request.keywords.map((word, index) => [word, index]));
37927
- const found = input.action === "serp" && request.keywords.length > 1 ? [...fetched].sort((a, b) => Number(a.position ?? 1e9) - Number(b.position ?? 1e9) || (order.get(String(a.keyword)) ?? 0) - (order.get(String(b.keyword)) ?? 0)) : fetched;
38376
+ const found = input.action === "serp" && request.keywords.length > 1 ? [...fetched].sort((a, b) => Number(a.position ?? 0) - Number(b.position ?? 0) || (order.get(String(a.keyword)) ?? 0) - (order.get(String(b.keyword)) ?? 0)) : fetched;
37928
38377
  const { rows, note } = input.action === "competitors" ? realCompetitors(found, target) : {
37929
38378
  rows: found,
37930
38379
  note: null
@@ -37942,7 +38391,7 @@ async function runResearch(input, keys, options) {
37942
38391
  const perKeyword = input.action === "serp" ? Object.fromEntries(request.keywords.map((word) => [word, rated.rows.filter((row) => row.keyword === word).length])) : null;
37943
38392
  return {
37944
38393
  text: rowsText({
37945
- title: `${provider} ${input.action}${target ? ` for ${target}` : ""}${scope ? ` (${SCOPE_TEXT[scope]})` : ""}${LINK_ACTIONS.includes(input.action) ? ", all countries" : `, ${country}${language ? `, language ${language}` : ""}`}.`,
38394
+ title: `${provider} ${input.action}${device && provider === "dataforseo" ? ` (${device})` : ""}${target ? ` for ${target}` : ""}${scope ? ` (${SCOPE_TEXT[scope]})` : ""}${LINK_ACTIONS.includes(input.action) ? ", all countries" : `, ${country}${language ? `, language ${language}` : ""}`}.`,
37946
38395
  rows: rated.rows,
37947
38396
  offset,
37948
38397
  limit: size,
@@ -37950,16 +38399,19 @@ async function runResearch(input, keys, options) {
37950
38399
  notes: [
37951
38400
  `${provider} estimates, in the provider’s own units.`,
37952
38401
  ...compared ? [`Compared: ${compared.withPage} of ${compared.results} top results had a real page, so shared_pages counts only those; ${compared.results - compared.withPage} came back as Google redirect links and could not be compared by page. ${compared.withSite} had a known site for shared_sites. Unique pages and sites are counted per keyword.${compared.withPage < compared.results / 2 && provider === "dataforseo" ? " With this few pages, shared_pages cannot settle one page or several; provider ahrefs returns real pages." : ""}`] : [],
37953
- ...VOLUME_ACTIONS.includes(input.action) ? [`Volume model: ${VOLUME_MODEL[provider]} ${VOLUMES_DIFFER}`] : [],
38402
+ ...VOLUME_ACTIONS.includes(input.action) && builtIn ? [`Volume model: ${VOLUME_MODEL[builtIn]} ${VOLUMES_DIFFER}`] : [],
38403
+ ...input.action === "serp" || input.action === "ranked_keywords" ? [positionNote(provider)] : [],
38404
+ ...aiOverviews?.length ? [aiOverviewNote(aiOverviews, request.keywords.length)] : input.action === "serp" && provider === "dataforseo" && rows.length ? ["No AI Overview on these results pages."] : [],
37954
38405
  ...perKeyword ? [`Results per keyword: ${Object.entries(perKeyword).map(([word, count]) => `${word} ${count}`).join(", ")}. Rows run position by position across the keywords.`] : [],
37955
- ...siteTotal ? [siteTotalNote(provider, siteTotal, rows.length)] : [],
38406
+ ...siteTotal && builtIn ? [siteTotalNote(builtIn, siteTotal, rows.length)] : [],
37956
38407
  ...available !== void 0 && available > found.length ? [`Capped: ${provider} has ${available.toLocaleString("en-US")} rows for this; this read the first ${found.length.toLocaleString("en-US")} in its order.`] : [],
37957
38408
  ...!found.length && scope ? [emptyNote(provider, input.action)] : [],
37958
38409
  ...input.action === "serp" && provider === "dataforseo" ? [rankFailed ? "domain_rank is missing: the DataForSEO rank call failed, so it is unknown, not zero." : "domain_rank is DataForSEO's own authority score for the domain, 0 to 100. It is not Ahrefs' Domain Rating."] : [],
37959
38410
  ...input.action === "serp" && !drKey && provider === "dataforseo" ? ["No domain_rating column: Domain Rating by Ahrefs needs an Ahrefs key (or the hosted MCP at https://seoskill.dev/mcp, which adds it)."] : [],
37960
38411
  ...store && rated.rows.length > 50 ? ["The result is kept for a day, so the next pages, and the same rows in full, cost nothing more."] : [],
37961
- ...saved ? [`Read from the result saved at ${saved.savedAt}, at no new cost${paid ? ` (it cost $${paid} then)` : ""}. Results are kept for a day.`] : costUsd !== null ? [`DataForSEO cost: $${costUsd}.`] : [],
37962
- ...partial ? ["Partial: DataForSEO could not fetch some pages of results, so rows may be missing."] : [],
38412
+ ...saved ? [`Read from the result saved at ${saved.savedAt}, at no new cost${paid ? ` (it cost $${paid} then)` : ""}. Results are kept for a day.`] : costUsd !== null ? [builtIn ? `DataForSEO cost: $${costUsd}.` : `Cost the ${provider} package reports: $${costUsd}.`] : [],
38413
+ ...partial ? [`Partial: ${builtIn ? "DataForSEO" : provider} could not fetch some pages of results, so rows may be missing.`] : [],
38414
+ ...warnings?.length ? [`${provider} warnings: ${warnings.join(" ")}`] : [],
37963
38415
  ...missed.length ? [`Partial: ${provider} failed for ${missed.join(", ")}, so those keywords have no rows. Ask for them again.`] : [],
37964
38416
  ...noData.length ? [noDataNote(provider, input.action, noData)] : [],
37965
38417
  ...byDr ? [rows.length > GAP_RATED ? `Sorted by Domain Rating for the top ${GAP_RATED} rows by DataForSEO rank; the ${rows.length - GAP_RATED} rows after them follow by rank, with no Domain Rating.` : "Sorted by Domain Rating, then DataForSEO rank."] : [],
@@ -37977,13 +38429,15 @@ async function runResearch(input, keys, options) {
37977
38429
  action: input.action,
37978
38430
  target: target || null,
37979
38431
  scope,
37980
- siteTotal: siteTotal ? {
38432
+ siteTotal: siteTotal && builtIn ? {
37981
38433
  ...siteTotal,
37982
- model: TRAFFIC_MODEL[provider]
38434
+ model: TRAFFIC_MODEL[builtIn]
37983
38435
  } : null,
37984
38436
  available: available ?? null,
37985
38437
  country,
37986
38438
  language,
38439
+ ...device ? { device: provider === "dataforseo" ? device : null } : {},
38440
+ ...input.action === "serp" && provider === "dataforseo" ? { aiOverviews: aiOverviews ?? [] } : {},
37987
38441
  targetDomainRating: hasTargetDr ? targetDr : null,
37988
38442
  attribution: rated.added || hasTargetDr ? DR_ATTRIBUTION : null,
37989
38443
  leftOut: note,
@@ -37992,6 +38446,7 @@ async function runResearch(input, keys, options) {
37992
38446
  partial,
37993
38447
  missedKeywords: missed,
37994
38448
  noDataKeywords: noData,
38449
+ ...warnings?.length ? { warnings } : {},
37995
38450
  ...input.action === "link_gap" ? {
37996
38451
  competitors: input.competitors ?? [],
37997
38452
  cutAt: floor ?? null,
@@ -38007,6 +38462,18 @@ async function runResearch(input, keys, options) {
38007
38462
  }
38008
38463
  };
38009
38464
  }
38465
+ function positionNote(provider) {
38466
+ if (provider !== "dataforseo" && provider !== "ahrefs") return `position is the organic position the ${provider} package reports (its rankGroup); rank_absolute is its rank among every block on the page, and type is featured_snippet for a featured snippet, which has no organic position.`;
38467
+ return provider === "dataforseo" ? "position is the organic position: the nth organic link (DataForSEO rank_group). rank_absolute counts every block on the page from the top, AI Overviews, People Also Ask, videos and images included, so it is higher. A featured snippet has type featured_snippet and no organic position." : "position is the organic position: the nth row Ahrefs marks organic. ahrefs_position is Ahrefs' own position field, and type lists what each row is.";
38468
+ }
38469
+ function aiOverviewNote(overviews, keywords) {
38470
+ const each = overviews.map((overview) => {
38471
+ const sites = [...new Set(overview.references.map((ref) => ref.domain).filter((domain) => Boolean(domain)))];
38472
+ const unknown = overview.references.filter((ref) => !ref.domain).length;
38473
+ return `${overview.keyword}: ${overview.references.length} sources${sites.length ? ` (${sites.slice(0, 6).join(", ")}${sites.length > 6 ? ", …" : ""})` : ""}${unknown ? `, ${unknown} behind Google redirect links` : ""}`;
38474
+ });
38475
+ return `AI Overview shown for ${overviews.length} of ${keywords} keyword${keywords === 1 ? "" : "s"}. ${each.join("; ")}. The full list is in the JSON as aiOverviews.`;
38476
+ }
38010
38477
  const LINK_ACTIONS = [
38011
38478
  "backlinks",
38012
38479
  "pages_by_backlinks",
@@ -38634,7 +39101,7 @@ const percent = (site) => {
38634
39101
  const clicks = site.clicks;
38635
39102
  return Math.round(Math.abs(clicks.clicks - clicks.previous) / clicks.previous * 100);
38636
39103
  };
38637
- function issues(site, startMs, endMs) {
39104
+ function issues(site, startMs, endMs, first = false) {
38638
39105
  const found = [];
38639
39106
  if (site.index?.dropped) found.push({
38640
39107
  rank: 0,
@@ -38656,11 +39123,11 @@ function issues(site, startMs, endMs) {
38656
39123
  });
38657
39124
  if (crawled && crawl.new_issues > 0) found.push({
38658
39125
  rank: 3,
38659
- heading: plural$1(crawl.new_issues, "new issue")
39126
+ heading: plural$1(crawl.new_issues, first ? "open finding" : "new issue")
38660
39127
  });
38661
39128
  return found;
38662
39129
  }
38663
- function siteLines(site, startMs, endMs) {
39130
+ function siteLines(site, startMs, endMs, first = false) {
38664
39131
  const lines = [];
38665
39132
  const index = site.index;
38666
39133
  if (!index) lines.push("Google index: no checks ran this week yet.");
@@ -38677,7 +39144,7 @@ function siteLines(site, startMs, endMs) {
38677
39144
  if (crawled && crawl.status === "failed") lines.push(`Weekly crawl: stopped after ${plural$1(crawl.pages, "page")}${crawl.error ? `: ${sentence(crawl.error)}` : "."} It tries again next week.`);
38678
39145
  else if (crawled) {
38679
39146
  const rule = crawl.top_rule ? AUDIT_RULES[crawl.top_rule] : void 0;
38680
- lines.push(crawl.new_issues ? `Weekly crawl: ${plural$1(crawl.pages, "page")}, ${plural$1(crawl.new_issues, "new issue")}${rule ? `, most of them ${rule.charAt(0).toLowerCase()}${rule.slice(1)}` : ""}.` : `Weekly crawl: ${plural$1(crawl.pages, "page")}, no new issues.`);
39147
+ lines.push(crawl.new_issues ? `Weekly crawl: ${plural$1(crawl.pages, "page")}, ${plural$1(crawl.new_issues, first ? "open finding" : "new issue")}${rule ? `, most of them ${rule.charAt(0).toLowerCase()}${rule.slice(1)}` : ""}.` : `Weekly crawl: ${plural$1(crawl.pages, "page")}, no new issues.`);
38681
39148
  } else if (site.added_at < startMs) lines.push(`Weekly crawl: none finished this week${site.start_error ? `: ${sentence(site.start_error)}` : "."} It tries again each hour.`);
38682
39149
  else lines.push("Weekly crawl: the first crawl is still running.");
38683
39150
  const clicks = site.clicks;
@@ -38745,7 +39212,7 @@ function weekMarkdown(data) {
38745
39212
  const byName = (a, b) => siteName(a.property).localeCompare(siteName(b.property)) || a.property.localeCompare(b.property);
38746
39213
  const rows = data.sites.map((site) => ({
38747
39214
  site,
38748
- found: issues(site, startMs, endMs),
39215
+ found: issues(site, startMs, endMs, data.first),
38749
39216
  waiting: !site.crawl || !site.index
38750
39217
  }));
38751
39218
  const attention = rows.filter((row) => row.found.length);
@@ -38757,7 +39224,7 @@ function weekMarkdown(data) {
38757
39224
  "",
38758
39225
  attention.length ? total === 1 ? "Your site had changes worth a look this week." : `${attention.length} of your ${total} sites had changes worth a look this week.` : quiet.length ? total === 1 ? "Your site had a quiet week." : quiet.length === total ? `All ${total} of your sites had a quiet week.` : `${quiet.length} of your ${total} sites had a quiet week.` : "Your sites are still on their first checks."
38759
39226
  ];
38760
- for (const { site, found } of rows) out.push("", `## ${siteName(site.property)}${found.length ? `: ${list(found.map((item) => item.heading))}` : ""}`, "", ...siteLines(site, startMs, endMs).map((line) => `- ${line}`), ...opportunityLines(site), ...measuredLines(site), ...updateLines(site), ...site.link ? [`- [View site](${site.link})`] : []);
39227
+ for (const { site, found } of rows) out.push("", `## ${siteName(site.property)}${found.length ? `: ${list(found.map((item) => item.heading))}` : ""}`, "", ...siteLines(site, startMs, endMs, data.first).map((line) => `- ${line}`), ...opportunityLines(site), ...measuredLines(site), ...updateLines(site), ...site.link ? [`- [View site](${site.link})`] : []);
38761
39228
  return out.join("\n");
38762
39229
  }
38763
39230
  async function runAllSites(deps, sites) {
@@ -38984,7 +39451,7 @@ const SHARED_TOOLS = [
38984
39451
  title: "Keyword, competitor, backlink and AI answer data",
38985
39452
  description: `Data from the person’s own DataForSEO or Ahrefs account, which costs them credits. It uses the key the person saved on their seoskill.dev account (or on this machine for the CLI). Actions: ${RESEARCH_HELP}`,
38986
39453
  input: z.object({
38987
- provider: z.enum(RESEARCH_PROVIDERS).optional().describe("Optional. Without it, the saved key; DataForSEO when both are saved."),
39454
+ provider: z.string().max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional().describe(`${RESEARCH_PROVIDERS.join(" or ")}. Optional: without it, the saved key; DataForSEO when both are saved. For serp in the CLI and the local MCP server it can also be the id of an installed provider package with live search results.`),
38988
39455
  action: z.enum(RESEARCH_ACTIONS),
38989
39456
  keywords: z.array(z.string().min(1)).max(100).default([]),
38990
39457
  target: z.string().default("").describe("Domain (the whole site), a path ending in / for every URL under it, such as example.com/blog/, or one page."),
@@ -38995,12 +39462,13 @@ const SHARED_TOOLS = [
38995
39462
  brand: z.array(z.string().min(1).max(60)).max(10).default([]).describe("ai_answers only: the brand names that count as a mention, such as Datadog for datadoghq.com. Default: each domain's name."),
38996
39463
  country: z.string().regex(/^[a-z]{2}$/).default("us").describe("Two-letter country code."),
38997
39464
  depth: z.number().int().min(1).max(100).optional().describe(`serp only: results per keyword, default 20. Deeper costs more.`),
39465
+ device: z.enum(RESEARCH_DEVICES).optional().describe("serp only: desktop (default) or mobile. Google ranks pages differently on each. DataForSEO only."),
38998
39466
  ...detail
38999
39467
  }),
39000
39468
  run: async (surface, input) => {
39001
39469
  const more = (offset) => `seo_research with the same input, detail "rows", offset ${offset}`;
39002
39470
  if (input.action === "ai_answers") {
39003
- if (input.provider === "ahrefs") throw new CommandError("INVALID_INPUT", "ai_answers uses dataforseo.");
39471
+ if (input.provider && input.provider !== "dataforseo") throw new CommandError("INVALID_INPUT", "ai_answers uses dataforseo.");
39004
39472
  return runAiAnswers({
39005
39473
  site: input.keywords.length || input.tasks.length ? input.target : await surface.resolveSite(input.target),
39006
39474
  country: input.country,
@@ -39015,6 +39483,7 @@ const SHARED_TOOLS = [
39015
39483
  return runResearch(input, await surface.keys(), {
39016
39484
  drKey: surface.drKey,
39017
39485
  store: surface.researchStore,
39486
+ installedSerp: surface.installedSerp,
39018
39487
  call: more
39019
39488
  });
39020
39489
  }
@@ -40894,6 +41363,133 @@ async function uninstallProviderExtension(id, options = {}) {
40894
41363
  ], { cwd: packagesDir });
40895
41364
  return removeInstalledProviderPackage(id);
40896
41365
  }
41366
+ // packages/core/src/provider-extensions/serp.ts
41367
+ const nullableText = z.string().max(5e4).nullable();
41368
+ const serpSnapshotResultSchema = z.object({
41369
+ observedAt: z.string().max(100),
41370
+ effectiveKeyword: z.string().trim().min(1).max(1e3),
41371
+ searchEngineDomain: z.string().max(253).nullable(),
41372
+ checkUrl: z.url().max(8192).nullable(),
41373
+ resultCount: z.number().int().nonnegative().safe().nullable(),
41374
+ pagesCount: z.number().int().nonnegative().max(100).safe().nullable(),
41375
+ features: z.array(z.string().trim().min(1).max(100)).max(100),
41376
+ organicResults: z.array(z.object({
41377
+ rankGroup: z.number().int().positive().safe(),
41378
+ rankAbsolute: z.number().int().positive().safe(),
41379
+ page: z.number().int().positive().max(100).safe(),
41380
+ domain: z.string().trim().min(1).max(253),
41381
+ url: z.url().max(8192),
41382
+ title: z.string().max(1e4).nullable(),
41383
+ description: nullableText,
41384
+ isFeaturedSnippet: z.boolean().nullable()
41385
+ }).strict()).max(100),
41386
+ localPack: z.object({
41387
+ present: z.boolean(),
41388
+ returnedRows: z.number().int().nonnegative().max(100).safe(),
41389
+ retainedRows: z.number().int().nonnegative().max(100).safe(),
41390
+ invalidRows: z.number().int().nonnegative().max(100).safe(),
41391
+ results: z.array(z.unknown()).max(100)
41392
+ }).strict(),
41393
+ coverage: z.object({
41394
+ returnedRows: z.number().int().nonnegative().max(1e4).safe(),
41395
+ retainedRows: z.number().int().nonnegative().max(100).safe(),
41396
+ invalidRows: z.number().int().nonnegative().max(1e4).safe(),
41397
+ providerTotalRows: z.number().int().nonnegative().safe().nullable(),
41398
+ completeness: z.enum([
41399
+ "complete",
41400
+ "partial",
41401
+ "capped"
41402
+ ]),
41403
+ nextCursor: z.string().max(500).nullable()
41404
+ }).strict(),
41405
+ request: z.object({
41406
+ endpoint: z.string().trim().min(1).max(500),
41407
+ filters: z.record(z.string().min(1).max(100), z.union([
41408
+ z.string().max(1e3),
41409
+ z.number().finite(),
41410
+ z.boolean()
41411
+ ])),
41412
+ sort: z.array(z.string().min(1).max(200)).max(20)
41413
+ }).strict(),
41414
+ cost: z.object({
41415
+ estimatedMicros: z.number().int().nonnegative().safe().nullable(),
41416
+ actualMicros: z.number().int().nonnegative().safe().nullable(),
41417
+ taskIds: z.array(z.string().min(1).max(500)).max(20),
41418
+ native: z.object({
41419
+ unit: z.string().trim().min(1).max(100),
41420
+ estimatedUnits: z.number().finite().nonnegative().nullable(),
41421
+ actualUnits: z.number().finite().nonnegative().nullable(),
41422
+ remainingBefore: z.number().finite().nonnegative().nullable()
41423
+ }).strict().optional()
41424
+ }).strict(),
41425
+ qualityWarnings: z.array(z.object({
41426
+ code: z.string().trim().min(1).max(100),
41427
+ message: z.string().trim().min(1).max(500),
41428
+ field: z.string().max(200).optional(),
41429
+ row: z.number().int().positive().safe().optional()
41430
+ }).strict()).max(50)
41431
+ }).strict();
41432
+ const same = (list, value) => !list || list.some((item) => item.toLowerCase() === value.toLowerCase());
41433
+ function supports(capability, market) {
41434
+ return capability.markets.some((entry) => same(entry.searchEngines, market.searchEngine) && same(entry.countryCodes, market.countryCode) && same(entry.languageCodes, market.languageCode) && same(entry.devices, market.device ?? "desktop"));
41435
+ }
41436
+ const installedProviderSerp = async (input) => {
41437
+ const loaded = await loadProviderExtensions();
41438
+ const provider = loaded.registry.get(input.provider);
41439
+ if (!provider) throw new CommandError("INVALID_INPUT", `No provider ${input.provider} is installed. The built-in providers are dataforseo and ahrefs; \`seo providers list\` shows installed packages.`);
41440
+ const capability = loaded.registry.capability(provider.id, "serp-snapshot");
41441
+ if (!capability) throw new CommandError("INVALID_INPUT", `${provider.displayName} has no live search results (serp-snapshot), so it cannot answer serp. \`seo providers describe ${provider.id}\` lists its own actions.`);
41442
+ const market = {
41443
+ searchEngine: "google",
41444
+ countryCode: input.country.toUpperCase(),
41445
+ languageCode: input.language,
41446
+ device: input.device
41447
+ };
41448
+ if (!supports(capability, market)) throw new CommandError("INVALID_INPUT", `${provider.displayName} has no ${input.device} Google results for country ${input.country} in language ${input.language}.`);
41449
+ let account;
41450
+ let credentials;
41451
+ try {
41452
+ account = normalizeProviderExtensionAccount(provider, readProviderExtensionAccount(provider.id));
41453
+ credentials = await readProviderExtensionCredentials({
41454
+ providerId: provider.id,
41455
+ account,
41456
+ fields: provider.connection.fields
41457
+ });
41458
+ } catch (error) {
41459
+ throw new CommandError("NO_KEY", `${error instanceof Error ? error.message : String(error)} Connect it with \`seo providers connect ${provider.id}\`.`);
41460
+ }
41461
+ const depth = Math.min(input.depth, capability.maxDepth);
41462
+ let raw;
41463
+ try {
41464
+ raw = await capability.run({
41465
+ account,
41466
+ credentials,
41467
+ keyword: input.keyword,
41468
+ market,
41469
+ depth
41470
+ }, runtimeForProvider(provider.id, { maxRequests: capability.maxRequests }));
41471
+ } catch (error) {
41472
+ throw new CommandError("PROVIDER_ERROR", safeProviderAdapterError(provider.id, error, credentials).message);
41473
+ }
41474
+ const parsed = serpSnapshotResultSchema.safeParse(raw);
41475
+ if (!parsed.success) throw new CommandError("PROVIDER_ERROR", `${provider.displayName} returned a results page that does not match the serp-snapshot shape.`);
41476
+ const result = parsed.data;
41477
+ const warnings = result.qualityWarnings.map((warning) => warning.message);
41478
+ return {
41479
+ rows: [...result.organicResults].sort((a, b) => a.rankAbsolute - b.rankAbsolute || (a.url < b.url ? -1 : 1)).map((item) => ({
41480
+ position: item.isFeaturedSnippet ? null : item.rankGroup,
41481
+ rank_absolute: item.rankAbsolute,
41482
+ type: item.isFeaturedSnippet ? "featured_snippet" : "organic",
41483
+ url: item.url,
41484
+ domain: item.domain,
41485
+ title: item.title
41486
+ })),
41487
+ costMicros: result.cost.actualMicros,
41488
+ partial: result.coverage.completeness === "partial",
41489
+ warnings,
41490
+ depth
41491
+ };
41492
+ };
40897
41493
  // packages/core/src/providers/ahrefs/schema.ts
40898
41494
  const ahrefsLimitsAndUsageResponseSchema = z.object({ limits_and_usage: z.object({
40899
41495
  api_key_expiration_date: z.string().trim().min(1).max(100),
@@ -41943,6 +42539,6 @@ function isoDate(daysAgo = 0) {
41943
42539
  date.setUTCDate(date.getUTCDate() - daysAgo);
41944
42540
  return date.toISOString().slice(0, 10);
41945
42541
  }
41946
- export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_IMPRESSIONS, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_MAX_PAGES, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, DecisionModelError, DecisionRunner, GOOGLE_TIMEOUT_MS, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, LINT_MAX_FILES, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_MAX_PAGES, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PAGE_TYPES, PROVIDER_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SITEMAP_FILE_URL_LIMIT, SITE_CHECK_MAX_ISSUES, SITE_CHECK_MAX_PAGES, SITE_FACTS_DEFAULT_CONCURRENCY, SITE_FACTS_MAX_CONCURRENCY, SITE_FACTS_MAX_SITES, SITE_FACTS_TIMEOUT_MS, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, TYPESAFE_API_KEY_ENV, URL_INSPECTION_TIMEOUT_MS, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, builtPagePath, cacheDb, cannibalDataStatus, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkBuiltSite, checkDatabaseReadiness, checkSites, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, cloudWeek, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlCoverageCaveat, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectAgentName, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchFailureKind, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, internalLinksReportWith, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, lintPaths, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, localResearchStore, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readClickySiteKeys, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readLintFiles, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteStartUrl, snapshotFetchedPage, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, typesafeDecisionModel, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updateNotice, updatePostmortemCsvFiles, updatePostmortemWorkflow, urlInspectionQuota, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, withinGoogleTimeout, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
42542
+ export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_IMPRESSIONS, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_MAX_PAGES, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, DecisionModelError, DecisionRunner, GOOGLE_TIMEOUT_MS, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, LINT_MAX_FILES, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_MAX_PAGES, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PAGE_TYPES, PROVIDER_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SITEMAP_FILE_URL_LIMIT, SITE_CHECK_MAX_ISSUES, SITE_CHECK_MAX_PAGES, SITE_FACTS_DEFAULT_CONCURRENCY, SITE_FACTS_MAX_CONCURRENCY, SITE_FACTS_MAX_SITES, SITE_FACTS_TIMEOUT_MS, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, TYPESAFE_API_KEY_ENV, URL_INSPECTION_TIMEOUT_MS, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, builtPagePath, cacheDb, cannibalDataStatus, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkBuiltSite, checkDatabaseReadiness, checkSites, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, cloudWeek, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlCoverageCaveat, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectAgentName, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchFailureKind, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, installedProviderSerp, internalLinksReport, internalLinksReportWith, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, lintPaths, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, localResearchStore, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readClickySiteKeys, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readLintFiles, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteStartUrl, snapshotFetchedPage, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, typesafeDecisionModel, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updateNotice, updatePostmortemCsvFiles, updatePostmortemWorkflow, urlInspectionQuota, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, withinGoogleTimeout, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
41947
42543
 
41948
42544
  //# sourceMappingURL=index.js.map