seo 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-ZQwQYL0U.js";
2
+ import { n as version, t as load } from "./esm-CQJgJ8TD.js";
3
3
  import { SEO_PROVIDER_API_VERSION } from "./provider-sdk.js";
4
4
  import crypto, { createHash, randomUUID, sign } from "node:crypto";
5
5
  import { z } from "zod";
@@ -1614,7 +1614,7 @@ async function googleJson(gfetch, url, input) {
1614
1614
  return readJson(response, input.label, { maxBytes: input.maxBytes });
1615
1615
  }
1616
1616
  const GSC = "https://www.googleapis.com/webmasters/v3";
1617
- const PAGE_ROWS = 25e3;
1617
+ const PAGE_ROWS$1 = 25e3;
1618
1618
  async function searchAnalyticsRows(gfetch, site, body, options) {
1619
1619
  const rows = [];
1620
1620
  const { maxRows: requested, ...apiBody } = body;
@@ -1625,7 +1625,7 @@ async function searchAnalyticsRows(gfetch, site, body, options) {
1625
1625
  while (true) {
1626
1626
  const remaining = typeof maxRows === "number" ? maxRows - rows.length : void 0;
1627
1627
  if (typeof remaining === "number" && remaining <= 0) break;
1628
- const rowLimit = Math.min(apiBody.rowLimit ?? PAGE_ROWS, remaining ?? PAGE_ROWS, PAGE_ROWS);
1628
+ const rowLimit = Math.min(apiBody.rowLimit ?? PAGE_ROWS$1, remaining ?? PAGE_ROWS$1, PAGE_ROWS$1);
1629
1629
  const json = await googleJson(gfetch, url, {
1630
1630
  ...options,
1631
1631
  label: "Search Console",
@@ -1702,7 +1702,7 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1702
1702
  //#endregion
1703
1703
  //#region packages/core/src/bing/api.ts
1704
1704
  const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1705
- const MAX_BYTES$3 = 8e6;
1705
+ const MAX_BYTES$4 = 8e6;
1706
1706
  function bingCall(auth, options) {
1707
1707
  return async (method, params = {}) => {
1708
1708
  const url = new URL(`${BASE}/${method}`);
@@ -1727,7 +1727,7 @@ function bingCall(auth, options) {
1727
1727
  if (response.status === 429 || /throttle/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is limiting requests. Try again in a few minutes.");
1728
1728
  throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
1729
1729
  }
1730
- return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$3 })).d;
1730
+ return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$4 })).d;
1731
1731
  };
1732
1732
  }
1733
1733
  //#endregion
@@ -4263,6 +4263,7 @@ const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
4263
4263
  const CACHE_MAX_AGE_MS = 720 * 60 * 60 * 1e3;
4264
4264
  const CACHE_LIMITS = {
4265
4265
  gsc_cache: 48 * MEBIBYTE$2,
4266
+ url_inspection_cache: 16 * MEBIBYTE$2,
4266
4267
  ga4_cache: 24 * MEBIBYTE$2,
4267
4268
  provider_cache: 32 * MEBIBYTE$2,
4268
4269
  http_cache: 128 * MEBIBYTE$2,
@@ -4280,6 +4281,16 @@ const CACHE_POLICIES = [
4280
4281
  "response_json"
4281
4282
  ]
4282
4283
  },
4284
+ {
4285
+ table: "url_inspection_cache",
4286
+ keys: ["site_url", "url"],
4287
+ timestamp: "fetched_at",
4288
+ sizeColumns: [
4289
+ "site_url",
4290
+ "url",
4291
+ "status_json"
4292
+ ]
4293
+ },
4283
4294
  {
4284
4295
  table: "ga4_cache",
4285
4296
  keys: ["property_id", "query_hash"],
@@ -4583,6 +4594,17 @@ CREATE INDEX IF NOT EXISTS idx_url_inspection_reservations_minute
4583
4594
  credential_key, property_site_url, created_at
4584
4595
  );
4585
4596
 
