seo 0.10.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-CM7xTOjT.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";
@@ -27,6 +27,7 @@ import { TextDecoder as TextDecoder$1, promisify } from "node:util";
27
27
  import { execFile } from "node:child_process";
28
28
  import pino from "pino";
29
29
  import pretty from "pino-pretty";
30
+ import { request } from "node:https";
30
31
  //#region packages/core/src/analytics/providers.ts
31
32
  const ANALYTICS_PROVIDER_IDS = ["google", "clicky"];
32
33
  const ANALYTICS_PROVIDER_DETAILS = {
@@ -1613,7 +1614,7 @@ async function googleJson(gfetch, url, input) {
1613
1614
  return readJson(response, input.label, { maxBytes: input.maxBytes });
1614
1615
  }
1615
1616
  const GSC = "https://www.googleapis.com/webmasters/v3";
1616
- const PAGE_ROWS = 25e3;
1617
+ const PAGE_ROWS$1 = 25e3;
1617
1618
  async function searchAnalyticsRows(gfetch, site, body, options) {
1618
1619
  const rows = [];
1619
1620
  const { maxRows: requested, ...apiBody } = body;
@@ -1624,7 +1625,7 @@ async function searchAnalyticsRows(gfetch, site, body, options) {
1624
1625
  while (true) {
1625
1626
  const remaining = typeof maxRows === "number" ? maxRows - rows.length : void 0;
1626
1627
  if (typeof remaining === "number" && remaining <= 0) break;
1627
- 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);
1628
1629
  const json = await googleJson(gfetch, url, {
1629
1630
  ...options,
1630
1631
  label: "Search Console",
@@ -1701,7 +1702,7 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1701
1702
  //#endregion
1702
1703
  //#region packages/core/src/bing/api.ts
1703
1704
  const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1704
- const MAX_BYTES$2 = 8e6;
1705
+ const MAX_BYTES$4 = 8e6;
1705
1706
  function bingCall(auth, options) {
1706
1707
  return async (method, params = {}) => {
1707
1708
  const url = new URL(`${BASE}/${method}`);
@@ -1726,7 +1727,7 @@ function bingCall(auth, options) {
1726
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.");
1727
1728
  throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
1728
1729
  }
1729
- return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$2 })).d;
1730
+ return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$4 })).d;
1730
1731
  };
1731
1732
  }
1732
1733
  //#endregion
@@ -4262,6 +4263,7 @@ const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
4262
4263
  const CACHE_MAX_AGE_MS = 720 * 60 * 60 * 1e3;
4263
4264
  const CACHE_LIMITS = {
4264
4265
  gsc_cache: 48 * MEBIBYTE$2,
4266
+ url_inspection_cache: 16 * MEBIBYTE$2,
4265
4267
  ga4_cache: 24 * MEBIBYTE$2,
4266
4268
  provider_cache: 32 * MEBIBYTE$2,
4267
4269
  http_cache: 128 * MEBIBYTE$2,
@@ -4279,6 +4281,16 @@ const CACHE_POLICIES = [
4279
4281
  "response_json"
4280
4282
  ]
4281
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
+ },
4282
4294
  {
4283
4295
  table: "ga4_cache",
4284
4296
  keys: ["property_id", "query_hash"],
@@ -4582,6 +4594,17 @@ CREATE INDEX IF NOT EXISTS idx_url_inspection_reservations_minute
4582
4594
  credential_key, property_site_url, created_at
4583
4595
  );
4584
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
+
4585
4608
  CREATE TABLE IF NOT EXISTS performance_reports (
4586
4609
  id TEXT PRIMARY KEY,
4587
4610
  url TEXT NOT NULL,
@@ -4614,10 +4637,12 @@ function initDb(database, isNewDatabase) {
4614
4637
  database.exec("DROP TABLE IF EXISTS provider_spend_notices");
4615
4638
  ensureColumn(database, "http_cache", "metadata_json", "TEXT");
4616
4639
  }).immediate();
4617
- runCacheMaintenance(database, {
4618
- compact: true,
4619
- allowFullVacuum: true
4620
- });
4640
+ try {
4641
+ runCacheMaintenance(database, {
4642
+ compact: true,
4643
+ allowFullVacuum: true
4644
+ });
4645
+ } catch {}
4621
4646
  }
4622
4647
  function getDb() {
4623
4648
  if (db) return db;
@@ -4628,12 +4653,41 @@ function getDb() {
4628
4653
  mode: 448
4629
4654
  });
4630
4655
  ensurePrivateDatabaseFiles(path);
4631
- db = new SqliteDatabase(path);
4632
- ensurePrivateDatabaseFiles(path);
4633
- initDb(db, isNewDatabase);
4634
- ensurePrivateDatabaseFiles(path);
4656
+ const opened = new SqliteDatabase(path);
4657
+ try {
4658
+ ensurePrivateDatabaseFiles(path);
4659
+ initDb(opened, isNewDatabase);
4660
+ ensurePrivateDatabaseFiles(path);
4661
+ } catch (error) {
4662
+ opened.close();
4663
+ throw error;
4664
+ }
4665
+ db = opened;
4635
4666
  return db;
4636
4667
  }
4668
+ let memoryDb;
4669
+ function memoryCache() {
4670
+ if (!memoryDb) {
4671
+ memoryDb = new SqliteDatabase(":memory:");
4672
+ memoryDb.exec(CREATE_SQL);
4673
+ }
4674
+ return memoryDb;
4675
+ }
4676
+ function cacheDb() {
4677
+ if (memoryDb) return memoryDb;
4678
+ try {
4679
+ return getDb();
4680
+ } catch {
4681
+ return memoryCache();
4682
+ }
4683
+ }
4684
+ function writeToCache(write) {
4685
+ try {
4686
+ write(cacheDb());
4687
+ } catch {
4688
+ memoryCache();
4689
+ }
4690
+ }
4637
4691
  function noteCacheWrite(sizeBytes = 0, database = getDb()) {
4638
4692
  const counter = cacheMaintenanceCounters.get(database) ?? {
4639
4693
  writes: 0,
@@ -4662,6 +4716,7 @@ function getCacheStats() {
4662
4716
  const counts = {
4663
4717
  sites: database.prepare("SELECT COUNT(*) AS count FROM sites").get(),
4664
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(),
4665
4720
  google_analytics_cache: database.prepare("SELECT COUNT(*) AS count FROM ga4_cache").get(),
4666
4721
  provider_cache: { count: providerCount("dataforseo") },
4667
4722
  ahrefs_cache: { count: providerCount("ahrefs") },
@@ -4693,8 +4748,9 @@ function clearCache(provider, olderThanMs) {
4693
4748
  compactCacheDatabase(database, { allowFullVacuum: true });
4694
4749
  return info.changes;
4695
4750
  }
4696
- 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"] : [
4697
4752
  "gsc_cache",
4753
+ "url_inspection_cache",
4698
4754
  "ga4_cache",
4699
4755
  "provider_cache",
4700
4756
  "http_cache"
@@ -4715,7 +4771,7 @@ function ga4RequestCanUseCache(body) {
4715
4771
  }
4716
4772
  async function runGa4Report(propertyId, body, opts = {}) {
4717
4773
  const { client, identity } = await createGoogleAccessTokenClient(opts.accountEmail);
4718
- const db = getDb();
4774
+ const db = cacheDb();
4719
4775
  const queryHash = hashKey([
4720
4776
  propertyId,
4721
4777
  identity,
@@ -4728,10 +4784,12 @@ async function runGa4Report(propertyId, body, opts = {}) {
4728
4784
  if (cacheable) {
4729
4785
  const requestJson = JSON.stringify(body);
4730
4786
  const responseJson = JSON.stringify(result);
4731
- db.prepare(`INSERT OR REPLACE INTO ga4_cache
4732
- (property_id, query_hash, request_json, response_json, row_count, fetched_at, expires_at)
4733
- VALUES (?, ?, ?, ?, ?, ?, ?)`).run(propertyId, queryHash, requestJson, responseJson, result.rowCount ?? result.rows?.length ?? 0, Date.now(), Date.now() + 864e5);
4734
- noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson));
4787
+ writeToCache((database) => {
4788
+ database.prepare(`INSERT OR REPLACE INTO ga4_cache
4789
+ (property_id, query_hash, request_json, response_json, row_count, fetched_at, expires_at)
4790
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(propertyId, queryHash, requestJson, responseJson, result.rowCount ?? result.rows?.length ?? 0, Date.now(), Date.now() + 864e5);
4791
+ noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson), database);
4792
+ });
4735
4793
  }
4736
4794
  return result;
4737
4795
  }
@@ -7190,7 +7248,7 @@ async function fetchRobotsSource(origin, refresh, options) {
7190
7248
  const robotsUrl = new URL("/robots.txt", origin).toString();
7191
7249
  const key = hashKey(["robots", robotsUrl]);
7192
7250
  if (!refresh) {
7193
- const cached = getDb().prepare("SELECT status, body_blob, expires_at FROM http_cache WHERE url_hash = ?").get(key);
7251
+ const cached = cacheDb().prepare("SELECT status, body_blob, expires_at FROM http_cache WHERE url_hash = ?").get(key);
7194
7252
  if (cached?.body_blob && cached.status !== void 0 && cacheableStatus(cached.status) && cached.expires_at && cached.expires_at > Date.now()) return {
7195
7253
  robotsUrl,
7196
7254
  status: cached.status,
@@ -7209,12 +7267,12 @@ async function fetchRobotsSource(origin, refresh, options) {
7209
7267
  signal: controller.signal
7210
7268
  });
7211
7269
  const text = await readBoundedResponseText(response, MAX_ROBOTS_RESPONSE_BYTES, "robots.txt response");
7212
- if (options.writeCache !== false && cacheableStatus(response.status)) {
7213
- getDb().prepare(`INSERT OR REPLACE INTO http_cache
7214
- (url_hash, url, status, headers_json, body_blob, etag, fetched_at, expires_at)
7215
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(key, robotsUrl, response.status, "{}", Buffer.from(text), null, Date.now(), Date.now() + 864e5);
7216
- noteCacheWrite(Buffer.byteLength(text));
7217
- }
7270
+ if (options.writeCache !== false && cacheableStatus(response.status)) writeToCache((database) => {
7271
+ database.prepare(`INSERT OR REPLACE INTO http_cache
7272
+ (url_hash, url, status, headers_json, body_blob, etag, fetched_at, expires_at)
7273
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(key, robotsUrl, response.status, "{}", Buffer.from(text), null, Date.now(), Date.now() + 864e5);
7274
+ noteCacheWrite(Buffer.byteLength(text), database);
7275
+ });
7218
7276
  return {
7219
7277
  robotsUrl,
7220
7278
  status: response.status,
@@ -7321,7 +7379,6 @@ function decodePageFetchCacheEvidence(value) {
7321
7379
  }
7322
7380
  async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, respectRobots = false, writeCache = true) {
7323
7381
  const startedAt = Date.now();
7324
- const db = getDb();
7325
7382
  const key = hashKey(["page", url]);
7326
7383
  const host = new URL(url).host;
7327
7384
  let robots = respectRobots ? await fetchRobots(new URL(url).origin, url, refresh, {
@@ -7329,7 +7386,7 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
7329
7386
  signal
7330
7387
  }) : void 0;
7331
7388
  if (robots && robots.allowed !== true) throw new RobotsAccessError(robots.allowed === false ? "robots-disallowed" : "robots-deferred", robots);
7332
- const cached = db.prepare("SELECT status, headers_json, body_blob, metadata_json, expires_at FROM http_cache WHERE url_hash = ? AND expires_at > ?").get(key, Date.now());
7389
+ const cached = cacheDb().prepare("SELECT status, headers_json, body_blob, metadata_json, expires_at FROM http_cache WHERE url_hash = ? AND expires_at > ?").get(key, Date.now());
7333
7390
  if (!refresh && cached) {
7334
7391
  const evidence = decodePageFetchCacheEvidence(cached.metadata_json);
7335
7392
  return {
@@ -7431,12 +7488,12 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
7431
7488
  warnings: [...robots.allowed === null ? [`robots.txt availability is unknown${robots.status ? ` after HTTP ${robots.status}` : ""}; this fetch does not prove crawler access is allowed.`] : [], ...accessBlock ? [`${accessBlock.guidance.summary} Identify the crawler as ${accessBlock.crawler.userAgent}.`] : []],
7432
7489
  robotsTxt: pageRobotsEvidence(robots)
7433
7490
  };
7434
- if (!accessBlock && writeCache) {
7435
- db.prepare(`INSERT OR REPLACE INTO http_cache
7436
- (url_hash, url, status, headers_json, body_blob, metadata_json, etag, fetched_at, expires_at)
7437
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(key, url, response.response.status, JSON.stringify(headerMap), Buffer.from(html), encodePageFetchCacheEvidence(result), response.response.headers.get("etag"), Date.now(), Date.now() + 36e5);
7438
- noteCacheWrite(Buffer.byteLength(html));
7439
- }
7491
+ if (!accessBlock && writeCache) writeToCache((database) => {
7492
+ database.prepare(`INSERT OR REPLACE INTO http_cache
7493
+ (url_hash, url, status, headers_json, body_blob, metadata_json, etag, fetched_at, expires_at)
7494
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(key, url, response.response.status, JSON.stringify(headerMap), Buffer.from(html), encodePageFetchCacheEvidence(result), response.response.headers.get("etag"), Date.now(), Date.now() + 36e5);
7495
+ noteCacheWrite(Buffer.byteLength(html), database);
7496
+ });
7440
7497
  return result;
7441
7498
  }
7442
7499
  //#endregion
@@ -8189,6 +8246,19 @@ function reserveUrlInspectionQuota(input) {
8189
8246
  if ("error" in result) throw result.error;
8190
8247
  return result.reservation;
8191
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
+ }
8192
8262
  function finalizeUrlInspectionQuota(input) {
8193
8263
  const database = input.database ?? getDb();
8194
8264
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -8233,6 +8303,13 @@ function inspectionResponseError(response, input) {
8233
8303
  if (response.status >= 500) return new SeoError("PROVIDER_UNAVAILABLE", `Google URL Inspection is unavailable (${response.status}). Try again later.`);
8234
8304
  return new SeoError("PROVIDER_UNAVAILABLE", `URL Inspection failed with ${response.status}.`);
8235
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
+ }
8236
8313
  async function inspectUrl(input) {
8237
8314
  const inspectionUrl = assertUrlMatchesGscProperty(input.siteUrl, input.inspectionUrl);
8238
8315
  const { client, quotaIdentity } = await getAuthorized(input.accountEmail ?? await cloudAccountFor(input.siteUrl));
@@ -8294,7 +8371,7 @@ function resolveSearchDateWindow(input = 28) {
8294
8371
  }
8295
8372
  async function querySearchAnalytics(site, body, opts = {}) {
8296
8373
  const { client, identity } = await getAuthorized(opts.accountEmail ?? await cloudAccountFor(site));
8297
- const db = getDb();
8374
+ const db = cacheDb();
8298
8375
  const queryHash = hashKey([
8299
8376
  site,
8300
8377
  identity,
@@ -8314,10 +8391,12 @@ async function querySearchAnalytics(site, body, opts = {}) {
8314
8391
  const calls = result.calls;
8315
8392
  const requestJson = JSON.stringify(body);
8316
8393
  const responseJson = JSON.stringify(allRows);
8317
- db.prepare(`INSERT OR REPLACE INTO gsc_cache
8318
- (site_url, query_hash, request_json, response_json, row_count, fetched_at, expires_at)
8319
- VALUES (?, ?, ?, ?, ?, ?, ?)`).run(site, queryHash, requestJson, responseJson, allRows.length, Date.now(), Date.now() + 864e5);
8320
- noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson));
8394
+ writeToCache((database) => {
8395
+ database.prepare(`INSERT OR REPLACE INTO gsc_cache
8396
+ (site_url, query_hash, request_json, response_json, row_count, fetched_at, expires_at)
8397
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(site, queryHash, requestJson, responseJson, allRows.length, Date.now(), Date.now() + 864e5);
8398
+ noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson), database);
8399
+ });
8321
8400
  return {
8322
8401
  rows: allRows,
8323
8402
  calls,
@@ -8453,8 +8532,10 @@ async function queryPagesTopQueriesBatch(site, pageUrls, range = 28) {
8453
8532
  //#region packages/core/src/gsc/client/sites.ts
8454
8533
  async function listSites(_refresh = false, accountEmail) {
8455
8534
  const entries = !accountEmail && cloudLinked() ? await allCloudSites() : await listSearchConsoleSites(googleFetchFor((await getAuthorized(accountEmail)).client), LOCAL_SIGN_IN);
8456
- const insert = getDb().prepare("INSERT OR REPLACE INTO sites (site_url, display_name, permission, added_at, is_default) VALUES (?, ?, ?, ?, COALESCE((SELECT is_default FROM sites WHERE site_url = ?), 0))");
8457
- for (const site of entries) insert.run(site.siteUrl, site.siteUrl, site.permissionLevel ?? null, Date.now(), site.siteUrl);
8535
+ writeToCache((database) => {
8536
+ const insert = database.prepare("INSERT OR REPLACE INTO sites (site_url, display_name, permission, added_at, is_default) VALUES (?, ?, ?, ?, COALESCE((SELECT is_default FROM sites WHERE site_url = ?), 0))");
8537
+ for (const site of entries) insert.run(site.siteUrl, site.siteUrl, site.permissionLevel ?? null, Date.now(), site.siteUrl);
8538
+ });
8458
8539
  return entries;
8459
8540
  }
8460
8541
  async function allCloudSites() {
@@ -10132,7 +10213,7 @@ const actionPriority = new Map([
10132
10213
  "unverified",
10133
10214
  "covered"
10134
10215
  ].map((type, index) => [type, index]));
10135
- function plural$6(count, singular, pluralLabel) {
10216
+ function plural$7(count, singular, pluralLabel) {
10136
10217
  return count === 1 ? singular : pluralLabel;
10137
10218
  }
10138
10219
  function actionable(item) {
@@ -10166,7 +10247,7 @@ function pageOpportunityVerdict(input) {
10166
10247
  if (focus === "unverified") return "GSC visibility exists, but the page was not verified and no specific change is justified yet.";
10167
10248
  if (focus === "covered") return "The verified page covers the sampled queries and CTR is not materially below its directional benchmark.";
10168
10249
  const count = analysis.summary.opportunities;
10169
- const prefix = `${count} ${plural$6(count, "actionable observation", "actionable observations")} found.`;
10250
+ const prefix = `${count} ${plural$7(count, "actionable observation", "actionable observations")} found.`;
10170
10251
  if (focus === "content-gap") return `${prefix} Verified body coverage is the main issue; confirm intent before expanding the page.`;
10171
10252
  if (focus === "serp-framing") return `${prefix} The body is relevant, but its title, meta description, or H1 framing is weak.`;
10172
10253
  if (focus === "ranking") return `${prefix} Most upside requires stronger relevance or authority; no CTR-only lift is claimed for page-two queries.`;
@@ -10181,7 +10262,7 @@ function pageOpportunityRecommendations(analysis) {
10181
10262
  }
10182
10263
  const recommendations = [`Start with "${top.query}". ${top.recommendation}`];
10183
10264
  const technical = actions.filter((item) => item.opportunityType === "technical-check").length;
10184
- 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.`);
10185
10266
  const gaps = actions.filter((item) => item.opportunityType === "content-gap").length;
10186
10267
  if (gaps >= 3) recommendations.push(`Group the ${gaps} verified content gaps into useful sections rather than adding one thin paragraph per query.`);
10187
10268
  return recommendations;
@@ -14185,7 +14266,7 @@ function availabilityOf(status) {
14185
14266
  function errorText(error) {
14186
14267
  return error instanceof Error ? error.message : String(error);
14187
14268
  }
14188
- async function get(input, url, headers, readBody = true) {
14269
+ async function get$1(input, url, headers, readBody = true) {
14189
14270
  const controller = abortController({
14190
14271
  timeoutMs: input.timeoutMs,
14191
14272
  signal: input.signal
@@ -14242,7 +14323,7 @@ function llmsLinks(text, base) {
14242
14323
  return links;
14243
14324
  }
14244
14325
  async function checkLlmsTxt(input) {
14245
- 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());
14246
14327
  const availability = result.availability === "available" && isHtml(result.contentType) ? "absent" : result.availability;
14247
14328
  const exists = availability === "available";
14248
14329
  const base = {
@@ -14259,7 +14340,7 @@ async function checkLlmsTxt(input) {
14259
14340
  for (const url of found.slice(0, LLMS_LINK_LIMIT)) {
14260
14341
  if (input.signal?.aborted) break;
14261
14342
  checked += 1;
14262
- const link = brokenLink(await get(input, url, void 0, false), false);
14343
+ const link = brokenLink(await get$1(input, url, void 0, false), false);
14263
14344
  if (link) broken.push(link);
14264
14345
  }
14265
14346
  return {
@@ -14303,7 +14384,7 @@ function sitemapUrlsFromRobots(text) {
14303
14384
  return [...urls];
14304
14385
  }
14305
14386
  async function checkRobotsAiAccess(input) {
14306
- 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());
14307
14388
  const exists = result.availability === "available";
14308
14389
  const unknown = result.availability === "rate-limited" || result.availability === "unreachable";
14309
14390
  const declared = exists ? declaredUserAgents(result.text) : /* @__PURE__ */ new Set();
@@ -14332,7 +14413,7 @@ async function checkAgentResources(input) {
14332
14413
  const resources = [];
14333
14414
  for (const path of AGENT_RESOURCE_PATHS) {
14334
14415
  if (input.signal?.aborted) break;
14335
- const result = await get(input, new URL(path, input.url).toString());
14416
+ const result = await get$1(input, new URL(path, input.url).toString());
14336
14417
  const exists = result.availability === "available" && !isHtml(result.contentType);
14337
14418
  let validJson;
14338
14419
  if (exists && /\bjson\b/i.test(result.contentType)) try {
@@ -14352,7 +14433,7 @@ async function checkAgentResources(input) {
14352
14433
  return resources;
14353
14434
  }
14354
14435
  async function checkMarkdownNegotiation(input) {
14355
- const result = await get(input, input.url, { accept: "text/markdown" }, false);
14436
+ const result = await get$1(input, input.url, { accept: "text/markdown" }, false);
14356
14437
  const vary = result.headers?.get("vary") ?? "";
14357
14438
  return {
14358
14439
  url: result.url,
@@ -14399,7 +14480,7 @@ async function checkMarkdownCopies(input) {
14399
14480
  const url = page.markdownAlternates?.[0];
14400
14481
  if (!url || !input.allows(url)) continue;
14401
14482
  checked += 1;
14402
- const result = await get({
14483
+ const result = await get$1({
14403
14484
  ...input,
14404
14485
  url
14405
14486
  }, url);
@@ -15565,7 +15646,7 @@ function taskIds(value) {
15565
15646
  }
15566
15647
  }
15567
15648
  function readProviderCache(key, schema, options = {}) {
15568
- const database = options.database ?? getDb();
15649
+ const database = options.database ?? cacheDb();
15569
15650
  const now = options.now ?? Date.now();
15570
15651
  const hash = requestHash(key, stableProviderRequestJson(key.request));
15571
15652
  const row = database.prepare(`SELECT response_json, row_count, source_cost_micros, task_ids_json,
@@ -15586,9 +15667,11 @@ function readProviderCache(key, schema, options = {}) {
15586
15667
  taskIds: taskIds(row.task_ids_json)
15587
15668
  };
15588
15669
  } catch {
15589
- database.prepare(`DELETE FROM provider_cache
15590
- WHERE provider = ? AND credential_scope = ? AND operation = ?
15591
- AND request_hash = ?`).run(key.provider, key.credentialScope, key.operation, hash);
15670
+ try {
15671
+ database.prepare(`DELETE FROM provider_cache
15672
+ WHERE provider = ? AND credential_scope = ? AND operation = ?
15673
+ AND request_hash = ?`).run(key.provider, key.credentialScope, key.operation, hash);
15674
+ } catch {}
15592
15675
  return null;
15593
15676
  }
15594
15677
  }
@@ -15599,19 +15682,22 @@ function writeProviderCache(key, input, options = {}) {
15599
15682
  code: "configuration",
15600
15683
  message: `Provider cache TTL must be between 1 and ${CACHE_MAX_AGE_MS} milliseconds.`
15601
15684
  });
15602
- const database = options.database ?? getDb();
15603
15685
  const now = options.now ?? Date.now();
15604
15686
  const expiresAt = now + input.ttlMs;
15605
15687
  const requestJson = stableProviderRequestJson(key.request);
15606
15688
  const responseJson = JSON.stringify(input.data);
15607
15689
  const normalizedTaskIds = [...new Set(input.taskIds)].filter((id) => id.length > 0 && id.length <= 100).sort((left, right) => left < right ? -1 : left > right ? 1 : 0).slice(0, 20);
15608
15690
  const taskIdsJson = JSON.stringify(normalizedTaskIds);
15609
- database.prepare(`INSERT OR REPLACE INTO provider_cache (
15610
- provider, credential_scope, operation, request_hash, request_json,
15611
- response_json, row_count, source_cost_micros, task_ids_json,
15612
- fetched_at, expires_at
15613
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(key.provider, key.credentialScope, key.operation, requestHash(key, requestJson), requestJson, responseJson, input.rowCount, input.sourceCostMicros, taskIdsJson, now, expiresAt);
15614
- noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson) + Buffer.byteLength(taskIdsJson), database);
15691
+ const save = (database) => {
15692
+ database.prepare(`INSERT OR REPLACE INTO provider_cache (
15693
+ provider, credential_scope, operation, request_hash, request_json,
15694
+ response_json, row_count, source_cost_micros, task_ids_json,
15695
+ fetched_at, expires_at
15696
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(key.provider, key.credentialScope, key.operation, requestHash(key, requestJson), requestJson, responseJson, input.rowCount, input.sourceCostMicros, taskIdsJson, now, expiresAt);
15697
+ noteCacheWrite(Buffer.byteLength(requestJson) + Buffer.byteLength(responseJson) + Buffer.byteLength(taskIdsJson), database);
15698
+ };
15699
+ if (options.database) save(options.database);
15700
+ else writeToCache(save);
15615
15701
  return {
15616
15702
  data: input.data,
15617
15703
  storedAt: new Date(now).toISOString(),
@@ -16251,7 +16337,7 @@ const installedPackageJsonSchema = z.object({
16251
16337
  function packageDirectory(packagesDir, packageName) {
16252
16338
  return join(packagesDir, "node_modules", ...packageName.split("/"));
16253
16339
  }
16254
- function inside(parent, child) {
16340
+ function inside$1(parent, child) {
16255
16341
  const path = relative(parent, child);
16256
16342
  return path === "" || !path.startsWith("..") && !isAbsolute(path);
16257
16343
  }
@@ -16283,14 +16369,14 @@ async function loadOne(installed, registry, options) {
16283
16369
  async function loadProviderPackage(input, options) {
16284
16370
  const importModule = options.importModule ?? ((url) => import(url));
16285
16371
  const packageDir = realpathSync(packageDirectory(options.packagesDir, input.package));
16286
- 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.");
16287
16373
  const packageJsonPath = join(packageDir, "package.json");
16288
16374
  const packageJson = installedPackageJsonSchema.parse(JSON.parse(readFileSync(packageJsonPath, "utf8")));
16289
16375
  if (packageJson.name !== input.package || packageJson.version !== input.version) throw new Error("Installed provider package does not match its saved record.");
16290
16376
  const packageRegistry = new ProviderExtensionRegistry();
16291
16377
  for (const entry of packageJson.seo.providers) {
16292
16378
  const entryPath = realpathSync(resolve(dirname(packageJsonPath), entry));
16293
- 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.");
16294
16380
  await providerActivate((await importModule(`${pathToFileURL(entryPath).href}?seo-provider=${encodeURIComponent(input.version)}`)).default)(packageRegistry.hostFor({
16295
16381
  package: input.package,
16296
16382
  version: input.version
@@ -16326,7 +16412,7 @@ async function loadInstalledProviderExtensions(options = {}) {
16326
16412
  const DEFAULT_MAX_REQUESTS = 8;
16327
16413
  const MAX_RESPONSE_BYTES$1 = 10 * 1024 * 1024;
16328
16414
  const MAX_REQUEST_BODY_BYTES = 64 * 1024;
16329
- const TIMEOUT_MS$1 = 15e3;
16415
+ const TIMEOUT_MS$3 = 15e3;
16330
16416
  function normalizeProviderExtensionAccount(provider, account) {
16331
16417
  const fields = provider.connection.fields.filter((field) => field.kind === "account");
16332
16418
  const allowed = new Set(fields.map((field) => field.id));
@@ -16373,7 +16459,7 @@ function runtimeForProvider(provider, options = {}) {
16373
16459
  },
16374
16460
  fetch: providerFetch,
16375
16461
  maxResponseBytes: MAX_RESPONSE_BYTES$1,
16376
- timeoutMs: TIMEOUT_MS$1,
16462
+ timeoutMs: TIMEOUT_MS$3,
16377
16463
  retry: (input.method ?? "GET") === "GET" ? "safe" : "never",
16378
16464
  schema: z.unknown()
16379
16465
  });
@@ -17954,7 +18040,7 @@ function analyzeCtrUnderperformersFromRows(input) {
17954
18040
  const MAX_GSC_ROWS$3 = 1e5;
17955
18041
  const DEFAULT_DAYS$3 = 28;
17956
18042
  const MAX_DAYS$2 = 548;
17957
- function plural$5(count, singular, pluralLabel = `${singular}s`) {
18043
+ function plural$6(count, singular, pluralLabel = `${singular}s`) {
17958
18044
  return count === 1 ? singular : pluralLabel;
17959
18045
  }
17960
18046
  async function ctrUnderperformersReportWith(input, dependencies) {
@@ -18057,11 +18143,11 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18057
18143
  minImpressions,
18058
18144
  limit,
18059
18145
  brandFiltering: input.includeBrand ? "included" : "excluded",
18060
- 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.`
18061
18147
  },
18062
18148
  items,
18063
18149
  caveats: [
18064
- `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.`,
18065
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"}).`,
18066
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.`,
18067
18153
  minImpressionsCaveat({
@@ -18069,13 +18155,13 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18069
18155
  belowMinimumRows: selection.belowMinimumRows
18070
18156
  }),
18071
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.",
18072
- ...selection.fragmentRows ? [`${selection.fragmentRows} source ${plural$5(selection.fragmentRows, "row")} with a #fragment URL ${selection.fragmentRows === 1 ? "was" : "were"} merged into the base URL.`] : [],
18073
- ...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.`] : [],
18074
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.",
18075
- ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$5(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
18076
- ...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.`] : [],
18077
18163
  ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$3} was reached, so lower-ranked source rows may be absent.`] : [],
18078
- ...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.`] : []
18079
18165
  ],
18080
18166
  recommendations: top ? [
18081
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.`,
@@ -21910,7 +21996,7 @@ function anomalyEvidence(anomaly) {
21910
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}`}).`;
21911
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}`}).`;
21912
21998
  }
21913
- function plural$4(count, singular, pluralLabel = `${singular}s`) {
21999
+ function plural$5(count, singular, pluralLabel = `${singular}s`) {
21914
22000
  return count === 1 ? singular : pluralLabel;
21915
22001
  }
21916
22002
  function strongestSignal(anomalies) {
@@ -21966,13 +22052,13 @@ function interpretUpdateCorrelation(input) {
21966
22052
  summary,
21967
22053
  evidence: [
21968
22054
  ...input.anomalies.map(anomalyEvidence),
21969
- 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.",
21970
22056
  largeMovement ? "Both clicks and impressions moved sharply in the same direction." : "Clicks and impressions did not both meet the sharp-movement evidence threshold.",
21971
22057
  hasConfounders ? `Known overlapping site changes: ${confounders.map((confounder) => confounder.title).join("; ")}.` : "No overlapping site changes were supplied."
21972
22058
  ],
21973
22059
  caveats: [
21974
22060
  `GSC window: ${input.days} days; recent comparison window: ${input.recentDays} days.`,
21975
- `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.`,
21976
22062
  input.refresh ? "Data freshness: cache bypassed for GSC data." : "Data freshness: local GSC cache may have been used.",
21977
22063
  "Source: official Google Search Status Dashboard Ranking incidents only; third-party volatility and unconfirmed chatter are not included.",
21978
22064
  "An update-window overlap cannot establish causation, even when movement is large and no known site change was recorded.",
@@ -22140,7 +22226,7 @@ function analyzeTrafficAnomalyRows(input) {
22140
22226
  if (step && changeDate) {
22141
22227
  const previousDate = aggregated.rows[step.index - 1]?.keys[0];
22142
22228
  const gap = previousDate ? calendarDays(previousDate, changeDate) - 2 : 0;
22143
- 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.`] : []);
22144
22230
  return compareSegments({
22145
22231
  site: input.site,
22146
22232
  metric,
@@ -22171,9 +22257,9 @@ function analyzeTrafficAnomalyRows(input) {
22171
22257
  const comparisonMissingDays = input.recentDays - comparisonRows.length;
22172
22258
  const missingDays = expectedDays - aggregated.rows.length;
22173
22259
  const caveats = [
22174
- ...missingDays > 0 ? [`${missingDays} requested calendar ${plural$4(missingDays, "day")} had no returned date aggregate and ${missingDays === 1 ? "was" : "were"} not filled with zeros.`] : [],
22175
- ...aggregated.invalidRows > 0 ? [`${aggregated.invalidRows} invalid provider ${plural$4(aggregated.invalidRows, "row")} ${aggregated.invalidRows === 1 ? "was" : "were"} excluded.`] : [],
22176
- ...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.`] : [],
22177
22263
  ...stepCaveats
22178
22264
  ];
22179
22265
  return {
@@ -23254,7 +23340,7 @@ const VOID = /* @__PURE__ */ new Set([
23254
23340
  "col",
23255
23341
  "embed"
23256
23342
  ]);
23257
- const ENTITIES = {
23343
+ const ENTITIES$1 = {
23258
23344
  amp: "&",
23259
23345
  lt: "<",
23260
23346
  gt: ">",
@@ -23279,7 +23365,7 @@ function decode(text) {
23279
23365
  const point = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
23280
23366
  return point > 0 && point <= 1114111 ? String.fromCodePoint(point) : "";
23281
23367
  }
23282
- return ENTITIES[code.toLowerCase()] ?? match;
23368
+ return ENTITIES$1[code.toLowerCase()] ?? match;
23283
23369
  });
23284
23370
  }
23285
23371
  const clean$1 = (value) => value.split(A_OPEN).join("").replace(/\s+/g, " ").trim();
@@ -23520,7 +23606,7 @@ var SentenceStore = class SentenceStore {
23520
23606
  });
23521
23607
  }
23522
23608
  };
23523
- const bareHost = (host) => host.toLowerCase().replace(/^www\./, "");
23609
+ const bareHost$1 = (host) => host.toLowerCase().replace(/^www\./, "");
23524
23610
  function crawlUrl(value, base) {
23525
23611
  try {
23526
23612
  const url = base ? new URL(value, base) : new URL(value);
@@ -23586,10 +23672,10 @@ async function crawlForLinks(input) {
23586
23672
  async function crawl(input, robots, sentences, renderer, warnings) {
23587
23673
  const start = crawlUrl(input.startUrl) ?? input.startUrl;
23588
23674
  let origin = new URL(start).origin;
23589
- const site = bareHost(new URL(start).host);
23675
+ const site = bareHost$1(new URL(start).host);
23590
23676
  const sameSite = (url) => {
23591
23677
  try {
23592
- return bareHost(new URL(url).host) === site;
23678
+ return bareHost$1(new URL(url).host) === site;
23593
23679
  } catch {
23594
23680
  return false;
23595
23681
  }
@@ -24083,15 +24169,15 @@ function internalLinksReportRange(input, fallbackDays, now) {
24083
24169
  days: Math.round((Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / DAY$1) + 1
24084
24170
  };
24085
24171
  }
24086
- const plural$3 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
24172
+ const plural$4 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
24087
24173
  function internalLinksVerdict(report) {
24088
24174
  const { counts } = report;
24089
24175
  if (report.dataStatus === "blocked") return "robots.txt does not allow this crawler to read the site, so nothing was checked.";
24090
24176
  if (report.dataStatus === "empty") return "No HTML page could be crawled, so there is nothing to link.";
24091
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.";
24092
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.`;
24093
- 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)`;
24094
- 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")}`;
24095
24181
  const capped = report.crawl.capReached ? ` The crawl stopped at its limit of ${report.crawl.pageLimit} pages, so pages beyond it were not checked.` : "";
24096
24182
  return `${judged} ${scope}${counts.heldBack ? `; ${counts.heldBack} held back by the evidence floors` : ""}.${capped}`;
24097
24183
  }
@@ -25992,7 +26078,7 @@ function reportId(input) {
25992
26078
  })).digest("hex").slice(0, 20)}`;
25993
26079
  }
25994
26080
  function cacheGet(id, now) {
25995
- const row = getDb().prepare("SELECT report_json FROM performance_reports WHERE id = ? AND expires_at > ?").get(id, now);
26081
+ const row = cacheDb().prepare("SELECT report_json FROM performance_reports WHERE id = ? AND expires_at > ?").get(id, now);
25996
26082
  if (!row?.report_json) return void 0;
25997
26083
  try {
25998
26084
  const report = JSON.parse(row.report_json);
@@ -26007,10 +26093,12 @@ function cacheSet(report, now) {
26007
26093
  raw: void 0
26008
26094
  };
26009
26095
  const reportJson = JSON.stringify(stored);
26010
- getDb().prepare(`INSERT OR REPLACE INTO performance_reports
26011
- (id, url, strategy, report_json, created_at, expires_at)
26012
- VALUES (?, ?, ?, ?, ?, ?)`).run(report.id, report.url, report.strategy, reportJson, Date.parse(report.generatedAt), now + CACHE_TTL_MS);
26013
- noteCacheWrite(Buffer.byteLength(reportJson));
26096
+ writeToCache((database) => {
26097
+ database.prepare(`INSERT OR REPLACE INTO performance_reports
26098
+ (id, url, strategy, report_json, created_at, expires_at)
26099
+ VALUES (?, ?, ?, ?, ?, ?)`).run(report.id, report.url, report.strategy, reportJson, Date.parse(report.generatedAt), now + CACHE_TTL_MS);
26100
+ noteCacheWrite(Buffer.byteLength(reportJson), database);
26101
+ });
26014
26102
  }
26015
26103
  async function fallbackReport(input) {
26016
26104
  const fetched = await fetchPage(input.url, {
@@ -26530,14 +26618,14 @@ function* retainedQueryClusterRows(input) {
26530
26618
  };
26531
26619
  }
26532
26620
  }
26533
- function plural$2(count, singular, pluralLabel = `${singular}s`) {
26621
+ function plural$3(count, singular, pluralLabel = `${singular}s`) {
26534
26622
  return count === 1 ? singular : pluralLabel;
26535
26623
  }
26536
26624
  function reportVerdict(input) {
26537
26625
  if (!input.clusters.length) return "No query clusters were generated for this scope.";
26538
26626
  if (input.highOpportunityClusters > 0) {
26539
26627
  const verb = input.highOpportunityClusters === 1 ? "has" : "have";
26540
- 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.`;
26541
26629
  }
26542
26630
  return "No obvious high-opportunity clusters were found. Use this report for content structure and intent mapping rather than urgent fixes.";
26543
26631
  }
@@ -26678,7 +26766,7 @@ function movementLine(report) {
26678
26766
  }
26679
26767
  function countPhrase(count, singular, pluralLabel = `${singular}s`) {
26680
26768
  if (count === 0) return `no ${pluralLabel}`;
26681
- return `${count} ${plural$1(count, singular, pluralLabel)}`;
26769
+ return `${count} ${plural$2(count, singular, pluralLabel)}`;
26682
26770
  }
26683
26771
  function headlineLine(report) {
26684
26772
  const parts = [
@@ -26733,7 +26821,7 @@ function decayDiagnosisLabel(diagnosis) {
26733
26821
  if (diagnosis === "lost_clicks") return "clicks dropped without one dominant signal";
26734
26822
  return diagnosis.replaceAll("_", " ");
26735
26823
  }
26736
- function plural$1(count, singular, pluralLabel = `${singular}s`) {
26824
+ function plural$2(count, singular, pluralLabel = `${singular}s`) {
26737
26825
  return count === 1 ? singular : pluralLabel;
26738
26826
  }
26739
26827
  function decayClusterLine(report) {
@@ -26741,7 +26829,7 @@ function decayClusterLine(report) {
26741
26829
  if (!group) return "No material decay cluster stood out.";
26742
26830
  const diagnosis = decayDiagnosisLabel(group.diagnosis);
26743
26831
  const samples = group.sampleQueries.slice(0, 2).join("; ");
26744
- 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}.` : ""}`;
26745
26833
  }
26746
26834
  function cannibalSuppressionLine(report) {
26747
26835
  const suppressed = report.cannibalization.selection.suppressedQueries;
@@ -26759,21 +26847,21 @@ function contentOpportunityBullets(report) {
26759
26847
  if (unavailableSections.length) bullets.push(`Could not assess ${unavailableSections.join(", ")} opportunities in this run; see report caveats.`);
26760
26848
  if (report.summary.decayItems) {
26761
26849
  const top = report.decay.items[0];
26762
- 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.`);
26763
26851
  bullets.push(decayClusterLine(report));
26764
26852
  }
26765
26853
  if (report.summary.cannibalItems) {
26766
26854
  const top = report.cannibalization.items[0];
26767
- 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.`);
26768
26856
  }
26769
26857
  if (report.cannibalization.selection.suppressedQueries) bullets.push(cannibalSuppressionLine(report));
26770
26858
  if (report.summary.strikingDistanceItems) {
26771
26859
  const top = report.strikingDistance.items[0];
26772
- 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.`);
26773
26861
  }
26774
26862
  if (!skippedReason(report, "quick-win opportunities") && report.summary.quickWinItems) {
26775
26863
  const top = report.quickWins.items[0];
26776
- 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.`);
26777
26865
  }
26778
26866
  const templateLine = templateOpportunityLine(report);
26779
26867
  if (!templateLine.startsWith("No dominant")) bullets.push(templateLine);
@@ -28495,11 +28583,12 @@ function ga4MatchReason(match, site) {
28495
28583
  //#endregion
28496
28584
  //#region packages/core/src/commands/clicky.ts
28497
28585
  const CLICKY_KEY_PREFIX = "x-clicky-";
28586
+ const MAX_INSPECT_LIMIT = 2e3;
28498
28587
  //#endregion
28499
28588
  //#region packages/core/src/commands/page-facts.ts
28500
28589
  const MAX_PAGES = 20;
28501
- const TIMEOUT_MS = 6e3;
28502
- const MAX_BYTES = 1e6;
28590
+ const TIMEOUT_MS$2 = 6e3;
28591
+ const MAX_BYTES$2 = 1e6;
28503
28592
  const clean = (value) => value?.trim().replace(/\s+/g, " ") || null;
28504
28593
  async function boundedText(response) {
28505
28594
  if (!response.body) return "";
@@ -28507,7 +28596,7 @@ async function boundedText(response) {
28507
28596
  const decoder = new TextDecoder();
28508
28597
  let text = "";
28509
28598
  let total = 0;
28510
- while (total < MAX_BYTES) {
28599
+ while (total < MAX_BYTES$2) {
28511
28600
  const { done, value } = await reader.read();
28512
28601
  if (done) return text + decoder.decode();
28513
28602
  total += value.byteLength;
@@ -28587,12 +28676,14 @@ async function facts(url) {
28587
28676
  "user-agent": SEO_CRAWLER_USER_AGENT,
28588
28677
  accept: "text/html"
28589
28678
  },
28590
- signal: AbortSignal.timeout(TIMEOUT_MS)
28679
+ signal: AbortSignal.timeout(TIMEOUT_MS$2)
28591
28680
  });
28592
28681
  out.status = response.status;
28593
28682
  const location = response.headers.get("location");
28594
28683
  if (location) out.redirects_to = new URL(location, url).toString();
28595
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);
28596
28687
  if (response.status !== 200 || !response.headers.get("content-type")?.includes("html")) {
28597
28688
  out.robots = headerRobots;
28598
28689
  await response.body?.cancel();
@@ -28609,6 +28700,37 @@ async function facts(url) {
28609
28700
  function checkPages(urls) {
28610
28701
  return Promise.all(urls.slice(0, MAX_PAGES).map(facts));
28611
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
+ }
28612
28734
  //#endregion
28613
28735
  //#region packages/core/src/commands/report.ts
28614
28736
  const SECTIONS = {
@@ -28641,16 +28763,23 @@ const RESEARCH_HELP = `keyword_metrics (keywords: volume, difficulty, CPC), rank
28641
28763
  function indexStatusFrom(result) {
28642
28764
  const status = result?.inspectionResult?.indexStatusResult ?? result?.indexStatusResult ?? {};
28643
28765
  const text = (value) => typeof value === "string" ? value : null;
28766
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
28644
28767
  return {
28645
28768
  verdict: text(status.verdict),
28646
28769
  coverage: text(status.coverageState),
28647
28770
  indexing: text(status.indexingState),
28648
28771
  last_crawl: text(status.lastCrawlTime),
28649
28772
  google_canonical: text(status.googleCanonical),
28650
- 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)
28651
28779
  };
28652
28780
  }
28653
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.`;
28654
28783
  //#endregion
28655
28784
  //#region packages/core/src/commands/tools.ts
28656
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.");
@@ -28685,9 +28814,15 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
28685
28814
  site,
28686
28815
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD: the first day after the change."),
28687
28816
  pages: z.string().min(1).optional().describe("Optional URL text, such as /blog/.")
28688
- }), `${LOST_PAGES_RULE}`, z.object({
28817
+ }), `${INDEXING_RULE}${LOST_PAGES_RULE}`, z.object({
28689
28818
  site,
28690
- ...dates
28819
+ ...dates,
28820
+ ...detail
28821
+ }), z.object({
28822
+ site,
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
28691
28826
  }), `${RESEARCH_HELP}`, z.object({
28692
28827
  provider: z.enum(RESEARCH_PROVIDERS),
28693
28828
  action: z.enum(RESEARCH_ACTIONS),
@@ -28705,6 +28840,36 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
28705
28840
  ...detail
28706
28841
  });
28707
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
28708
28873
  //#region packages/core/src/providers/api-key-credentials.ts
28709
28874
  function createApiKeyCredentialStore(input) {
28710
28875
  const normalize = (value) => {
@@ -28900,16 +29065,19 @@ function nodeCommandDeps(options) {
28900
29065
  accountEmail: options.account
28901
29066
  }),
28902
29067
  checkPages,
28903
- inspect: async (site, url) => {
28904
- try {
28905
- return indexStatusFrom(await inspectUrl({
28906
- siteUrl: site,
28907
- inspectionUrl: url,
28908
- accountEmail: options.account
28909
- }));
28910
- } catch {
28911
- return null;
28912
- }
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
28913
29081
  },
28914
29082
  pageMarkdown: async (url) => extractMainMarkdown(await fetchPage(url, { refresh: options.refresh })),
28915
29083
  call: options.call,
@@ -30564,6 +30732,94 @@ async function resetSeoData() {
30564
30732
  });
30565
30733
  }
30566
30734
  //#endregion
30735
+ //#region packages/core/src/update-notice.ts
30736
+ const REGISTRY_URL = "https://registry.npmjs.org/seo/latest";
30737
+ const CHECK_EVERY_MS = 864e5;
30738
+ const TIMEOUT_MS = 2e3;
30739
+ const MAX_BYTES = 65536;
30740
+ let checking = false;
30741
+ function runByNpx(env, cliPath) {
30742
+ if (env.npm_command === "exec" || /npx/i.test(env.npm_execpath ?? "")) return true;
30743
+ if (!cliPath) return false;
30744
+ let path = cliPath;
30745
+ try {
30746
+ path = realpathSync(cliPath);
30747
+ } catch {}
30748
+ return /[\\/](?:_npx|dlx)[\\/]/.test(path);
30749
+ }
30750
+ function isNewerVersion(current, latest) {
30751
+ const parse = (value) => value.replace(/^v/, "").split("-")[0]?.split(".").map((part) => Number.parseInt(part, 10)) ?? [];
30752
+ const currentParts = parse(current);
30753
+ const latestParts = parse(latest);
30754
+ for (let index = 0; index < 3; index += 1) {
30755
+ const currentPart = currentParts[index] ?? 0;
30756
+ const latestPart = latestParts[index] ?? 0;
30757
+ if (!Number.isFinite(currentPart) || !Number.isFinite(latestPart)) return false;
30758
+ if (latestPart > currentPart) return true;
30759
+ if (latestPart < currentPart) return false;
30760
+ }
30761
+ return false;
30762
+ }
30763
+ function isLatestCache(value) {
30764
+ if (!value || typeof value !== "object") return false;
30765
+ const cache = value;
30766
+ return typeof cache.checkedAt === "number" && typeof cache.latest === "string" && /^\d+\.\d+\.\d+$/.test(cache.latest);
30767
+ }
30768
+ function checkRegistry(cacheFile, now) {
30769
+ if (checking) return;
30770
+ checking = true;
30771
+ const done = () => {
30772
+ checking = false;
30773
+ };
30774
+ const req = request(REGISTRY_URL, { headers: { accept: "application/json" } });
30775
+ const timer = setTimeout(() => req.destroy(), TIMEOUT_MS);
30776
+ timer.unref();
30777
+ req.on("socket", (socket) => socket.unref());
30778
+ req.on("error", () => void 0);
30779
+ req.on("close", () => {
30780
+ clearTimeout(timer);
30781
+ done();
30782
+ });
30783
+ req.on("response", (response) => {
30784
+ const chunks = [];
30785
+ let size = 0;
30786
+ response.on("error", () => void 0);
30787
+ response.on("data", (chunk) => {
30788
+ size += chunk.length;
30789
+ if (size > MAX_BYTES) req.destroy();
30790
+ else chunks.push(chunk);
30791
+ });
30792
+ response.on("end", () => {
30793
+ if (response.statusCode !== 200 || size > MAX_BYTES) return;
30794
+ readJson(new Response(Buffer.concat(chunks)), "The npm registry").then((body) => {
30795
+ const cache = {
30796
+ checkedAt: now,
30797
+ latest: body.version
30798
+ };
30799
+ if (isLatestCache(cache)) writeJsonAtomic(cacheFile, cache, 384);
30800
+ }).catch(() => void 0);
30801
+ });
30802
+ });
30803
+ req.end();
30804
+ }
30805
+ function updateNotice(options = {}) {
30806
+ try {
30807
+ const env = options.env ?? process.env;
30808
+ const argv = options.argv ?? process.argv;
30809
+ if (env.NO_UPDATE_NOTIFIER || argv.includes("--no-update-notifier") || isTelemetryCi(env)) return;
30810
+ const cacheFile = options.cacheFile ?? join(getSeoCliPaths().cacheDir, "latest-version.json");
30811
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
30812
+ const stored = readJsonFile(cacheFile);
30813
+ const cache = isLatestCache(stored) ? stored : void 0;
30814
+ if (!cache || now - cache.checkedAt >= CHECK_EVERY_MS) checkRegistry(cacheFile, now);
30815
+ const current = options.version ?? SEO_VERSION;
30816
+ if (!cache || !isNewerVersion(current, cache.latest)) return void 0;
30817
+ return runByNpx(env, options.cliPath ?? argv[1]) ? `seo ${cache.latest} is out (you have ${current}). Run npx -y seo@latest; in an MCP config use seo@latest, then restart the client.` : `seo ${cache.latest} is out (you have ${current}). Update with: npm i -g seo`;
30818
+ } catch {
30819
+ return;
30820
+ }
30821
+ }
30822
+ //#endregion
30567
30823
  //#region packages/core/src/utils/time.ts
30568
30824
  function parseOlderThan(input) {
30569
30825
  if (!input) return;
@@ -30598,6 +30854,6 @@ function isoDate(daysAgo = 0) {
30598
30854
  return date.toISOString().slice(0, 10);
30599
30855
  }
30600
30856
  //#endregion
30601
- 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, 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, 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, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, 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 };
30602
30858
 
30603
30859
  //# sourceMappingURL=index.js.map