4597
+ CREATE TABLE IF NOT EXISTS url_inspection_cache (
4598
+ site_url TEXT NOT NULL,
4599
+ url TEXT NOT NULL,
4600
+ status_json TEXT NOT NULL,
4601
+ fetched_at INTEGER NOT NULL,
4602
+ expires_at INTEGER NOT NULL,
4603
+ PRIMARY KEY(site_url, url)
4604
+ ) WITHOUT ROWID;
4605
+ CREATE INDEX IF NOT EXISTS idx_url_inspection_cache_expires
4606
+ ON url_inspection_cache(expires_at);
4607
+
4586
4608
  CREATE TABLE IF NOT EXISTS performance_reports (
4587
4609
  id TEXT PRIMARY KEY,
4588
4610
  url TEXT NOT NULL,
@@ -4694,6 +4716,7 @@ function getCacheStats() {
4694
4716
  const counts = {
4695
4717
  sites: database.prepare("SELECT COUNT(*) AS count FROM sites").get(),
4696
4718
  gsc_cache: database.prepare("SELECT COUNT(*) AS count FROM gsc_cache").get(),
4719
+ url_inspection_cache: database.prepare("SELECT COUNT(*) AS count FROM url_inspection_cache").get(),
4697
4720
  google_analytics_cache: database.prepare("SELECT COUNT(*) AS count FROM ga4_cache").get(),
4698
4721
  provider_cache: { count: providerCount("dataforseo") },
4699
4722
  ahrefs_cache: { count: providerCount("ahrefs") },
@@ -4725,8 +4748,9 @@ function clearCache(provider, olderThanMs) {
4725
4748
  compactCacheDatabase(database, { allowFullVacuum: true });
4726
4749
  return info.changes;
4727
4750
  }
4728
- const tables = provider === "gsc" ? ["gsc_cache"] : provider === "google-analytics" ? ["ga4_cache"] : provider === "http" ? ["http_cache"] : [
4751
+ const tables = provider === "gsc" ? ["gsc_cache", "url_inspection_cache"] : provider === "google-analytics" ? ["ga4_cache"] : provider === "http" ? ["http_cache"] : [
4729
4752
  "gsc_cache",
4753
+ "url_inspection_cache",
4730
4754
  "ga4_cache",
4731
4755
  "provider_cache",
4732
4756
  "http_cache"
@@ -8222,6 +8246,19 @@ function reserveUrlInspectionQuota(input) {
8222
8246
  if ("error" in result) throw result.error;
8223
8247
  return result.reservation;
8224
8248
  }
8249
+ function readUrlInspectionQuota(input) {
8250
+ const database = input.database ?? getDb();
8251
+ const now = input.now ?? /* @__PURE__ */ new Date();
8252
+ const bucket = database.prepare(`SELECT limit_count, used_count, reserved_count, blocked_until
8253
+ FROM url_inspection_quota_buckets
8254
+ WHERE credential_key = ? AND property_site_url = ? AND quota_date = ?`).get(input.credentialKey, input.property, urlInspectionQuotaDate(now));
8255
+ const limit = bucket?.limit_count ?? 2e3;
8256
+ return {
8257
+ used: (bucket?.blocked_until ?? 0) > now.getTime() ? limit : Math.min(limit, (bucket?.used_count ?? 0) + (bucket?.reserved_count ?? 0)),
8258
+ limit,
8259
+ resetAt: urlInspectionQuotaResetAt(now).toISOString()
8260
+ };
8261
+ }
8225
8262
  function finalizeUrlInspectionQuota(input) {
8226
8263
  const database = input.database ?? getDb();
8227
8264
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -8266,6 +8303,13 @@ function inspectionResponseError(response, input) {
8266
8303
  if (response.status >= 500) return new SeoError("PROVIDER_UNAVAILABLE", `Google URL Inspection is unavailable (${response.status}). Try again later.`);
8267
8304
  return new SeoError("PROVIDER_UNAVAILABLE", `URL Inspection failed with ${response.status}.`);
8268
8305
  }
8306
+ async function urlInspectionQuota(input) {
8307
+ const { quotaIdentity } = await getAuthorized(input.accountEmail ?? await cloudAccountFor(input.siteUrl));
8308
+ return readUrlInspectionQuota({
8309
+ credentialKey: credentialKey(quotaIdentity),
8310
+ property: input.siteUrl
8311
+ });
8312
+ }
8269
8313
  async function inspectUrl(input) {
8270
8314
  const inspectionUrl = assertUrlMatchesGscProperty(input.siteUrl, input.inspectionUrl);
8271
8315
  const { client, quotaIdentity } = await getAuthorized(input.accountEmail ?? await cloudAccountFor(input.siteUrl));
@@ -10169,7 +10213,7 @@ const actionPriority = new Map([
10169
10213
  "unverified",
10170
10214
  "covered"
10171
10215
  ].map((type, index) => [type, index]));
10172
- function plural$6(count, singular, pluralLabel) {
10216
+ function plural$7(count, singular, pluralLabel) {
10173
10217
  return count === 1 ? singular : pluralLabel;
10174
10218
  }
10175
10219
  function actionable(item) {
@@ -10203,7 +10247,7 @@ function pageOpportunityVerdict(input) {
10203
10247
  if (focus === "unverified") return "GSC visibility exists, but the page was not verified and no specific change is justified yet.";
10204
10248
  if (focus === "covered") return "The verified page covers the sampled queries and CTR is not materially below its directional benchmark.";
10205
10249
  const count = analysis.summary.opportunities;
10206
- const prefix = `${count} ${plural$6(count, "actionable observation", "actionable observations")} found.`;
10250
+ const prefix = `${count} ${plural$7(count, "actionable observation", "actionable observations")} found.`;
10207
10251
  if (focus === "content-gap") return `${prefix} Verified body coverage is the main issue; confirm intent before expanding the page.`;
10208
10252
  if (focus === "serp-framing") return `${prefix} The body is relevant, but its title, meta description, or H1 framing is weak.`;
10209
10253
  if (focus === "ranking") return `${prefix} Most upside requires stronger relevance or authority; no CTR-only lift is claimed for page-two queries.`;
@@ -10218,7 +10262,7 @@ function pageOpportunityRecommendations(analysis) {
10218
10262
  }
10219
10263
  const recommendations = [`Start with "${top.query}". ${top.recommendation}`];
10220
10264
  const technical = actions.filter((item) => item.opportunityType === "technical-check").length;
10221
- if (technical) recommendations.push(`Resolve the technical evidence affecting ${technical} ${plural$6(technical, "row", "rows")} before content work.`);
10265
+ if (technical) recommendations.push(`Resolve the technical evidence affecting ${technical} ${plural$7(technical, "row", "rows")} before content work.`);
10222
10266
  const gaps = actions.filter((item) => item.opportunityType === "content-gap").length;
10223
10267
  if (gaps >= 3) recommendations.push(`Group the ${gaps} verified content gaps into useful sections rather than adding one thin paragraph per query.`);
10224
10268
  return recommendations;
@@ -14222,7 +14266,7 @@ function availabilityOf(status) {
14222
14266
  function errorText(error) {
14223
14267
  return error instanceof Error ? error.message : String(error);
14224
14268
  }
14225
- async function get(input, url, headers, readBody = true) {
14269
+ async function get$1(input, url, headers, readBody = true) {
14226
14270
  const controller = abortController({
14227
14271
  timeoutMs: input.timeoutMs,
14228
14272
  signal: input.signal
@@ -14279,7 +14323,7 @@ function llmsLinks(text, base) {
14279
14323
  return links;
14280
14324
  }
14281
14325
  async function checkLlmsTxt(input) {
14282
- const result = await get(input, new URL("/llms.txt", input.url).toString());
14326
+ const result = await get$1(input, new URL("/llms.txt", input.url).toString());
14283
14327
  const availability = result.availability === "available" && isHtml(result.contentType) ? "absent" : result.availability;
14284
14328
  const exists = availability === "available";
14285
14329
  const base = {
@@ -14296,7 +14340,7 @@ async function checkLlmsTxt(input) {
14296
14340
  for (const url of found.slice(0, LLMS_LINK_LIMIT)) {
14297
14341
  if (input.signal?.aborted) break;
14298
14342
  checked += 1;
14299
- const link = brokenLink(await get(input, url, void 0, false), false);
14343
+ const link = brokenLink(await get$1(input, url, void 0, false), false);
14300
14344
  if (link) broken.push(link);
14301
14345
  }
14302
14346
  return {
@@ -14340,7 +14384,7 @@ function sitemapUrlsFromRobots(text) {
14340
14384
  return [...urls];
14341
14385
  }
14342
14386
  async function checkRobotsAiAccess(input) {
14343
- const result = await get(input, new URL("/robots.txt", input.url).toString());
14387
+ const result = await get$1(input, new URL("/robots.txt", input.url).toString());
14344
14388
  const exists = result.availability === "available";
14345
14389
  const unknown = result.availability === "rate-limited" || result.availability === "unreachable";
14346
14390
  const declared = exists ? declaredUserAgents(result.text) : /* @__PURE__ */ new Set();
@@ -14369,7 +14413,7 @@ async function checkAgentResources(input) {
14369
14413
  const resources = [];
14370
14414
  for (const path of AGENT_RESOURCE_PATHS) {
14371
14415
  if (input.signal?.aborted) break;
14372
- const result = await get(input, new URL(path, input.url).toString());
14416
+ const result = await get$1(input, new URL(path, input.url).toString());
14373
14417
  const exists = result.availability === "available" && !isHtml(result.contentType);
14374
14418
  let validJson;
14375
14419
  if (exists && /\bjson\b/i.test(result.contentType)) try {
@@ -14389,7 +14433,7 @@ async function checkAgentResources(input) {
14389
14433
  return resources;
14390
14434
  }
14391
14435
  async function checkMarkdownNegotiation(input) {
14392
- const result = await get(input, input.url, { accept: "text/markdown" }, false);
14436
+ const result = await get$1(input, input.url, { accept: "text/markdown" }, false);
14393
14437
  const vary = result.headers?.get("vary") ?? "";
14394
14438
  return {
14395
14439
  url: result.url,
@@ -14436,7 +14480,7 @@ async function checkMarkdownCopies(input) {
14436
14480
  const url = page.markdownAlternates?.[0];
14437
14481
  if (!url || !input.allows(url)) continue;
14438
14482
  checked += 1;
14439
- const result = await get({
14483
+ const result = await get$1({
14440
14484
  ...input,
14441
14485
  url
14442
14486
  }, url);
@@ -16293,7 +16337,7 @@ const installedPackageJsonSchema = z.object({
16293
16337
  function packageDirectory(packagesDir, packageName) {
16294
16338
  return join(packagesDir, "node_modules", ...packageName.split("/"));
16295
16339
  }
16296
- function inside(parent, child) {
16340
+ function inside$1(parent, child) {
16297
16341
  const path = relative(parent, child);
16298
16342
  return path === "" || !path.startsWith("..") && !isAbsolute(path);
16299
16343
  }
@@ -16325,14 +16369,14 @@ async function loadOne(installed, registry, options) {
16325
16369
  async function loadProviderPackage(input, options) {
16326
16370
  const importModule = options.importModule ?? ((url) => import(url));
16327
16371
  const packageDir = realpathSync(packageDirectory(options.packagesDir, input.package));
16328
- if (!inside(realpathSync(options.packagesDir), packageDir)) throw new Error("Installed provider package resolves outside the provider directory.");
16372
+ if (!inside$1(realpathSync(options.packagesDir), packageDir)) throw new Error("Installed provider package resolves outside the provider directory.");
16329
16373
  const packageJsonPath = join(packageDir, "package.json");
16330
16374
  const packageJson = installedPackageJsonSchema.parse(JSON.parse(readFileSync(packageJsonPath, "utf8")));
16331
16375
  if (packageJson.name !== input.package || packageJson.version !== input.version) throw new Error("Installed provider package does not match its saved record.");
16332
16376
  const packageRegistry = new ProviderExtensionRegistry();
16333
16377
  for (const entry of packageJson.seo.providers) {
16334
16378
  const entryPath = realpathSync(resolve(dirname(packageJsonPath), entry));
16335
- if (!inside(packageDir, entryPath)) throw new Error("Provider entry point resolves outside its package.");
16379
+ if (!inside$1(packageDir, entryPath)) throw new Error("Provider entry point resolves outside its package.");
16336
16380
  await providerActivate((await importModule(`${pathToFileURL(entryPath).href}?seo-provider=${encodeURIComponent(input.version)}`)).default)(packageRegistry.hostFor({
16337
16381
  package: input.package,
16338
16382
  version: input.version
@@ -16368,7 +16412,7 @@ async function loadInstalledProviderExtensions(options = {}) {
16368
16412
  const DEFAULT_MAX_REQUESTS = 8;
16369
16413
  const MAX_RESPONSE_BYTES$1 = 10 * 1024 * 1024;
16370
16414
  const MAX_REQUEST_BODY_BYTES = 64 * 1024;
16371
- const TIMEOUT_MS$2 = 15e3;
16415
+ const TIMEOUT_MS$3 = 15e3;
16372
16416
  function normalizeProviderExtensionAccount(provider, account) {
16373
16417
  const fields = provider.connection.fields.filter((field) => field.kind === "account");
16374
16418
  const allowed = new Set(fields.map((field) => field.id));
@@ -16415,7 +16459,7 @@ function runtimeForProvider(provider, options = {}) {
16415
16459
  },
16416
16460
  fetch: providerFetch,
16417
16461
  maxResponseBytes: MAX_RESPONSE_BYTES$1,
16418
- timeoutMs: TIMEOUT_MS$2,
16462
+ timeoutMs: TIMEOUT_MS$3,
16419
16463
  retry: (input.method ?? "GET") === "GET" ? "safe" : "never",
16420
16464
  schema: z.unknown()
16421
16465
  });
@@ -17996,7 +18040,7 @@ function analyzeCtrUnderperformersFromRows(input) {
17996
18040
  const MAX_GSC_ROWS$3 = 1e5;
17997
18041
  const DEFAULT_DAYS$3 = 28;
17998
18042
  const MAX_DAYS$2 = 548;
17999
- function plural$5(count, singular, pluralLabel = `${singular}s`) {
18043
+ function plural$6(count, singular, pluralLabel = `${singular}s`) {
18000
18044
  return count === 1 ? singular : pluralLabel;
18001
18045
  }
18002
18046
  async function ctrUnderperformersReportWith(input, dependencies) {
@@ -18099,11 +18143,11 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18099
18143
  minImpressions,
18100
18144
  limit,
18101
18145
  brandFiltering: input.includeBrand ? "included" : "excluded",
18102
- verdict: top ? `${selection.eligibleUnderperformers} CTR ${plural$5(selection.eligibleUnderperformers, "underperformer")} found in retained rows, with a calculated heuristic shortfall of ${totalClickShortfall.toFixed(0)} clicks for this window. Start review with "${top.query}" because it has the largest gap.` : dataStatus === "partial" ? "No material CTR underperformers remained in the validated retained rows, but partial evidence prevents an all-clear." : `No high-impression rows at average position below 4 in the retained rows are materially below the heuristic CTR benchmark.`
18146
+ verdict: top ? `${selection.eligibleUnderperformers} CTR ${plural$6(selection.eligibleUnderperformers, "underperformer")} found in retained rows, with a calculated heuristic shortfall of ${totalClickShortfall.toFixed(0)} clicks for this window. Start review with "${top.query}" because it has the largest gap.` : dataStatus === "partial" ? "No material CTR underperformers remained in the validated retained rows, but partial evidence prevents an all-clear." : `No high-impression rows at average position below 4 in the retained rows are materially below the heuristic CTR benchmark.`
18103
18147
  },
18104
18148
  items,
18105
18149
  caveats: [
18106
- `Date window: ${range.startDate} to ${range.endDate} (${days} ${plural$5(days, "day")}), using final GSC data where available.`,
18150
+ `Date window: ${range.startDate} to ${range.endDate} (${days} ${plural$6(days, "day")}), using final GSC data where available.`,
18107
18151
  `Brand queries: ${input.includeBrand ? "included" : "excluded"}. Brand terms used: ${brand.terms.join(", ") || "none"} (${brand.source === "provided" ? "provided" : "host forms, plus the bare domain stem only when Search Console shows the site is its navigational answer"}).`,
18108
18152
  `Only rows at average position below 4 with at least ${minImpressions} impressions were checked. Rows at position 4 to 10 belong to quick-wins, which uses the same site CTR curve.`,
18109
18153
  minImpressionsCaveat({
@@ -18111,13 +18155,13 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18111
18155
  belowMinimumRows: selection.belowMinimumRows
18112
18156
  }),
18113
18157
  "Expected CTR uses the site CTR curve from all page-one rows (75th percentile of qualified URL CTRs per position) when enough peer data exists, otherwise the fallback position curve.",
18114
- ...selection.fragmentRows ? [`${selection.fragmentRows} source ${plural$5(selection.fragmentRows, "row")} with a #fragment URL ${selection.fragmentRows === 1 ? "was" : "were"} merged into the base URL.`] : [],
18115
- ...selection.flaggedRows ? [`${selection.flaggedRows} eligible ${plural$5(selection.flaggedRows, "row")} carry heuristic query flags (likely-automated or navigational-other) and rank after unflagged rows.`] : [],
18158
+ ...selection.fragmentRows ? [`${selection.fragmentRows} source ${plural$6(selection.fragmentRows, "row")} with a #fragment URL ${selection.fragmentRows === 1 ? "was" : "were"} merged into the base URL.`] : [],
18159
+ ...selection.flaggedRows ? [`${selection.flaggedRows} eligible ${plural$6(selection.flaggedRows, "row")} carry heuristic query flags (likely-automated or navigational-other) and rank after unflagged rows.`] : [],
18116
18160
  "CTR benchmarks and calculated click shortfalls are directional heuristics, not forecasts or proof that clicks are available. Validate the search intent and live SERP before editing.",
18117
- ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$5(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
18118
- ...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$5(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
18161
+ ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$6(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
18162
+ ...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$6(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
18119
18163
  ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$3} was reached, so lower-ranked source rows may be absent.`] : [],
18120
- ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$5(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
18164
+ ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$6(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
18121
18165
  ],
18122
18166
  recommendations: top ? [
18123
18167
  `Review the live SERP and displayed snippet for "${top.query}" first. Test the title or meta description only if the observed framing does not match the query intent.`,
@@ -21952,7 +21996,7 @@ function anomalyEvidence(anomaly) {
21952
21996
  if (anomaly.direction === "normal") return `${formatMetric(anomaly.metric)} changed ${changePhrase} (${anomaly.baselineMean.toLocaleString("en-GB")} -> ${anomaly.comparisonMean.toLocaleString("en-GB")} per observed day${anomaly.zScore === null ? "; the baseline had no observed variance" : `, z ${anomaly.zScore}`}).`;
21953
21997
  return `${formatMetric(anomaly.metric)} ${direction} ${changePhrase} (${anomaly.baselineMean.toLocaleString("en-GB")} -> ${anomaly.comparisonMean.toLocaleString("en-GB")} per observed day${anomaly.zScore === null ? "; outside the flat observed baseline" : `, z ${anomaly.zScore}`}).`;
21954
21998
  }
21955
- function plural$4(count, singular, pluralLabel = `${singular}s`) {
21999
+ function plural$5(count, singular, pluralLabel = `${singular}s`) {
21956
22000
  return count === 1 ? singular : pluralLabel;
21957
22001
  }
21958
22002
  function strongestSignal(anomalies) {
@@ -22008,13 +22052,13 @@ function interpretUpdateCorrelation(input) {
22008
22052
  summary,
22009
22053
  evidence: [
22010
22054
  ...input.anomalies.map(anomalyEvidence),
22011
- hasUpdate ? `${input.overlappingUpdates.length} official Google Ranking update ${plural$4(input.overlappingUpdates.length, "window")} overlapped the change date or comparison window: ${updateNames.join(", ")}.` : "No official Google Ranking update windows overlapped the change date or comparison window.",
22055
+ hasUpdate ? `${input.overlappingUpdates.length} official Google Ranking update ${plural$5(input.overlappingUpdates.length, "window")} overlapped the change date or comparison window: ${updateNames.join(", ")}.` : "No official Google Ranking update windows overlapped the change date or comparison window.",
22012
22056
  largeMovement ? "Both clicks and impressions moved sharply in the same direction." : "Clicks and impressions did not both meet the sharp-movement evidence threshold.",
22013
22057
  hasConfounders ? `Known overlapping site changes: ${confounders.map((confounder) => confounder.title).join("; ")}.` : "No overlapping site changes were supplied."
22014
22058
  ],
22015
22059
  caveats: [
22016
22060
  `GSC window: ${input.days} days; recent comparison window: ${input.recentDays} days.`,
22017
- `Update overlap padding: ${input.paddingDays} ${plural$4(input.paddingDays, "day")} either side of the step-change date, or of the recent comparison window when no step change was found.`,
22061
+ `Update overlap padding: ${input.paddingDays} ${plural$5(input.paddingDays, "day")} either side of the step-change date, or of the recent comparison window when no step change was found.`,
22018
22062
  input.refresh ? "Data freshness: cache bypassed for GSC data." : "Data freshness: local GSC cache may have been used.",
22019
22063
  "Source: official Google Search Status Dashboard Ranking incidents only; third-party volatility and unconfirmed chatter are not included.",
22020
22064
  "An update-window overlap cannot establish causation, even when movement is large and no known site change was recorded.",
@@ -22182,7 +22226,7 @@ function analyzeTrafficAnomalyRows(input) {
22182
22226
  if (step && changeDate) {
22183
22227
  const previousDate = aggregated.rows[step.index - 1]?.keys[0];
22184
22228
  const gap = previousDate ? calendarDays(previousDate, changeDate) - 2 : 0;
22185
- stepCaveats.push(`${formatMetric(metric)} changed level on ${changeDate}, so its baseline is every observed day before that date and its comparison is every observed day from it.`, ...gap > 0 ? [`The ${metric} change date follows ${gap} missing ${plural$4(gap, "day")}; the change may have started on one of them.`] : []);
22229
+ stepCaveats.push(`${formatMetric(metric)} changed level on ${changeDate}, so its baseline is every observed day before that date and its comparison is every observed day from it.`, ...gap > 0 ? [`The ${metric} change date follows ${gap} missing ${plural$5(gap, "day")}; the change may have started on one of them.`] : []);
22186
22230
  return compareSegments({
22187
22231
  site: input.site,
22188
22232
  metric,
@@ -22213,9 +22257,9 @@ function analyzeTrafficAnomalyRows(input) {
22213
22257
  const comparisonMissingDays = input.recentDays - comparisonRows.length;
22214
22258
  const missingDays = expectedDays - aggregated.rows.length;
22215
22259
  const caveats = [
22216
- ...missingDays > 0 ? [`${missingDays} requested calendar ${plural$4(missingDays, "day")} had no returned date aggregate and ${missingDays === 1 ? "was" : "were"} not filled with zeros.`] : [],
22217
- ...aggregated.invalidRows > 0 ? [`${aggregated.invalidRows} invalid provider ${plural$4(aggregated.invalidRows, "row")} ${aggregated.invalidRows === 1 ? "was" : "were"} excluded.`] : [],
22218
- ...aggregated.duplicateRows > 0 ? [`${aggregated.duplicateRows} duplicate date ${plural$4(aggregated.duplicateRows, "row")} ${aggregated.duplicateRows === 1 ? "was" : "were"} aggregated before analysis.`] : [],
22260
+ ...missingDays > 0 ? [`${missingDays} requested calendar ${plural$5(missingDays, "day")} had no returned date aggregate and ${missingDays === 1 ? "was" : "were"} not filled with zeros.`] : [],
22261
+ ...aggregated.invalidRows > 0 ? [`${aggregated.invalidRows} invalid provider ${plural$5(aggregated.invalidRows, "row")} ${aggregated.invalidRows === 1 ? "was" : "were"} excluded.`] : [],
22262
+ ...aggregated.duplicateRows > 0 ? [`${aggregated.duplicateRows} duplicate date ${plural$5(aggregated.duplicateRows, "row")} ${aggregated.duplicateRows === 1 ? "was" : "were"} aggregated before analysis.`] : [],
22219
22263
  ...stepCaveats
22220
22264
  ];
22221
22265
  return {
@@ -23296,7 +23340,7 @@ const VOID = /* @__PURE__ */ new Set([
23296
23340
  "col",
23297
23341
  "embed"
23298
23342
  ]);
23299
- const ENTITIES = {
23343
+ const ENTITIES$1 = {
23300
23344
  amp: "&",
23301
23345
  lt: "<",
23302
23346
  gt: ">",
@@ -23321,7 +23365,7 @@ function decode(text) {
23321
23365
  const point = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
23322
23366
  return point > 0 && point <= 1114111 ? String.fromCodePoint(point) : "";
23323
23367
  }
23324
- return ENTITIES[code.toLowerCase()] ?? match;
23368
+ return ENTITIES$1[code.toLowerCase()] ?? match;
23325
23369
  });
23326
23370
  }
23327
23371
  const clean$1 = (value) => value.split(A_OPEN).join("").replace(/\s+/g, " ").trim();
@@ -23562,7 +23606,7 @@ var SentenceStore = class SentenceStore {
23562
23606
  });
23563
23607
  }
23564
23608
  };
23565
- const bareHost = (host) => host.toLowerCase().replace(/^www\./, "");
23609
+ const bareHost$1 = (host) => host.toLowerCase().replace(/^www\./, "");
23566
23610
  function crawlUrl(value, base) {
23567
23611
  try {
23568
23612
  const url = base ? new URL(value, base) : new URL(value);
@@ -23628,10 +23672,10 @@ async function crawlForLinks(input) {
23628
23672
  async function crawl(input, robots, sentences, renderer, warnings) {
23629
23673
  const start = crawlUrl(input.startUrl) ?? input.startUrl;
23630
23674
  let origin = new URL(start).origin;
23631
- const site = bareHost(new URL(start).host);
23675
+ const site = bareHost$1(new URL(start).host);
23632
23676
  const sameSite = (url) => {
23633
23677
  try {
23634
- return bareHost(new URL(url).host) === site;
23678
+ return bareHost$1(new URL(url).host) === site;
23635
23679
  } catch {
23636
23680
  return false;
23637
23681
  }
@@ -24125,15 +24169,15 @@ function internalLinksReportRange(input, fallbackDays, now) {
24125
24169
  days: Math.round((Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / DAY$1) + 1
24126
24170
  };
24127
24171
  }
24128
- const plural$3 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
24172
+ const plural$4 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
24129
24173
  function internalLinksVerdict(report) {
24130
24174
  const { counts } = report;
24131
24175
  if (report.dataStatus === "blocked") return "robots.txt does not allow this crawler to read the site, so nothing was checked.";
24132
24176
  if (report.dataStatus === "empty") return "No HTML page could be crawled, so there is nothing to link.";
24133
24177
  if (report.dataStatus === "target-not-crawled") return "The target page could not be crawled as its own page (it failed, left the site, or names another page as canonical). Check it with seo page first.";
24134
24178
  if (report.dataStatus === "target-not-linkable") return `The target is ${report.target?.noindex ? "noindex" : `a ${report.target?.type?.replaceAll("_", " ")} page`}, so no links to it are suggested.`;
24135
- const judged = report.judge.status === "used" || report.judge.status === "partial" ? `${counts.strong} strong and ${counts.possible} possible link${counts.strong + counts.possible === 1 ? "" : "s"} judged by ${report.judge.model}${counts.heuristic ? `, plus ${plural$3(counts.heuristic, "heuristic link")} where a request failed` : ""}` : `${plural$3(counts.heuristic, "heuristic link")} (no decision model)`;
24136
- const scope = report.mode === "url" ? `to ${report.target?.url}` : `across ${plural$3(counts.pagesCrawled, "page")}`;
24179
+ const judged = report.judge.status === "used" || report.judge.status === "partial" ? `${counts.strong} strong and ${counts.possible} possible link${counts.strong + counts.possible === 1 ? "" : "s"} judged by ${report.judge.model}${counts.heuristic ? `, plus ${plural$4(counts.heuristic, "heuristic link")} where a request failed` : ""}` : `${plural$4(counts.heuristic, "heuristic link")} (no decision model)`;
24180
+ const scope = report.mode === "url" ? `to ${report.target?.url}` : `across ${plural$4(counts.pagesCrawled, "page")}`;
24137
24181
  const capped = report.crawl.capReached ? ` The crawl stopped at its limit of ${report.crawl.pageLimit} pages, so pages beyond it were not checked.` : "";
24138
24182
  return `${judged} ${scope}${counts.heldBack ? `; ${counts.heldBack} held back by the evidence floors` : ""}.${capped}`;
24139
24183
  }
@@ -26574,14 +26618,14 @@ function* retainedQueryClusterRows(input) {
26574
26618
  };
26575
26619
  }
26576
26620
  }
26577
- function plural$2(count, singular, pluralLabel = `${singular}s`) {
26621
+ function plural$3(count, singular, pluralLabel = `${singular}s`) {
26578
26622
  return count === 1 ? singular : pluralLabel;
26579
26623
  }
26580
26624
  function reportVerdict(input) {
26581
26625
  if (!input.clusters.length) return "No query clusters were generated for this scope.";
26582
26626
  if (input.highOpportunityClusters > 0) {
26583
26627
  const verb = input.highOpportunityClusters === 1 ? "has" : "have";
26584
- return `${input.highOpportunityClusters} ${plural$2(input.highOpportunityClusters, "cluster")} ${verb} enough demand and weak enough performance to review first.`;
26628
+ return `${input.highOpportunityClusters} ${plural$3(input.highOpportunityClusters, "cluster")} ${verb} enough demand and weak enough performance to review first.`;
26585
26629
  }
26586
26630
  return "No obvious high-opportunity clusters were found. Use this report for content structure and intent mapping rather than urgent fixes.";
26587
26631
  }
@@ -26722,7 +26766,7 @@ function movementLine(report) {
26722
26766
  }
26723
26767
  function countPhrase(count, singular, pluralLabel = `${singular}s`) {
26724
26768
  if (count === 0) return `no ${pluralLabel}`;
26725
- return `${count} ${plural$1(count, singular, pluralLabel)}`;
26769
+ return `${count} ${plural$2(count, singular, pluralLabel)}`;
26726
26770
  }
26727
26771
  function headlineLine(report) {
26728
26772
  const parts = [
@@ -26777,7 +26821,7 @@ function decayDiagnosisLabel(diagnosis) {
26777
26821
  if (diagnosis === "lost_clicks") return "clicks dropped without one dominant signal";
26778
26822
  return diagnosis.replaceAll("_", " ");
26779
26823
  }
26780
- function plural$1(count, singular, pluralLabel = `${singular}s`) {
26824
+ function plural$2(count, singular, pluralLabel = `${singular}s`) {
26781
26825
  return count === 1 ? singular : pluralLabel;
26782
26826
  }
26783
26827
  function decayClusterLine(report) {
@@ -26785,7 +26829,7 @@ function decayClusterLine(report) {
26785
26829
  if (!group) return "No material decay cluster stood out.";
26786
26830
  const diagnosis = decayDiagnosisLabel(group.diagnosis);
26787
26831
  const samples = group.sampleQueries.slice(0, 2).join("; ");
26788
- return `Top decay cluster: ${group.count} ${plural$1(group.count, "finding")} in the ${group.template.label} template (${diagnosis}), ${group.totalClickLoss.toFixed(0)} fewer observed clicks across rows retained in both windows. Action: ${group.recommendation}${samples ? ` Example queries: ${samples}.` : ""}`;
26832
+ return `Top decay cluster: ${group.count} ${plural$2(group.count, "finding")} in the ${group.template.label} template (${diagnosis}), ${group.totalClickLoss.toFixed(0)} fewer observed clicks across rows retained in both windows. Action: ${group.recommendation}${samples ? ` Example queries: ${samples}.` : ""}`;
26789
26833
  }
26790
26834
  function cannibalSuppressionLine(report) {
26791
26835
  const suppressed = report.cannibalization.selection.suppressedQueries;
@@ -26803,21 +26847,21 @@ function contentOpportunityBullets(report) {
26803
26847
  if (unavailableSections.length) bullets.push(`Could not assess ${unavailableSections.join(", ")} opportunities in this run; see report caveats.`);
26804
26848
  if (report.summary.decayItems) {
26805
26849
  const top = report.decay.items[0];
26806
- bullets.push(top ? `${report.summary.decayItems} observed retained query/page ${plural$1(report.summary.decayItems, "decline")} need review. Start with "${top.query}" on ${top.url}; it had ${top.clickLoss.toFixed(0)} fewer clicks.` : `${report.summary.decayItems} observed retained query/page ${plural$1(report.summary.decayItems, "decline")} need review.`);
26850
+ bullets.push(top ? `${report.summary.decayItems} observed retained query/page ${plural$2(report.summary.decayItems, "decline")} need review. Start with "${top.query}" on ${top.url}; it had ${top.clickLoss.toFixed(0)} fewer clicks.` : `${report.summary.decayItems} observed retained query/page ${plural$2(report.summary.decayItems, "decline")} need review.`);
26807
26851
  bullets.push(decayClusterLine(report));
26808
26852
  }
26809
26853
  if (report.summary.cannibalItems) {
26810
26854
  const top = report.cannibalization.items[0];
26811
- bullets.push(top ? `${report.summary.cannibalItems} multi-URL query ${plural$1(report.summary.cannibalItems, "candidate")} need intent and technical review. Start with "${top.query}" and decide whether ${top.pages.length} ranking URLs answer the same intent.` : `${report.summary.cannibalItems} multi-URL query ${plural$1(report.summary.cannibalItems, "candidate")} need intent and technical review.`);
26855
+ bullets.push(top ? `${report.summary.cannibalItems} multi-URL query ${plural$2(report.summary.cannibalItems, "candidate")} need intent and technical review. Start with "${top.query}" and decide whether ${top.pages.length} ranking URLs answer the same intent.` : `${report.summary.cannibalItems} multi-URL query ${plural$2(report.summary.cannibalItems, "candidate")} need intent and technical review.`);
26812
26856
  }
26813
26857
  if (report.cannibalization.selection.suppressedQueries) bullets.push(cannibalSuppressionLine(report));
26814
26858
  if (report.summary.strikingDistanceItems) {
26815
26859
  const top = report.strikingDistance.items[0];
26816
- bullets.push(top ? `${countLabel(report.summary.strikingDistanceItems, "query/page candidate")} ${plural$1(report.summary.strikingDistanceItems, "has", "have")} an average GSC position above 10 and at most 20. Start by investigating "${top.query}" on ${top.url}; it averages position ${top.position.toFixed(1)} with ${top.impressions.toFixed(0)} impressions.` : `${countLabel(report.summary.strikingDistanceItems, "query/page candidate")} ${plural$1(report.summary.strikingDistanceItems, "has", "have")} an average GSC position above 10 and at most 20.`);
26860
+ bullets.push(top ? `${countLabel(report.summary.strikingDistanceItems, "query/page candidate")} ${plural$2(report.summary.strikingDistanceItems, "has", "have")} an average GSC position above 10 and at most 20. Start by investigating "${top.query}" on ${top.url}; it averages position ${top.position.toFixed(1)} with ${top.impressions.toFixed(0)} impressions.` : `${countLabel(report.summary.strikingDistanceItems, "query/page candidate")} ${plural$2(report.summary.strikingDistanceItems, "has", "have")} an average GSC position above 10 and at most 20.`);
26817
26861
  }
26818
26862
  if (!skippedReason(report, "quick-win opportunities") && report.summary.quickWinItems) {
26819
26863
  const top = report.quickWins.items[0];
26820
- bullets.push(top ? `${report.summary.quickWinItems} retained query/page ${plural$1(report.summary.quickWinItems, "candidate")} fell below a site-peer or versioned heuristic CTR target. Start with "${top.query}" on ${top.url}; it had ${top.impressions.toFixed(0)} impressions at average position ${top.position.toFixed(1)}.` : `${report.summary.quickWinItems} CTR-target ${plural$1(report.summary.quickWinItems, "candidate")} need review.`);
26864
+ bullets.push(top ? `${report.summary.quickWinItems} retained query/page ${plural$2(report.summary.quickWinItems, "candidate")} fell below a site-peer or versioned heuristic CTR target. Start with "${top.query}" on ${top.url}; it had ${top.impressions.toFixed(0)} impressions at average position ${top.position.toFixed(1)}.` : `${report.summary.quickWinItems} CTR-target ${plural$2(report.summary.quickWinItems, "candidate")} need review.`);
26821
26865
  }
26822
26866
  const templateLine = templateOpportunityLine(report);
26823
26867
  if (!templateLine.startsWith("No dominant")) bullets.push(templateLine);
@@ -28539,11 +28583,12 @@ function ga4MatchReason(match, site) {
28539
28583
  //#endregion
28540
28584
  //#region packages/core/src/commands/clicky.ts
28541
28585
  const CLICKY_KEY_PREFIX = "x-clicky-";
28586
+ const MAX_INSPECT_LIMIT = 2e3;
28542
28587
  //#endregion
28543
28588
  //#region packages/core/src/commands/page-facts.ts
28544
28589
  const MAX_PAGES = 20;
28545
- const TIMEOUT_MS$1 = 6e3;
28546
- const MAX_BYTES$1 = 1e6;
28590
+ const TIMEOUT_MS$2 = 6e3;
28591
+ const MAX_BYTES$2 = 1e6;
28547
28592
  const clean = (value) => value?.trim().replace(/\s+/g, " ") || null;
28548
28593
  async function boundedText(response) {
28549
28594
  if (!response.body) return "";
@@ -28551,7 +28596,7 @@ async function boundedText(response) {
28551
28596
  const decoder = new TextDecoder();
28552
28597
  let text = "";
28553
28598
  let total = 0;
28554
- while (total < MAX_BYTES$1) {
28599
+ while (total < MAX_BYTES$2) {
28555
28600
  const { done, value } = await reader.read();
28556
28601
  if (done) return text + decoder.decode();
28557
28602
  total += value.byteLength;
@@ -28631,12 +28676,14 @@ async function facts(url) {
28631
28676
  "user-agent": SEO_CRAWLER_USER_AGENT,
28632
28677
  accept: "text/html"
28633
28678
  },
28634
- signal: AbortSignal.timeout(TIMEOUT_MS$1)
28679
+ signal: AbortSignal.timeout(TIMEOUT_MS$2)
28635
28680
  });
28636
28681
  out.status = response.status;
28637
28682
  const location = response.headers.get("location");
28638
28683
  if (location) out.redirects_to = new URL(location, url).toString();
28639
28684
  const headerRobots = response.headers.get("x-robots-tag");
28685
+ const auth = response.headers.get("www-authenticate");
28686
+ if (auth) out.www_authenticate = auth.slice(0, 200);
28640
28687
  if (response.status !== 200 || !response.headers.get("content-type")?.includes("html")) {
28641
28688
  out.robots = headerRobots;
28642
28689
  await response.body?.cancel();
@@ -28653,6 +28700,37 @@ async function facts(url) {
28653
28700
  function checkPages(urls) {
28654
28701
  return Promise.all(urls.slice(0, MAX_PAGES).map(facts));
28655
28702
  }
28703
+ const MAX_HOPS = 5;
28704
+ const redirectKind = (status) => status === 301 || status === 308 ? "permanent" : status === 302 || status === 303 || status === 307 ? "temporary" : null;
28705
+ async function liveCheck(url) {
28706
+ const first = await facts(url);
28707
+ const hops = [];
28708
+ const seen = /* @__PURE__ */ new Set([url]);
28709
+ let last = first;
28710
+ while (last.redirects_to && last.status !== null) {
28711
+ const kind = redirectKind(last.status);
28712
+ if (!kind) break;
28713
+ const to = last.redirects_to;
28714
+ hops.push({
28715
+ from: last.url,
28716
+ status: last.status,
28717
+ to,
28718
+ kind
28719
+ });
28720
+ if (seen.has(to) || hops.length > MAX_HOPS) return {
28721
+ first,
28722
+ hops,
28723
+ last: null
28724
+ };
28725
+ seen.add(to);
28726
+ last = await facts(to);
28727
+ }
28728
+ return {
28729
+ first,
28730
+ hops,
28731
+ last
28732
+ };
28733
+ }
28656
28734
  //#endregion
28657
28735
  //#region packages/core/src/commands/report.ts
28658
28736
  const SECTIONS = {
@@ -28685,16 +28763,23 @@ const RESEARCH_HELP = `keyword_metrics (keywords: volume, difficulty, CPC), rank
28685
28763
  function indexStatusFrom(result) {
28686
28764
  const status = result?.inspectionResult?.indexStatusResult ?? result?.indexStatusResult ?? {};
28687
28765
  const text = (value) => typeof value === "string" ? value : null;
28766
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
28688
28767
  return {
28689
28768
  verdict: text(status.verdict),
28690
28769
  coverage: text(status.coverageState),
28691
28770
  indexing: text(status.indexingState),
28692
28771
  last_crawl: text(status.lastCrawlTime),
28693
28772
  google_canonical: text(status.googleCanonical),
28694
- user_canonical: text(status.userCanonical)
28773
+ user_canonical: text(status.userCanonical),
28774
+ page_fetch: text(status.pageFetchState),
28775
+ robots_txt: text(status.robotsTxtState),
28776
+ crawled_as: text(status.crawledAs),
28777
+ referring_urls: list(status.referringUrls),
28778
+ sitemaps: list(status.sitemap)
28695
28779
  };
28696
28780
  }
28697
28781
  const LOST_PAGES_RULE = `Pages that had at least 3 clicks in the previous 28 days and kept ${.2 * 100}% or less of them in the last 28 days, with a live check of the top 20 and Google’s index status for the top 10 and the home page.`;
28782
+ const INDEXING_RULE = `Google’s indexing reasons for 100 known URLs a run in a fixed order (lost-click pages, Search Console pages by clicks then impressions, sitemap URLs, then the rest), each checked live and labelled fix, check, expected or indexed from the evidence, sorted by reason then URL.`;
28698
28783
  //#endregion
28699
28784
  //#region packages/core/src/commands/tools.ts
28700
28785
  const site = z.string().min(1).describe("Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.");
@@ -28729,9 +28814,15 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
28729
28814
  site,
28730
28815
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD: the first day after the change."),
28731
28816
  pages: z.string().min(1).optional().describe("Optional URL text, such as /blog/.")
28732
- }), `${LOST_PAGES_RULE}`, z.object({
28817
+ }), `${INDEXING_RULE}${LOST_PAGES_RULE}`, z.object({
28818
+ site,
28819
+ ...dates,
28820
+ ...detail
28821
+ }), z.object({
28733
28822
  site,
28734
- ...dates
28823
+ ...dates,
28824
+ max_urls: z.number().int().min(1).max(MAX_INSPECT_LIMIT).optional().describe(`URLs to inspect with Google, 100 by default, up to the URL Inspection quota left today.`),
28825
+ ...detail
28735
28826
  }), `${RESEARCH_HELP}`, z.object({
28736
28827
  provider: z.enum(RESEARCH_PROVIDERS),
28737
28828
  action: z.enum(RESEARCH_ACTIONS),
@@ -28749,6 +28840,36 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
28749
28840
  ...detail
28750
28841
  });
28751
28842
  //#endregion
28843
+ //#region packages/core/src/gsc/client/inspection-cache.ts
28844
+ const INSPECTION_FRESH_MS = 7 * 864e5;
28845
+ function readSavedInspections(site, urls, now = Date.now()) {
28846
+ const found = /* @__PURE__ */ new Map();
28847
+ try {
28848
+ const statement = cacheDb().prepare(`SELECT status_json, fetched_at FROM url_inspection_cache
28849
+ WHERE site_url = ? AND url = ? AND fetched_at > ?`);
28850
+ for (const url of urls) {
28851
+ const row = statement.get(site, url, now - INSPECTION_FRESH_MS);
28852
+ if (!row) continue;
28853
+ found.set(url, {
28854
+ status: JSON.parse(row.status_json),
28855
+ inspectedAt: new Date(row.fetched_at).toISOString()
28856
+ });
28857
+ }
28858
+ } catch {
28859
+ return /* @__PURE__ */ new Map();
28860
+ }
28861
+ return found;
28862
+ }
28863
+ function saveInspection(site, url, status, now = Date.now()) {
28864
+ const json = JSON.stringify(status);
28865
+ writeToCache((database) => {
28866
+ database.prepare(`INSERT OR REPLACE INTO url_inspection_cache
28867
+ (site_url, url, status_json, fetched_at, expires_at)
28868
+ VALUES (?, ?, ?, ?, ?)`).run(site, url, json, now, now + INSPECTION_FRESH_MS);
28869
+ noteCacheWrite(Buffer.byteLength(json) + url.length, database);
28870
+ });
28871
+ }
28872
+ //#endregion
28752
28873
  //#region packages/core/src/providers/api-key-credentials.ts
28753
28874
  function createApiKeyCredentialStore(input) {
28754
28875
  const normalize = (value) => {
@@ -28944,16 +29065,19 @@ function nodeCommandDeps(options) {
28944
29065
  accountEmail: options.account
28945
29066
  }),
28946
29067
  checkPages,
28947
- inspect: async (site, url) => {
28948
- try {
28949
- return indexStatusFrom(await inspectUrl({
28950
- siteUrl: site,
28951
- inspectionUrl: url,
28952
- accountEmail: options.account
28953
- }));
28954
- } catch {
28955
- return null;
28956
- }
29068
+ liveCheck,
29069
+ inspect: async (site, url) => indexStatusFrom(await inspectUrl({
29070
+ siteUrl: site,
29071
+ inspectionUrl: url,
29072
+ accountEmail: options.account
29073
+ })),
29074
+ inspectionQuota: (site) => urlInspectionQuota({
29075
+ siteUrl: site,
29076
+ accountEmail: options.account
29077
+ }),
29078
+ savedInspections: {
29079
+ read: (site, urls) => options.refresh ? /* @__PURE__ */ new Map() : readSavedInspections(site, urls),
29080
+ save: saveInspection
28957
29081
  },
28958
29082
  pageMarkdown: async (url) => extractMainMarkdown(await fetchPage(url, { refresh: options.refresh })),
28959
29083
  call: options.call,
@@ -30730,6 +30854,6 @@ function isoDate(daysAgo = 0) {
30730
30854
  return date.toISOString().slice(0, 10);
30731
30855
  }
30732
30856
  //#endregion
30733
- 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_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, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, 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_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, 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, 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, cacheDb, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, 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, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, 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, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, 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, 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, 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, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
30857
+ 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_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, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, 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_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, 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, 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, cacheDb, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, 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, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, 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, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, 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, 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, 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, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
30734
30858
 
30735
30859
  //# sourceMappingURL=index.js.map