seo 0.2.15 → 0.2.16

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 load, t as version } from "./package-Cuxp-LBy.js";
2
+ import { n as load, t as version } from "./package-D_ix15KL.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -1102,6 +1102,52 @@ async function exchangeCode(input) {
1102
1102
  return tokenResponse.json();
1103
1103
  }
1104
1104
  //#endregion
1105
+ //#region packages/core/src/keyword-sets/schema.ts
1106
+ const KEYWORD_SET_SCHEMA_SQL = `
1107
+ CREATE TABLE IF NOT EXISTS keyword_sets (
1108
+ id TEXT PRIMARY KEY,
1109
+ project_id TEXT NOT NULL,
1110
+ name TEXT NOT NULL,
1111
+ market_json TEXT NOT NULL,
1112
+ provider TEXT CHECK(provider IS NULL OR provider IN ('dataforseo', 'semrush', 'ahrefs')),
1113
+ source_report TEXT,
1114
+ created_at INTEGER NOT NULL,
1115
+ updated_at INTEGER NOT NULL,
1116
+ last_refreshed_at INTEGER,
1117
+ UNIQUE(project_id, name COLLATE NOCASE)
1118
+ );
1119
+ CREATE INDEX IF NOT EXISTS idx_keyword_sets_project
1120
+ ON keyword_sets(project_id, updated_at DESC, id);
1121
+
1122
+ CREATE TABLE IF NOT EXISTS keyword_set_items (
1123
+ set_id TEXT NOT NULL,
1124
+ normalized_keyword TEXT NOT NULL,
1125
+ display_keyword TEXT NOT NULL,
1126
+ metric_json TEXT,
1127
+ metric_provider TEXT CHECK(metric_provider IS NULL OR metric_provider IN ('dataforseo', 'semrush', 'ahrefs')),
1128
+ metric_observed_at TEXT,
1129
+ page_kind TEXT CHECK(page_kind IS NULL OR page_kind IN ('target', 'proposed')),
1130
+ page_url TEXT,
1131
+ created_at INTEGER NOT NULL,
1132
+ updated_at INTEGER NOT NULL,
1133
+ PRIMARY KEY(set_id, normalized_keyword),
1134
+ FOREIGN KEY(set_id) REFERENCES keyword_sets(id) ON DELETE CASCADE
1135
+ ) WITHOUT ROWID;
1136
+ CREATE INDEX IF NOT EXISTS idx_keyword_set_items_set
1137
+ ON keyword_set_items(set_id, normalized_keyword);
1138
+
1139
+ CREATE TABLE IF NOT EXISTS keyword_set_tags (
1140
+ set_id TEXT NOT NULL,
1141
+ normalized_keyword TEXT NOT NULL,
1142
+ tag TEXT NOT NULL,
1143
+ PRIMARY KEY(set_id, normalized_keyword, tag),
1144
+ FOREIGN KEY(set_id, normalized_keyword)
1145
+ REFERENCES keyword_set_items(set_id, normalized_keyword) ON DELETE CASCADE
1146
+ ) WITHOUT ROWID;
1147
+ CREATE INDEX IF NOT EXISTS idx_keyword_set_tags_lookup
1148
+ ON keyword_set_tags(set_id, tag, normalized_keyword);
1149
+ `;
1150
+ //#endregion
1105
1151
  //#region packages/core/src/storage/cache-maintenance.ts
1106
1152
  const MEBIBYTE$2 = 1024 * 1024;
1107
1153
  const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
@@ -1657,6 +1703,7 @@ function initDb(database, isNewDatabase) {
1657
1703
  if (isNewDatabase) database.pragma("auto_vacuum = INCREMENTAL");
1658
1704
  database.transaction(() => {
1659
1705
  database.exec(CREATE_SQL);
1706
+ database.exec(KEYWORD_SET_SCHEMA_SQL);
1660
1707
  database.exec(PROVIDER_SPEND_SCHEMA_SQL);
1661
1708
  database.exec(`DELETE FROM semrush_cache WHERE request_json LIKE '%"key"%'`);
1662
1709
  ensureColumn(database, "crawl_pages", "snapshot_json", "TEXT");
@@ -4890,7 +4937,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
4890
4937
  }
4891
4938
  //#endregion
4892
4939
  //#region packages/core/src/gsc/property-url.ts
4893
- function invalid(message) {
4940
+ function invalid$1(message) {
4894
4941
  throw new SeoError("INVALID_INPUT", message);
4895
4942
  }
4896
4943
  function normalizeHttpUrl(value) {
@@ -4898,10 +4945,10 @@ function normalizeHttpUrl(value) {
4898
4945
  try {
4899
4946
  url = new URL(value.trim());
4900
4947
  } catch {
4901
- return invalid(`Invalid URL: ${value}`);
4948
+ return invalid$1(`Invalid URL: ${value}`);
4902
4949
  }
4903
- if (url.protocol !== "http:" && url.protocol !== "https:") return invalid("The report URL must use http:// or https://.");
4904
- if (url.username || url.password) return invalid("The report URL must not contain embedded credentials.");
4950
+ if (url.protocol !== "http:" && url.protocol !== "https:") return invalid$1("The report URL must use http:// or https://.");
4951
+ if (url.username || url.password) return invalid$1("The report URL must not contain embedded credentials.");
4905
4952
  url.hash = "";
4906
4953
  return url.toString();
4907
4954
  }
@@ -4910,13 +4957,13 @@ function assertUrlMatchesGscProperty(property, value) {
4910
4957
  const parsed = new URL(url);
4911
4958
  if (property.startsWith("sc-domain:")) {
4912
4959
  const domain = property.slice(10).trim().toLowerCase();
4913
- if (!domain || /[/:]/.test(domain)) return invalid(`Invalid Search Console domain property: ${property}`);
4960
+ if (!domain || /[/:]/.test(domain)) return invalid$1(`Invalid Search Console domain property: ${property}`);
4914
4961
  const hostname = parsed.hostname.toLowerCase();
4915
- if (hostname !== domain && !hostname.endsWith(`.${domain}`)) return invalid(`${url} is outside the Search Console property ${property}.`);
4962
+ if (hostname !== domain && !hostname.endsWith(`.${domain}`)) return invalid$1(`${url} is outside the Search Console property ${property}.`);
4916
4963
  return url;
4917
4964
  }
4918
4965
  const prefix = normalizeHttpUrl(property);
4919
- if (!url.startsWith(prefix)) return invalid(`${url} is outside the Search Console property ${property}.`);
4966
+ if (!url.startsWith(prefix)) return invalid$1(`${url} is outside the Search Console property ${property}.`);
4920
4967
  return url;
4921
4968
  }
4922
4969
  //#endregion
@@ -23807,7 +23854,7 @@ const MAX_ACTIVE_RESERVATIONS = 100;
23807
23854
  const PROVIDER_SPEND_RETENTION_DAYS = RETENTION_MS / DAY_MS$1;
23808
23855
  const PROVIDER_SPEND_MAX_ROWS = MAX_LEDGER_ROWS;
23809
23856
  const PROVIDER_SPEND_MAX_BYTES = MAX_LEDGER_BYTES;
23810
- function boundedText(provider, value, label, max) {
23857
+ function boundedText$1(provider, value, label, max) {
23811
23858
  const normalized = value.trim();
23812
23859
  if (!normalized || normalized.length > max) throw new ProviderError({
23813
23860
  provider,
@@ -23904,10 +23951,10 @@ function reserveProviderSpend(input, options = {}) {
23904
23951
  const now = options.now ?? Date.now();
23905
23952
  const normalized = {
23906
23953
  ...input,
23907
- endpoint: boundedText(input.provider, input.endpoint, "Provider endpoint", 500),
23908
- projectId: input.projectId ? boundedText(input.provider, input.projectId, "Project id", 200) : void 0,
23909
- reportId: boundedText(input.provider, input.reportId, "Report id", 100),
23910
- reportRunId: boundedText(input.provider, input.reportRunId, "Report run id", 100),
23954
+ endpoint: boundedText$1(input.provider, input.endpoint, "Provider endpoint", 500),
23955
+ projectId: input.projectId ? boundedText$1(input.provider, input.projectId, "Project id", 200) : void 0,
23956
+ reportId: boundedText$1(input.provider, input.reportId, "Report id", 100),
23957
+ reportRunId: boundedText$1(input.provider, input.reportRunId, "Report run id", 100),
23911
23958
  requestedRows: nonnegativeInteger(input.provider, input.requestedRows, "Requested rows"),
23912
23959
  estimatedCostMicros: nonnegativeInteger(input.provider, input.estimatedCostMicros, "Estimated cost")
23913
23960
  };
@@ -23944,7 +23991,7 @@ function reserveProviderSpend(input, options = {}) {
23944
23991
  function finalizeProviderSpend(reservationId, result, options = {}) {
23945
23992
  const database = options.database ?? getDb();
23946
23993
  const now = options.now ?? Date.now();
23947
- const taskIds = [...new Set(result.taskIds)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0).slice(0, MAX_TASK_IDS).map((id) => boundedText(result.provider, id, "Provider task id", 100));
23994
+ const taskIds = [...new Set(result.taskIds)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0).slice(0, MAX_TASK_IDS).map((id) => boundedText$1(result.provider, id, "Provider task id", 100));
23948
23995
  const actualCostMicros = result.actualCostMicros === null ? null : nonnegativeInteger(result.provider, result.actualCostMicros, "Actual cost");
23949
23996
  const returnedRows = result.returnedRows === null ? null : nonnegativeInteger(result.provider, result.returnedRows, "Returned rows");
23950
23997
  return database.transaction(() => {
@@ -24277,7 +24324,7 @@ const dataForSeoUserDataResponseSchema = z.object({
24277
24324
  const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
24278
24325
  const nullableNumber = z.number().finite().nullable().optional();
24279
24326
  const nullableCount = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
24280
- const monthlySearchSchema = z.object({
24327
+ const monthlySearchSchema$1 = z.object({
24281
24328
  year: z.number().int(),
24282
24329
  month: z.number().int(),
24283
24330
  search_volume: z.number().finite().nullable().optional()
@@ -24288,7 +24335,7 @@ const keywordOverviewItemSchema = z.object({
24288
24335
  search_volume: nullableNumber,
24289
24336
  cpc: nullableNumber,
24290
24337
  competition: nullableNumber,
24291
- monthly_searches: z.array(monthlySearchSchema).max(MAX_PROVIDER_MONTHLY_SEARCH_ROWS).nullish(),
24338
+ monthly_searches: z.array(monthlySearchSchema$1).max(MAX_PROVIDER_MONTHLY_SEARCH_ROWS).nullish(),
24292
24339
  last_updated_time: z.string().trim().min(1).nullable().optional()
24293
24340
  }).nullish(),
24294
24341
  keyword_properties: z.object({ keyword_difficulty: nullableNumber }).nullish(),
@@ -24302,7 +24349,7 @@ const keywordOverviewItemSchema = z.object({
24302
24349
  }).passthrough();
24303
24350
  const keywordOverviewResultSchema = z.object({
24304
24351
  items_count: z.number().int().nonnegative().optional(),
24305
- items: z.array(keywordOverviewItemSchema).optional()
24352
+ items: z.array(keywordOverviewItemSchema).nullable().optional()
24306
24353
  }).passthrough();
24307
24354
  const keywordOverviewTaskSchema = z.object({
24308
24355
  id: z.string().trim().min(1).optional(),
@@ -25253,13 +25300,55 @@ var DataForSeoKeywordMetricsProvider = class {
25253
25300
  status: "available",
25254
25301
  markets: [{
25255
25302
  searchEngines: ["google"],
25256
- location: "any"
25303
+ location: "country-only"
25257
25304
  }]
25258
25305
  }];
25259
25306
  client;
25260
25307
  constructor(options = {}) {
25261
25308
  this.client = options.client ?? new DataForSeoClient(options);
25262
25309
  }
25310
+ async estimateKeywordMetricsCost(input) {
25311
+ if (!Number.isSafeInteger(input.requestedRows) || input.requestedRows < 1 || input.requestedRows > 1e4) throw new ProviderError({
25312
+ provider: "dataforseo",
25313
+ operation: "keyword-metrics-cost",
25314
+ code: "configuration",
25315
+ message: "Keyword metric cost estimates require 1 to 10000 rows."
25316
+ });
25317
+ if (!searchMarketSchema.safeParse(input.market).success) throw new ProviderError({
25318
+ provider: "dataforseo",
25319
+ operation: "keyword-metrics-cost",
25320
+ code: "configuration",
25321
+ message: "Keyword metric cost estimates require a valid market."
25322
+ });
25323
+ if (input.market.location) throw new ProviderError({
25324
+ provider: "dataforseo",
25325
+ operation: "keyword-metrics-cost",
25326
+ code: "configuration",
25327
+ message: "DataForSEO Labs keyword metrics supports country-level markets; omit market.location and use countryCode."
25328
+ });
25329
+ if (!this.client.userData) throw new ProviderError({
25330
+ provider: "dataforseo",
25331
+ operation: "keyword-metrics-cost",
25332
+ code: "configuration",
25333
+ message: "Keyword metric cost estimation is unavailable."
25334
+ });
25335
+ const account = await this.client.userData();
25336
+ const { perRequestMicros, perResultMicros } = account.keywordOverviewPrice;
25337
+ const requestCount = Math.ceil(input.requestedRows / 50);
25338
+ const complete = perRequestMicros !== null && perResultMicros !== null;
25339
+ return {
25340
+ schemaVersion: 1,
25341
+ provider: "dataforseo",
25342
+ capability: "keyword-metrics",
25343
+ currency: "USD",
25344
+ requestedRows: input.requestedRows,
25345
+ requestCount,
25346
+ estimatedMicros: complete ? requestCount * perRequestMicros + input.requestedRows * perResultMicros : null,
25347
+ observedAt: account.observedAt,
25348
+ completeness: complete ? "complete" : "unavailable",
25349
+ basis: complete ? "Current account pricing for normal-priority live keyword overview requests." : "The provider account response did not include complete keyword overview pricing."
25350
+ };
25351
+ }
25263
25352
  async keywordMetrics(input) {
25264
25353
  const market = searchMarketSchema.parse(input.market);
25265
25354
  if (market.searchEngine !== "google") throw new ProviderError({
@@ -25268,6 +25357,12 @@ var DataForSeoKeywordMetricsProvider = class {
25268
25357
  code: "configuration",
25269
25358
  message: "DataForSEO keyword metrics currently supports Google markets."
25270
25359
  });
25360
+ if (market.location) throw new ProviderError({
25361
+ provider: "dataforseo",
25362
+ operation: "keyword-metrics",
25363
+ code: "configuration",
25364
+ message: "DataForSEO Labs keyword metrics supports country-level markets; omit market.location and use countryCode."
25365
+ });
25271
25366
  if (input.keywords.length < 1 || input.keywords.length > MAX_KEYWORDS_PER_REPORT || input.keywords.some((keyword) => {
25272
25367
  const normalized = keyword.trim();
25273
25368
  return normalized.length < 1 || normalized.length > 80 || normalized.split(/\s+/u).length > 10;
@@ -25474,7 +25569,7 @@ function analyzeKeywordTrend(metric) {
25474
25569
  function keywordMetricsProvider(candidate) {
25475
25570
  return "keywordMetrics" in candidate && typeof candidate.keywordMetrics === "function" ? candidate : null;
25476
25571
  }
25477
- async function defaultCandidates$2() {
25572
+ async function defaultCandidates$3() {
25478
25573
  return [{
25479
25574
  adapter: new DataForSeoKeywordMetricsProvider(),
25480
25575
  connected: Boolean(await readDataForSeoCredentials()),
@@ -25483,6 +25578,7 @@ async function defaultCandidates$2() {
25483
25578
  }
25484
25579
  function providerResolutionError(input) {
25485
25580
  if (input.reason === "provider-not-connected") return new SeoError("PROVIDER_UNAVAILABLE", "No connected provider can supply keyword metrics. Run `seo providers dataforseo connect` first.");
25581
+ if (input.provider && input.reason === "market-not-supported") return new SeoError("INVALID_INPUT", input.provider === "dataforseo" ? "DataForSEO keyword metrics uses country-level Labs data. Omit location and keep countryCode, then run serp-results separately for a local snapshot." : `${input.provider} cannot supply keyword metrics for this market.`);
25486
25582
  if (input.provider) return new SeoError("PROVIDER_UNAVAILABLE", `${input.provider} cannot supply keyword metrics for this market.`);
25487
25583
  return new SeoError("PROVIDER_UNAVAILABLE", "No configured provider can supply keyword metrics for this market.");
25488
25584
  }
@@ -25528,7 +25624,7 @@ async function keywordMetricsReport(input, dependencies = {}) {
25528
25624
  const validated = validateInput$3(input);
25529
25625
  let candidates;
25530
25626
  try {
25531
- candidates = dependencies.candidates ?? await defaultCandidates$2();
25627
+ candidates = dependencies.candidates ?? await defaultCandidates$3();
25532
25628
  } catch (error) {
25533
25629
  return providerReportError(error);
25534
25630
  }
@@ -27087,7 +27183,7 @@ var DataForSeoKeywordDiscoveryProvider = class {
27087
27183
  status: "available",
27088
27184
  markets: [{
27089
27185
  searchEngines: ["google"],
27090
- location: "any"
27186
+ location: "country-only"
27091
27187
  }]
27092
27188
  }];
27093
27189
  client;
@@ -27102,6 +27198,12 @@ var DataForSeoKeywordDiscoveryProvider = class {
27102
27198
  code: "configuration",
27103
27199
  message: "DataForSEO keyword discovery currently supports Google."
27104
27200
  });
27201
+ if (market.location) throw new ProviderError({
27202
+ provider: "dataforseo",
27203
+ operation: "keyword-discovery",
27204
+ code: "configuration",
27205
+ message: "DataForSEO Labs keyword discovery supports country-level markets; omit market.location and use countryCode."
27206
+ });
27105
27207
  const seeds = [...new Set(input.seeds.map(normalizedKeyword$1))].filter(Boolean).sort(compareCodepoints$3);
27106
27208
  if (seeds.length < 1 || seeds.length > MAX_DISCOVERY_SEEDS || seeds.some((seed) => seed.length > 80 || seed.split(/\s+/u).length > 10)) throw new ProviderError({
27107
27209
  provider: "dataforseo",
@@ -27217,14 +27319,14 @@ function compareCodepoints$1(left, right) {
27217
27319
  function discoveryProvider(candidate) {
27218
27320
  return "discoverKeywords" in candidate && typeof candidate.discoverKeywords === "function" ? candidate : null;
27219
27321
  }
27220
- async function defaultCandidates$1() {
27322
+ async function defaultCandidates$2() {
27221
27323
  return [{
27222
27324
  adapter: new DataForSeoKeywordDiscoveryProvider(),
27223
27325
  connected: Boolean(await readDataForSeoCredentials()),
27224
27326
  priority: 10
27225
27327
  }];
27226
27328
  }
27227
- function providerError$1(error) {
27329
+ function providerError$2(error) {
27228
27330
  if (!(error instanceof ProviderError)) throw error;
27229
27331
  if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
27230
27332
  if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
@@ -27290,9 +27392,9 @@ async function keywordResearchReport(input, dependencies = {}) {
27290
27392
  });
27291
27393
  let candidates;
27292
27394
  try {
27293
- candidates = dependencies.candidates ?? await defaultCandidates$1();
27395
+ candidates = dependencies.candidates ?? await defaultCandidates$2();
27294
27396
  } catch (error) {
27295
- return providerError$1(error);
27397
+ return providerError$2(error);
27296
27398
  }
27297
27399
  const resolution = resolveProvider({
27298
27400
  capability: "keyword-discovery",
@@ -27300,7 +27402,10 @@ async function keywordResearchReport(input, dependencies = {}) {
27300
27402
  candidates,
27301
27403
  provider: validated.provider
27302
27404
  });
27303
- if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can discover keywords. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot discover keywords for this market.` : "No configured provider can discover keywords for this market.");
27405
+ if (resolution.status === "unavailable") {
27406
+ const message = resolution.reason === "provider-not-connected" ? "No connected provider can discover keywords. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot discover keywords for this market.` : "No configured provider can discover keywords for this market.";
27407
+ throw new SeoError(validated.provider && resolution.reason === "market-not-supported" ? "INVALID_INPUT" : "PROVIDER_UNAVAILABLE", validated.provider === "dataforseo" && resolution.reason === "market-not-supported" ? "DataForSEO keyword discovery uses country-level Labs data. Omit location and keep countryCode, then run serp-results separately for a local snapshot." : message);
27408
+ }
27304
27409
  const provider = discoveryProvider(resolution.provider);
27305
27410
  if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no keyword discovery implementation.");
27306
27411
  let evidence;
@@ -27318,7 +27423,7 @@ async function keywordResearchReport(input, dependencies = {}) {
27318
27423
  }
27319
27424
  });
27320
27425
  } catch (error) {
27321
- return providerError$1(error);
27426
+ return providerError$2(error);
27322
27427
  }
27323
27428
  const analysis = evidence.data.map((idea) => ({
27324
27429
  keyword: idea.keyword,
@@ -32022,6 +32127,7 @@ var DataForSeoSerpSnapshotProvider = class {
32022
32127
  const organicResults = mappedOrganicResults.slice(0, input.depth);
32023
32128
  const invalidRows = organicItems.length - mappedOrganicResults.length;
32024
32129
  const resultCapped = mappedOrganicResults.length > input.depth;
32130
+ const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
32025
32131
  if (invalidRows > 0) warnings.push({
32026
32132
  code: "invalid-organic-results",
32027
32133
  field: "organicResults",
@@ -32038,6 +32144,11 @@ var DataForSeoSerpSnapshotProvider = class {
32038
32144
  field: "checkUrl",
32039
32145
  message: "DataForSEO returned an invalid SERP check URL."
32040
32146
  });
32147
+ if (invalidResultCount) warnings.push({
32148
+ code: "invalid-serp-result-count",
32149
+ field: "resultCount",
32150
+ message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
32151
+ });
32041
32152
  const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
32042
32153
  return {
32043
32154
  schemaVersion: 1,
@@ -32049,7 +32160,7 @@ var DataForSeoSerpSnapshotProvider = class {
32049
32160
  searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
32050
32161
  checkedAt: checkedAt(result.datetime, snapshot.observedAt),
32051
32162
  checkUrl,
32052
- resultCount: result.se_results_count === void 0 ? null : result.se_results_count,
32163
+ resultCount: invalidResultCount ? null : result.se_results_count ?? null,
32053
32164
  pagesCount: result.pages_count === void 0 ? null : result.pages_count,
32054
32165
  features,
32055
32166
  organicResults
@@ -32064,8 +32175,8 @@ var DataForSeoSerpSnapshotProvider = class {
32064
32175
  returnedRows: snapshot.returnedRows,
32065
32176
  retainedRows: organicResults.length,
32066
32177
  invalidRows,
32067
- providerTotalRows: result.se_results_count ?? null,
32068
- completeness: invalidRows > 0 ? "partial" : resultCapped ? "capped" : "complete",
32178
+ providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
32179
+ completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
32069
32180
  nextCursor: null
32070
32181
  },
32071
32182
  cache: snapshot.cache,
@@ -32095,14 +32206,14 @@ function compareCodepoints(left, right) {
32095
32206
  function snapshotProvider(candidate) {
32096
32207
  return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
32097
32208
  }
32098
- async function defaultCandidates() {
32209
+ async function defaultCandidates$1() {
32099
32210
  return [{
32100
32211
  adapter: new DataForSeoSerpSnapshotProvider(),
32101
32212
  connected: Boolean(await readDataForSeoCredentials()),
32102
32213
  priority: 10
32103
32214
  }];
32104
32215
  }
32105
- function providerError(error) {
32216
+ function providerError$1(error) {
32106
32217
  if (!(error instanceof ProviderError)) throw error;
32107
32218
  if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
32108
32219
  if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
@@ -32162,9 +32273,9 @@ async function serpResultsReport(input, dependencies = {}) {
32162
32273
  });
32163
32274
  let candidates;
32164
32275
  try {
32165
- candidates = dependencies.candidates ?? await defaultCandidates();
32276
+ candidates = dependencies.candidates ?? await defaultCandidates$1();
32166
32277
  } catch (error) {
32167
- return providerError(error);
32278
+ return providerError$1(error);
32168
32279
  }
32169
32280
  const resolution = resolveProvider({
32170
32281
  capability: "serp-snapshot",
@@ -32189,7 +32300,7 @@ async function serpResultsReport(input, dependencies = {}) {
32189
32300
  }
32190
32301
  });
32191
32302
  } catch (error) {
32192
- return providerError(error);
32303
+ return providerError$1(error);
32193
32304
  }
32194
32305
  const domains = domainSummaries(evidence.data);
32195
32306
  const dataStatus = reportStatus$1(evidence);
@@ -32904,6 +33015,722 @@ function reportPresentation(report) {
32904
33015
  };
32905
33016
  }
32906
33017
  //#endregion
33018
+ //#region packages/core/src/keyword-sets/limits.ts
33019
+ const KEYWORD_SET_LIMITS = {
33020
+ setsPerProject: 25,
33021
+ totalSets: 100,
33022
+ keywordsPerSet: 1e4,
33023
+ totalKeywords: 1e5,
33024
+ mutationKeywords: 1e3,
33025
+ tagsPerKeyword: 20,
33026
+ outputRows: 1e3,
33027
+ logicalBytes: 64 * 1024 * 1024
33028
+ };
33029
+ const KEYWORD_SET_FIELD_LIMITS = {
33030
+ name: 80,
33031
+ projectId: 80,
33032
+ sourceReport: 100,
33033
+ keyword: 80,
33034
+ keywordWords: 10,
33035
+ tag: 40,
33036
+ url: 2048,
33037
+ metricJsonBytes: 16 * 1024
33038
+ };
33039
+ //#endregion
33040
+ //#region packages/core/src/keyword-sets/validation.ts
33041
+ const unavailableValueSchema = z.strictObject({
33042
+ state: z.enum([
33043
+ "missing",
33044
+ "unavailable",
33045
+ "invalid"
33046
+ ]),
33047
+ value: z.null(),
33048
+ reason: z.string().min(1).max(500)
33049
+ });
33050
+ const providerValueNumberSchema = z.discriminatedUnion("state", [z.strictObject({
33051
+ state: z.literal("observed"),
33052
+ value: z.number().finite()
33053
+ }), unavailableValueSchema]);
33054
+ const providerValueStringSchema = z.discriminatedUnion("state", [z.strictObject({
33055
+ state: z.literal("observed"),
33056
+ value: z.string().max(500)
33057
+ }), unavailableValueSchema]);
33058
+ const monthlySearchSchema = z.strictObject({
33059
+ year: z.number().int().min(2e3).max(2200),
33060
+ month: z.number().int().min(1).max(12),
33061
+ searchVolume: z.number().finite().nonnegative()
33062
+ });
33063
+ const providerValueMonthlySearchesSchema = z.discriminatedUnion("state", [z.strictObject({
33064
+ state: z.literal("observed"),
33065
+ value: z.array(monthlySearchSchema).max(24)
33066
+ }), unavailableValueSchema]);
33067
+ const keywordMetricSchema = z.strictObject({
33068
+ keyword: z.string().min(1).max(KEYWORD_SET_FIELD_LIMITS.keyword),
33069
+ monthlySearchVolume: providerValueNumberSchema,
33070
+ monthlySearches: providerValueMonthlySearchesSchema,
33071
+ searchVolumeUpdatedAt: providerValueStringSchema,
33072
+ cpcUsd: providerValueNumberSchema,
33073
+ paidCompetition: providerValueNumberSchema,
33074
+ keywordDifficulty: providerValueNumberSchema,
33075
+ intent: providerValueStringSchema,
33076
+ resultCount: providerValueNumberSchema
33077
+ });
33078
+ const savedMetricSchema = z.strictObject({
33079
+ schemaVersion: z.literal(1),
33080
+ provider: providerIdSchema,
33081
+ observedAt: z.iso.datetime(),
33082
+ metric: keywordMetricSchema
33083
+ });
33084
+ function invalid(message) {
33085
+ throw new SeoError("INVALID_INPUT", message);
33086
+ }
33087
+ function boundedText(value, label, max) {
33088
+ const normalized = value.trim();
33089
+ if (!normalized || normalized.length > max) invalid(`${label} must contain 1 to ${max} characters.`);
33090
+ return normalized;
33091
+ }
33092
+ function normalizeSavedKeyword(value) {
33093
+ const normalized = value.trim().replace(/\s+/gu, " ").toLowerCase();
33094
+ if (!normalized || normalized.length > KEYWORD_SET_FIELD_LIMITS.keyword || normalized.split(" ").length > KEYWORD_SET_FIELD_LIMITS.keywordWords) invalid(`Keywords must contain 1 to ${KEYWORD_SET_FIELD_LIMITS.keyword} characters and at most ${KEYWORD_SET_FIELD_LIMITS.keywordWords} words.`);
33095
+ return normalized;
33096
+ }
33097
+ function displayKeyword(value) {
33098
+ normalizeSavedKeyword(value);
33099
+ return value.trim().replace(/\s+/gu, " ");
33100
+ }
33101
+ function normalizedTags(values = []) {
33102
+ if (values.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
33103
+ const tags = [...new Set(values.map((value) => boundedText(value, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase()))].sort();
33104
+ if (tags.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
33105
+ return tags;
33106
+ }
33107
+ function pageMapping(value) {
33108
+ if (value === null || value === void 0) return value;
33109
+ if (value.kind !== "target" && value.kind !== "proposed") invalid("Page mapping kind must be target or proposed.");
33110
+ const raw = boundedText(value.url, "Page URL", KEYWORD_SET_FIELD_LIMITS.url);
33111
+ let url;
33112
+ try {
33113
+ url = new URL(raw);
33114
+ } catch {
33115
+ invalid("Page mapping must use an absolute HTTP or HTTPS URL.");
33116
+ }
33117
+ if (url.protocol !== "http:" && url.protocol !== "https:") invalid("Page mapping must use an absolute HTTP or HTTPS URL.");
33118
+ return {
33119
+ kind: value.kind,
33120
+ url: url.toString()
33121
+ };
33122
+ }
33123
+ function savedMetric(value, normalizedKeyword) {
33124
+ if (value === null || value === void 0) return {
33125
+ value,
33126
+ json: null
33127
+ };
33128
+ const parsed = savedMetricSchema.safeParse(value);
33129
+ if (!parsed.success) invalid("Use a valid normalized keyword metric snapshot.");
33130
+ if (normalizeSavedKeyword(parsed.data.metric.keyword) !== normalizedKeyword) invalid("A saved metric must describe the same keyword as the set item.");
33131
+ const json = JSON.stringify(parsed.data);
33132
+ if (Buffer.byteLength(json) > KEYWORD_SET_FIELD_LIMITS.metricJsonBytes) invalid(`A saved keyword metric cannot exceed ${KEYWORD_SET_FIELD_LIMITS.metricJsonBytes} bytes.`);
33133
+ return {
33134
+ value: parsed.data,
33135
+ json
33136
+ };
33137
+ }
33138
+ function validateMutationItems(items) {
33139
+ if (items.length < 1 || items.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid(`Add 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
33140
+ const normalized = /* @__PURE__ */ new Map();
33141
+ for (const item of items) {
33142
+ const key = normalizeSavedKeyword(item.keyword);
33143
+ const existing = normalized.get(key);
33144
+ const tags = normalizedTags([...existing?.tags ?? [], ...item.tags ?? []]);
33145
+ const page = pageMapping(item.page !== void 0 ? item.page : existing?.page);
33146
+ const metric = savedMetric(item.latestMetric !== void 0 ? item.latestMetric : existing?.metric, key);
33147
+ normalized.set(key, {
33148
+ keyword: existing?.keyword ?? displayKeyword(item.keyword),
33149
+ tags,
33150
+ page,
33151
+ metric: metric.value,
33152
+ metricJson: metric.json
33153
+ });
33154
+ }
33155
+ return normalized;
33156
+ }
33157
+ function parseStoredJson(value, schema, label) {
33158
+ let decoded;
33159
+ try {
33160
+ decoded = JSON.parse(value);
33161
+ } catch {
33162
+ throw new SeoError("INTERNAL_ERROR", `${label} contains invalid JSON.`);
33163
+ }
33164
+ const parsed = schema.safeParse(decoded);
33165
+ if (!parsed.success) throw new SeoError("INTERNAL_ERROR", `${label} has an invalid shape.`);
33166
+ return parsed.data;
33167
+ }
33168
+ //#endregion
33169
+ //#region packages/core/src/keyword-sets/rows.ts
33170
+ const SET_SELECT = `
33171
+ SELECT sets.*,
33172
+ (SELECT COUNT(*) FROM keyword_set_items items WHERE items.set_id = sets.id) AS keyword_count,
33173
+ (SELECT COUNT(DISTINCT tags.tag) FROM keyword_set_tags tags WHERE tags.set_id = sets.id) AS tag_count
33174
+ FROM keyword_sets sets`;
33175
+ function storeDatabase(options) {
33176
+ return options.database ?? getDb();
33177
+ }
33178
+ function rowToSet(row) {
33179
+ const provider = row.provider ? providerIdSchema.safeParse(row.provider) : void 0;
33180
+ if (provider && !provider.success) throw new SeoError("INTERNAL_ERROR", "Saved keyword set provider is invalid.");
33181
+ return {
33182
+ schemaVersion: 1,
33183
+ id: row.id,
33184
+ projectId: row.project_id,
33185
+ name: row.name,
33186
+ market: parseStoredJson(row.market_json, searchMarketSchema, "Saved search market"),
33187
+ provider: provider?.data ?? null,
33188
+ sourceReport: row.source_report,
33189
+ keywordCount: row.keyword_count,
33190
+ tagCount: row.tag_count,
33191
+ createdAt: new Date(row.created_at).toISOString(),
33192
+ updatedAt: new Date(row.updated_at).toISOString(),
33193
+ lastRefreshedAt: row.last_refreshed_at === null ? null : new Date(row.last_refreshed_at).toISOString()
33194
+ };
33195
+ }
33196
+ function findSetRow(db, projectId, idOrName) {
33197
+ return db.prepare(`${SET_SELECT}
33198
+ WHERE sets.project_id = ?
33199
+ AND (sets.id = ? OR sets.name = ? COLLATE NOCASE)
33200
+ ORDER BY CASE WHEN sets.id = ? THEN 0 ELSE 1 END
33201
+ LIMIT 1`).get(projectId, idOrName, idOrName, idOrName);
33202
+ }
33203
+ function requireSetRow(db, projectId, idOrName) {
33204
+ const row = findSetRow(db, projectId, idOrName);
33205
+ if (!row) invalid(`Keyword set not found: ${idOrName}`);
33206
+ return row;
33207
+ }
33208
+ function rowCount(db, sql, ...params) {
33209
+ return db.prepare(sql).get(...params).count;
33210
+ }
33211
+ function keywordSetLogicalBytes(db = getDb()) {
33212
+ return db.prepare(`SELECT
33213
+ COALESCE((SELECT SUM(length(id) + length(project_id) + length(name) + length(market_json) + COALESCE(length(provider), 0) + COALESCE(length(source_report), 0)) FROM keyword_sets), 0) +
33214
+ COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(display_keyword) + COALESCE(length(metric_json), 0) + COALESCE(length(metric_provider), 0) + COALESCE(length(metric_observed_at), 0) + COALESCE(length(page_kind), 0) + COALESCE(length(page_url), 0)) FROM keyword_set_items), 0) +
33215
+ COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(tag)) FROM keyword_set_tags), 0)
33216
+ AS bytes`).get().bytes;
33217
+ }
33218
+ function enforceKeywordSetLogicalBytes(db, maxLogicalBytes = KEYWORD_SET_LIMITS.logicalBytes) {
33219
+ if (keywordSetLogicalBytes(db) > maxLogicalBytes) invalid(`Saved keyword data cannot exceed ${maxLogicalBytes} logical bytes. Remove an unused set before adding more.`);
33220
+ }
33221
+ //#endregion
33222
+ //#region packages/core/src/keyword-sets/mutations.ts
33223
+ function addKeywordsToSet(input, options = {}) {
33224
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33225
+ const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
33226
+ const items = validateMutationItems(input.items);
33227
+ const db = storeDatabase(options);
33228
+ const set = requireSetRow(db, projectId, idOrName);
33229
+ const currentSetCount = rowCount(db, "SELECT COUNT(*) AS count FROM keyword_set_items WHERE set_id = ?", set.id);
33230
+ const currentTotalCount = rowCount(db, "SELECT COUNT(*) AS count FROM keyword_set_items");
33231
+ const existingRows = db.prepare(`SELECT * FROM keyword_set_items
33232
+ WHERE set_id = ? AND normalized_keyword IN (${[...items].map(() => "?").join(", ")})`).all(set.id, ...items.keys());
33233
+ const existing = new Map(existingRows.map((row) => [row.normalized_keyword, row]));
33234
+ const additions = items.size - existing.size;
33235
+ if (currentSetCount + additions > KEYWORD_SET_LIMITS.keywordsPerSet) invalid(`A keyword set can contain at most ${KEYWORD_SET_LIMITS.keywordsPerSet} keywords.`);
33236
+ if (currentTotalCount + additions > KEYWORD_SET_LIMITS.totalKeywords) invalid(`At most ${KEYWORD_SET_LIMITS.totalKeywords} keywords can be saved across all sets.`);
33237
+ const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
33238
+ const updated = /* @__PURE__ */ new Set();
33239
+ db.transaction(() => {
33240
+ for (const [normalizedKeyword, item] of items) {
33241
+ const existingItem = existing.get(normalizedKeyword);
33242
+ if (!existingItem) insertKeyword(db, set.id, normalizedKeyword, item, now);
33243
+ else if (updateKeyword(db, set.id, normalizedKeyword, item, existingItem, now)) updated.add(normalizedKeyword);
33244
+ if (addTags(db, set.id, normalizedKeyword, item.tags) && existingItem) {
33245
+ updated.add(normalizedKeyword);
33246
+ db.prepare(`UPDATE keyword_set_items SET updated_at = ?
33247
+ WHERE set_id = ? AND normalized_keyword = ?`).run(now, set.id, normalizedKeyword);
33248
+ }
33249
+ }
33250
+ if (additions > 0 || updated.size > 0) db.prepare("UPDATE keyword_sets SET updated_at = ? WHERE id = ?").run(now, set.id);
33251
+ enforceKeywordSetLogicalBytes(db, options.maxLogicalBytes);
33252
+ }).immediate();
33253
+ return {
33254
+ setId: set.id,
33255
+ requested: input.items.length,
33256
+ normalized: items.size,
33257
+ added: additions,
33258
+ removed: 0,
33259
+ existing: existing.size,
33260
+ updated: updated.size,
33261
+ keywordCount: currentSetCount + additions
33262
+ };
33263
+ }
33264
+ function insertKeyword(db, setId, normalizedKeyword, item, now) {
33265
+ db.prepare(`INSERT INTO keyword_set_items
33266
+ (set_id, normalized_keyword, display_keyword, metric_json, metric_provider,
33267
+ metric_observed_at, page_kind, page_url, created_at, updated_at)
33268
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(setId, normalizedKeyword, item.keyword, item.metricJson, item.metric?.provider ?? null, item.metric?.observedAt ?? null, item.page?.kind ?? null, item.page?.url ?? null, now, now);
33269
+ }
33270
+ function updateKeyword(db, setId, normalizedKeyword, item, existingItem, now) {
33271
+ const pageChanged = item.page !== void 0 && (existingItem.page_kind !== (item.page?.kind ?? null) || existingItem.page_url !== (item.page?.url ?? null));
33272
+ const metricChanged = item.metric !== void 0 && existingItem.metric_json !== item.metricJson;
33273
+ if (!pageChanged && !metricChanged) return false;
33274
+ db.prepare(`UPDATE keyword_set_items SET
33275
+ page_kind = CASE WHEN ? THEN ? ELSE page_kind END,
33276
+ page_url = CASE WHEN ? THEN ? ELSE page_url END,
33277
+ metric_json = CASE WHEN ? THEN ? ELSE metric_json END,
33278
+ metric_provider = CASE WHEN ? THEN ? ELSE metric_provider END,
33279
+ metric_observed_at = CASE WHEN ? THEN ? ELSE metric_observed_at END,
33280
+ updated_at = ?
33281
+ WHERE set_id = ? AND normalized_keyword = ?`).run(pageChanged ? 1 : 0, item.page?.kind ?? null, pageChanged ? 1 : 0, item.page?.url ?? null, metricChanged ? 1 : 0, item.metricJson, metricChanged ? 1 : 0, item.metric?.provider ?? null, metricChanged ? 1 : 0, item.metric?.observedAt ?? null, now, setId, normalizedKeyword);
33282
+ return true;
33283
+ }
33284
+ function addTags(db, setId, normalizedKeyword, tags) {
33285
+ let changed = false;
33286
+ for (const tag of tags) {
33287
+ const inserted = db.prepare(`INSERT OR IGNORE INTO keyword_set_tags
33288
+ (set_id, normalized_keyword, tag) VALUES (?, ?, ?)`).run(setId, normalizedKeyword, tag);
33289
+ changed ||= inserted.changes > 0;
33290
+ }
33291
+ return changed;
33292
+ }
33293
+ function removeKeywordsFromSet(input, options = {}) {
33294
+ if (input.keywords.length < 1 || input.keywords.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid(`Remove 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
33295
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33296
+ const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
33297
+ const keywords = [...new Set(input.keywords.map(normalizeSavedKeyword))];
33298
+ const db = storeDatabase(options);
33299
+ const set = requireSetRow(db, projectId, idOrName);
33300
+ const removed = db.prepare(`DELETE FROM keyword_set_items
33301
+ WHERE set_id = ? AND normalized_keyword IN (${keywords.map(() => "?").join(", ")})`).run(set.id, ...keywords).changes;
33302
+ const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
33303
+ if (removed > 0) db.prepare("UPDATE keyword_sets SET updated_at = ? WHERE id = ?").run(now, set.id);
33304
+ const keywordCount = rowCount(db, "SELECT COUNT(*) AS count FROM keyword_set_items WHERE set_id = ?", set.id);
33305
+ return {
33306
+ setId: set.id,
33307
+ requested: input.keywords.length,
33308
+ normalized: keywords.length,
33309
+ added: 0,
33310
+ removed,
33311
+ existing: keywords.length - removed,
33312
+ updated: 0,
33313
+ keywordCount
33314
+ };
33315
+ }
33316
+ function setKeywordSetRefreshTime(input, options = {}) {
33317
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33318
+ const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
33319
+ const refreshedAt = Date.parse(input.refreshedAt);
33320
+ if (!Number.isFinite(refreshedAt)) invalid("Use a valid refresh timestamp.");
33321
+ const db = storeDatabase(options);
33322
+ const row = requireSetRow(db, projectId, idOrName);
33323
+ const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
33324
+ db.prepare("UPDATE keyword_sets SET last_refreshed_at = ?, updated_at = ? WHERE id = ?").run(refreshedAt, now, row.id);
33325
+ return rowToSet(requireSetRow(db, projectId, row.id));
33326
+ }
33327
+ //#endregion
33328
+ //#region packages/core/src/keyword-sets/store.ts
33329
+ function createKeywordSet(input, options = {}) {
33330
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33331
+ const name = boundedText(input.name, "Keyword set name", KEYWORD_SET_FIELD_LIMITS.name);
33332
+ const market = searchMarketSchema.safeParse(input.market);
33333
+ if (!market.success) invalid("Use a valid search market.");
33334
+ const provider = input.provider === void 0 ? void 0 : providerIdSchema.safeParse(input.provider);
33335
+ if (provider && !provider.success) invalid("Use a supported keyword provider.");
33336
+ const sourceReport = input.sourceReport ? boundedText(input.sourceReport, "Source report", KEYWORD_SET_FIELD_LIMITS.sourceReport) : null;
33337
+ const db = storeDatabase(options);
33338
+ if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets") >= KEYWORD_SET_LIMITS.totalSets) invalid(`At most ${KEYWORD_SET_LIMITS.totalSets} keyword sets can be saved.`);
33339
+ if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets WHERE project_id = ?", projectId) >= KEYWORD_SET_LIMITS.setsPerProject) invalid(`A project can have at most ${KEYWORD_SET_LIMITS.setsPerProject} keyword sets.`);
33340
+ const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
33341
+ const id = (options.id ?? randomUUID)();
33342
+ try {
33343
+ db.transaction(() => {
33344
+ db.prepare(`INSERT INTO keyword_sets
33345
+ (id, project_id, name, market_json, provider, source_report, created_at, updated_at)
33346
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, projectId, name, JSON.stringify(market.data), provider?.data ?? null, sourceReport, now, now);
33347
+ enforceKeywordSetLogicalBytes(db, options.maxLogicalBytes);
33348
+ }).immediate();
33349
+ } catch (error) {
33350
+ if (/unique constraint/i.test(String(error))) invalid(`A keyword set named ${name} already exists for this project.`);
33351
+ throw error;
33352
+ }
33353
+ return rowToSet(requireSetRow(db, projectId, id));
33354
+ }
33355
+ function listKeywordSets(input, options = {}) {
33356
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33357
+ const limit = input.limit ?? 100;
33358
+ const offset = input.offset ?? 0;
33359
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) invalid("Keyword set list limit must be from 1 to 100.");
33360
+ if (!Number.isSafeInteger(offset) || offset < 0) invalid("Keyword set list offset must be zero or greater.");
33361
+ return storeDatabase(options).prepare(`${SET_SELECT}
33362
+ WHERE sets.project_id = ?
33363
+ ORDER BY sets.updated_at DESC, sets.id
33364
+ LIMIT ? OFFSET ?`).all(projectId, limit, offset).map(rowToSet);
33365
+ }
33366
+ function getKeywordSet(input, options = {}) {
33367
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33368
+ const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
33369
+ const tag = input.tag ? boundedText(input.tag, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase() : null;
33370
+ const limit = input.limit ?? 100;
33371
+ const offset = input.offset ?? 0;
33372
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > KEYWORD_SET_LIMITS.outputRows) invalid(`Keyword set output limit must be from 1 to ${KEYWORD_SET_LIMITS.outputRows}.`);
33373
+ if (!Number.isSafeInteger(offset) || offset < 0) invalid("Keyword set output offset must be zero or greater.");
33374
+ const db = storeDatabase(options);
33375
+ const setRow = requireSetRow(db, projectId, idOrName);
33376
+ const filterSql = tag ? `AND EXISTS (
33377
+ SELECT 1 FROM keyword_set_tags filter_tags
33378
+ WHERE filter_tags.set_id = items.set_id
33379
+ AND filter_tags.normalized_keyword = items.normalized_keyword
33380
+ AND filter_tags.tag = ?
33381
+ )` : "";
33382
+ const params = tag ? [setRow.id, tag] : [setRow.id];
33383
+ const total = rowCount(db, `SELECT COUNT(*) AS count FROM keyword_set_items items
33384
+ WHERE items.set_id = ? ${filterSql}`, ...params);
33385
+ const rows = db.prepare(`SELECT items.* FROM keyword_set_items items
33386
+ WHERE items.set_id = ? ${filterSql}
33387
+ ORDER BY items.normalized_keyword
33388
+ LIMIT ? OFFSET ?`).all(...params, limit, offset);
33389
+ const tagsByKeyword = loadTags(db, setRow.id, rows);
33390
+ const items = rows.map((row) => itemFromRow(row, tagsByKeyword.get(row.normalized_keyword) ?? []));
33391
+ return {
33392
+ schemaVersion: 1,
33393
+ set: rowToSet(setRow),
33394
+ items,
33395
+ pagination: {
33396
+ offset,
33397
+ limit,
33398
+ returned: items.length,
33399
+ total,
33400
+ nextOffset: offset + items.length < total ? offset + items.length : null
33401
+ },
33402
+ filter: { tag }
33403
+ };
33404
+ }
33405
+ function loadTags(db, setId, rows) {
33406
+ if (rows.length === 0) return /* @__PURE__ */ new Map();
33407
+ const tagRows = db.prepare(`SELECT normalized_keyword, tag FROM keyword_set_tags
33408
+ WHERE set_id = ? AND normalized_keyword IN (${rows.map(() => "?").join(", ")})
33409
+ ORDER BY normalized_keyword, tag`).all(setId, ...rows.map((row) => row.normalized_keyword));
33410
+ const tagsByKeyword = /* @__PURE__ */ new Map();
33411
+ for (const row of tagRows) {
33412
+ const tags = tagsByKeyword.get(row.normalized_keyword) ?? [];
33413
+ tags.push(row.tag);
33414
+ tagsByKeyword.set(row.normalized_keyword, tags);
33415
+ }
33416
+ return tagsByKeyword;
33417
+ }
33418
+ function itemFromRow(row, tags) {
33419
+ const latestMetric = row.metric_json ? parseStoredJson(row.metric_json, savedMetricSchema, "Saved keyword metric") : null;
33420
+ if (latestMetric && (latestMetric.provider !== row.metric_provider || latestMetric.observedAt !== row.metric_observed_at || normalizeSavedKeyword(latestMetric.metric.keyword) !== row.normalized_keyword)) throw new SeoError("INTERNAL_ERROR", "Saved keyword metric metadata is inconsistent.");
33421
+ const page = row.page_kind && row.page_url ? {
33422
+ kind: row.page_kind,
33423
+ url: row.page_url
33424
+ } : null;
33425
+ return {
33426
+ keyword: row.display_keyword,
33427
+ normalizedKeyword: row.normalized_keyword,
33428
+ tags,
33429
+ page,
33430
+ latestMetric,
33431
+ createdAt: new Date(row.created_at).toISOString(),
33432
+ updatedAt: new Date(row.updated_at).toISOString()
33433
+ };
33434
+ }
33435
+ function deleteKeywordSet(input, options = {}) {
33436
+ const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
33437
+ const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
33438
+ const db = storeDatabase(options);
33439
+ const row = findSetRow(db, projectId, idOrName);
33440
+ if (!row) return false;
33441
+ return db.prepare("DELETE FROM keyword_sets WHERE id = ?").run(row.id).changes > 0;
33442
+ }
33443
+ //#endregion
33444
+ //#region packages/core/src/keyword-sets/refresh.ts
33445
+ const REFRESH_BATCH_SIZE = 50;
33446
+ function estimator(adapter) {
33447
+ return "estimateKeywordMetricsCost" in adapter && typeof adapter.estimateKeywordMetricsCost === "function" ? adapter : null;
33448
+ }
33449
+ async function defaultCandidates() {
33450
+ return [{
33451
+ adapter: new DataForSeoKeywordMetricsProvider(),
33452
+ connected: Boolean(await readDataForSeoCredentials()),
33453
+ priority: 10
33454
+ }];
33455
+ }
33456
+ function providerError(error) {
33457
+ if (!(error instanceof ProviderError)) throw error;
33458
+ throw new SeoError(error.code === "rate-limit" ? "RATE_LIMITED" : "PROVIDER_UNAVAILABLE", error.message);
33459
+ }
33460
+ function summedCost(values) {
33461
+ return values.length > 0 && values.every((value) => value !== null) ? values.reduce((sum, value) => sum + (value ?? 0), 0) : null;
33462
+ }
33463
+ async function refreshKeywordSet(input, dependencies = {}) {
33464
+ const limit = input.limit ?? KEYWORD_SET_LIMITS.outputRows;
33465
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new SeoError("INVALID_INPUT", "Refresh limit must be from 1 to 1000.");
33466
+ if (input.provider && !providerIdSchema.safeParse(input.provider).success) throw new SeoError("INVALID_INPUT", "Use a supported keyword provider.");
33467
+ const offset = input.offset ?? 0;
33468
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1e5) throw new SeoError("INVALID_INPUT", "Refresh offset must be from 0 to 100000.");
33469
+ const detail = getKeywordSet({
33470
+ projectId: input.projectId,
33471
+ idOrName: input.idOrName,
33472
+ limit,
33473
+ offset
33474
+ }, dependencies.store);
33475
+ if (detail.items.length === 0) throw new SeoError("INSUFFICIENT_DATA", detail.set.keywordCount === 0 ? "The keyword set is empty." : "No keywords exist in the selected refresh window.");
33476
+ let candidates;
33477
+ try {
33478
+ candidates = dependencies.candidates ?? await defaultCandidates();
33479
+ } catch (error) {
33480
+ return providerError(error);
33481
+ }
33482
+ const resolution = resolveProvider({
33483
+ capability: "keyword-metrics",
33484
+ market: detail.set.market,
33485
+ candidates,
33486
+ provider: input.provider ?? detail.set.provider ?? void 0
33487
+ });
33488
+ if (resolution.status === "unavailable") {
33489
+ const selectedProvider = input.provider ?? detail.set.provider ?? void 0;
33490
+ throw new SeoError(resolution.reason === "market-not-supported" && selectedProvider ? "INVALID_INPUT" : "PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can refresh this keyword set." : resolution.reason === "market-not-supported" && selectedProvider === "dataforseo" ? "DataForSEO keyword estimates require a country-level saved set. Create the set without --location, then use a location-specific serp-results report for local result evidence." : "No configured provider can estimate and refresh this keyword set.");
33491
+ }
33492
+ const costEstimator = estimator(resolution.provider);
33493
+ if (!costEstimator) throw new SeoError("PROVIDER_UNAVAILABLE", `${resolution.provider.provider} cannot preview keyword metric costs.`);
33494
+ let cost;
33495
+ try {
33496
+ cost = await costEstimator.estimateKeywordMetricsCost({
33497
+ requestedRows: detail.items.length,
33498
+ market: detail.set.market
33499
+ });
33500
+ } catch (error) {
33501
+ return providerError(error);
33502
+ }
33503
+ if (cost.estimatedMicros === null) throw new SeoError("PROVIDER_UNAVAILABLE", "The provider did not return a usable cost estimate, so no paid refresh was started.");
33504
+ const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
33505
+ const generatedAt = now().toISOString();
33506
+ const completeSelection = detail.pagination.offset === 0 && detail.items.length === detail.set.keywordCount;
33507
+ const common = {
33508
+ schemaVersion: 1,
33509
+ generatedAt,
33510
+ set: {
33511
+ id: detail.set.id,
33512
+ name: detail.set.name,
33513
+ projectId: detail.set.projectId,
33514
+ totalKeywords: detail.set.keywordCount
33515
+ },
33516
+ selection: {
33517
+ offset: detail.pagination.offset,
33518
+ limit: detail.pagination.limit,
33519
+ selectedKeywords: detail.items.length,
33520
+ nextOffset: detail.pagination.nextOffset,
33521
+ completeness: completeSelection ? "complete" : "capped"
33522
+ },
33523
+ cost
33524
+ };
33525
+ if (!input.execute) return {
33526
+ ...common,
33527
+ mode: "preview",
33528
+ dataStatus: completeSelection ? "complete" : "partial",
33529
+ execution: null,
33530
+ caveats: ["This preview used current provider account pricing and did not start a paid keyword request.", ...completeSelection ? [] : ["The refresh limit covers only part of the saved keyword set."]],
33531
+ nextSteps: ["Review the selected market, keyword count, request count, and estimate before executing the refresh."]
33532
+ };
33533
+ const batches = Array.from({ length: Math.ceil(detail.items.length / REFRESH_BATCH_SIZE) }, (_, index) => detail.items.slice(index * REFRESH_BATCH_SIZE, (index + 1) * REFRESH_BATCH_SIZE));
33534
+ const warnings = [];
33535
+ const errors = [];
33536
+ const actualCosts = [];
33537
+ let savedSnapshots = 0;
33538
+ let completeBatches = 0;
33539
+ let partialBatches = 0;
33540
+ const reportRunId = randomUUID();
33541
+ for (const [index, batch] of batches.entries()) try {
33542
+ const report = await (dependencies.keywordMetrics ?? keywordMetricsReport)({
33543
+ keywords: batch.map((item) => item.keyword),
33544
+ market: detail.set.market,
33545
+ provider: resolution.provider.provider,
33546
+ projectId: detail.set.projectId,
33547
+ refresh: true,
33548
+ context: {
33549
+ reportId: "saved-keyword-refresh",
33550
+ reportRunId
33551
+ }
33552
+ }, {
33553
+ candidates,
33554
+ now
33555
+ });
33556
+ const metrics = new Map(report.evidence.data.map((metric) => [normalizeSavedKeyword(metric.keyword), metric]));
33557
+ const saved = batch.flatMap((item) => {
33558
+ const metric = metrics.get(item.normalizedKeyword);
33559
+ return metric ? [savedMetricItem(metric, report.evidence.provider, report.evidence.observedAt)] : [];
33560
+ });
33561
+ if (saved.length > 0) addKeywordsToSet({
33562
+ projectId: detail.set.projectId,
33563
+ idOrName: detail.set.id,
33564
+ items: saved
33565
+ }, dependencies.store);
33566
+ savedSnapshots += saved.length;
33567
+ actualCosts.push(report.evidence.cost.actualMicros);
33568
+ if (report.dataStatus !== "complete" || report.evidence.coverage.completeness !== "complete" || saved.length !== batch.length) {
33569
+ partialBatches += 1;
33570
+ warnings.push({
33571
+ batch: index + 1,
33572
+ message: saved.length !== batch.length ? `Batch retained ${saved.length} typed snapshots for ${batch.length} keywords.` : `Batch returned ${report.evidence.coverage.completeness} provider coverage for ${batch.length} keywords.`
33573
+ });
33574
+ continue;
33575
+ }
33576
+ completeBatches += 1;
33577
+ } catch (error) {
33578
+ errors.push({
33579
+ batch: index + 1,
33580
+ message: error instanceof Error ? error.message : String(error)
33581
+ });
33582
+ break;
33583
+ }
33584
+ if (errors.length === 0 && partialBatches === 0 && completeSelection) setKeywordSetRefreshTime({
33585
+ projectId: detail.set.projectId,
33586
+ idOrName: detail.set.id,
33587
+ refreshedAt: generatedAt
33588
+ }, dependencies.store);
33589
+ const complete = errors.length === 0 && partialBatches === 0 && completeSelection;
33590
+ return {
33591
+ ...common,
33592
+ mode: "executed",
33593
+ dataStatus: complete ? "complete" : savedSnapshots > 0 ? "partial" : "unavailable",
33594
+ execution: {
33595
+ attemptedBatches: completeBatches + partialBatches + errors.length,
33596
+ completeBatches,
33597
+ partialBatches,
33598
+ failedBatches: errors.length,
33599
+ savedSnapshots,
33600
+ actualMicros: summedCost(actualCosts),
33601
+ warnings,
33602
+ errors
33603
+ },
33604
+ caveats: [
33605
+ "Saved metrics are provider estimates for the set market, not first-party rankings or traffic forecasts.",
33606
+ ...completeSelection ? [] : ["The refresh limit covered only part of the saved keyword set."],
33607
+ ...errors.length ? ["Completed batches were retained; the failed batch and later batches remain stale."] : [],
33608
+ ...partialBatches > 0 ? ["Partial provider batches were retained with explicit missing or invalid states; the set was not marked fully refreshed."] : []
33609
+ ],
33610
+ nextSteps: [complete ? "Compare changed demand and competition evidence with Search Console before changing priorities." : "Resolve the incomplete coverage before treating the set as fully refreshed."]
33611
+ };
33612
+ }
33613
+ function savedMetricItem(metric, provider, observedAt) {
33614
+ return {
33615
+ keyword: metric.keyword,
33616
+ latestMetric: {
33617
+ schemaVersion: 1,
33618
+ provider,
33619
+ observedAt,
33620
+ metric
33621
+ }
33622
+ };
33623
+ }
33624
+ //#endregion
33625
+ //#region packages/core/src/analyze/saved-keywords.ts
33626
+ const DEFAULT_STALE_DAYS = 45;
33627
+ function savedKeywordSetReport(input, dependencies = {}) {
33628
+ const staleDays = input.staleDays ?? DEFAULT_STALE_DAYS;
33629
+ if (!Number.isSafeInteger(staleDays) || staleDays < 1 || staleDays > 365) throw new SeoError("INVALID_INPUT", "Metric freshness must be from 1 to 365 days.");
33630
+ const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
33631
+ const evidence = (dependencies.getKeywordSet ?? getKeywordSet)(input);
33632
+ const snapshots = evidence.items.filter((item) => item.latestMetric);
33633
+ const observedVolumes = snapshots.filter((item) => item.latestMetric?.metric.monthlySearchVolume.state === "observed");
33634
+ const staleCutoff = now.getTime() - staleDays * 864e5;
33635
+ const staleSnapshots = snapshots.filter((item) => Date.parse(item.latestMetric?.observedAt ?? "") < staleCutoff);
33636
+ const mapped = evidence.items.filter((item) => item.page);
33637
+ const tagGroups = groupedCounts(evidence.items.flatMap((item) => item.tags)).map(([tag, keywordCount]) => ({
33638
+ tag,
33639
+ keywordCount
33640
+ }));
33641
+ const pageMappings = groupedMappings(evidence);
33642
+ const missingMetrics = evidence.items.length - snapshots.length;
33643
+ const unavailableVolumeSnapshots = snapshots.length - observedVolumes.length;
33644
+ const unmapped = evidence.items.length - mapped.length;
33645
+ const partial = evidence.pagination.nextOffset !== null || evidence.pagination.offset > 0 || evidence.pagination.total !== evidence.set.keywordCount;
33646
+ const dataStatus = evidence.items.length === 0 ? "unavailable" : partial ? "partial" : "complete";
33647
+ return {
33648
+ schemaVersion: 1,
33649
+ generatedAt: now.toISOString(),
33650
+ dataStatus,
33651
+ summary: {
33652
+ name: evidence.set.name,
33653
+ totalKeywords: evidence.set.keywordCount,
33654
+ matchedKeywords: evidence.pagination.total,
33655
+ returnedKeywords: evidence.items.length,
33656
+ metricSnapshots: snapshots.length,
33657
+ observedVolumes: observedVolumes.length,
33658
+ observedZeroVolumes: observedVolumes.filter((item) => item.latestMetric?.metric.monthlySearchVolume.value === 0).length,
33659
+ unavailableVolumeSnapshots,
33660
+ staleMetricSnapshots: staleSnapshots.length,
33661
+ mappedKeywords: mapped.length,
33662
+ targetPages: pageMappings.filter((item) => item.kind === "target").length,
33663
+ proposedPages: pageMappings.filter((item) => item.kind === "proposed").length,
33664
+ distinctTags: tagGroups.length,
33665
+ verdict: `${evidence.items.length} saved keywords were returned; search volume is observed for ${observedVolumes.length}, unavailable in ${unavailableVolumeSnapshots} saved snapshot${unavailableVolumeSnapshots === 1 ? "" : "s"}, and ${mapped.length} have page mappings.`
33666
+ },
33667
+ evidence,
33668
+ analysis: {
33669
+ metricFreshnessDays: staleDays,
33670
+ tagGroups,
33671
+ pageMappings
33672
+ },
33673
+ findings: [
33674
+ ...missingMetrics > 0 ? [{
33675
+ code: "missing-metrics",
33676
+ evidenceRef: "evidence.items[*].latestMetric",
33677
+ detail: `${missingMetrics} returned keywords have no saved provider metric snapshot.`,
33678
+ action: "Preview a keyword-set refresh before using provider metrics to prioritize them."
33679
+ }] : [],
33680
+ ...unavailableVolumeSnapshots > 0 ? [{
33681
+ code: "unavailable-volume-evidence",
33682
+ evidenceRef: "evidence.items[*].latestMetric.metric.monthlySearchVolume",
33683
+ detail: `${unavailableVolumeSnapshots} saved metric snapshot${unavailableVolumeSnapshots === 1 ? "" : "s"} ${unavailableVolumeSnapshots === 1 ? "has" : "have"} no observed search volume. This is not an observed zero.`,
33684
+ action: "Keep these terms separate from observed-zero terms and verify them with another source or a later refresh before using demand as a filter."
33685
+ }] : [],
33686
+ ...staleSnapshots.length > 0 ? [{
33687
+ code: "stale-metrics",
33688
+ evidenceRef: "evidence.items[*].latestMetric.observedAt",
33689
+ detail: `${staleSnapshots.length} saved metric snapshots are older than ${staleDays} days.`,
33690
+ action: "Preview and selectively refresh the stale provider evidence."
33691
+ }] : [],
33692
+ ...unmapped > 0 ? [{
33693
+ code: "unmapped-keywords",
33694
+ evidenceRef: "evidence.items[*].page",
33695
+ detail: `${unmapped} returned keywords have no existing or proposed page mapping.`,
33696
+ action: "Review shared intent and current results before mapping terms to an existing page or a proposed page."
33697
+ }] : []
33698
+ ],
33699
+ caveats: [
33700
+ "Tags are user-managed research labels, not proof that keywords share one intent or should use one page.",
33701
+ "Provider metrics are market estimates; missing evidence differs from an observed zero.",
33702
+ "Page mappings record planning state and do not prove that a page ranks, is indexed, or should be created.",
33703
+ ...partial ? ["This view is filtered or paginated and cannot support a complete-set all-clear."] : []
33704
+ ],
33705
+ nextSteps: [
33706
+ "Compare relevant saved terms with Search Console evidence for the project before changing priorities.",
33707
+ "Inspect current results in the same market before turning a tag group into a page or programmatic template.",
33708
+ "Use the refresh preview to check provider cost before updating saved metric snapshots."
33709
+ ]
33710
+ };
33711
+ }
33712
+ function groupedCounts(values) {
33713
+ const counts = /* @__PURE__ */ new Map();
33714
+ for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
33715
+ return [...counts].sort(([left, leftCount], [right, rightCount]) => rightCount - leftCount || (left < right ? -1 : left > right ? 1 : 0)).slice(0, 50);
33716
+ }
33717
+ function groupedMappings(evidence) {
33718
+ const counts = /* @__PURE__ */ new Map();
33719
+ for (const item of evidence.items) {
33720
+ if (!item.page) continue;
33721
+ const key = `${item.page.kind}\u0000${item.page.url}`;
33722
+ const current = counts.get(key);
33723
+ counts.set(key, {
33724
+ ...item.page,
33725
+ count: (current?.count ?? 0) + 1
33726
+ });
33727
+ }
33728
+ return [...counts.values()].sort((left, right) => right.count - left.count || (left.url < right.url ? -1 : left.url > right.url ? 1 : 0)).slice(0, 50).map(({ count, ...mapping }) => ({
33729
+ ...mapping,
33730
+ keywordCount: count
33731
+ }));
33732
+ }
33733
+ //#endregion
32907
33734
  //#region packages/core/src/analyze/second-page.ts
32908
33735
  const MAX_SOURCE_ROWS = 1e5;
32909
33736
  const defaultDependencies = {
@@ -37546,6 +38373,7 @@ const TELEMETRY_REPORTS = [
37546
38373
  "keyword-metrics",
37547
38374
  "keyword-opportunities",
37548
38375
  "keyword-research",
38376
+ "saved-keywords",
37549
38377
  "link-evidence",
37550
38378
  "server-log-analysis",
37551
38379
  "link-recovery",
@@ -37891,6 +38719,6 @@ function isoDate(daysAgo = 0) {
37891
38719
  return date.toISOString().slice(0, 10);
37892
38720
  }
37893
38721
  //#endregion
37894
- export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, removeIndexNowKey, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, submitIndexNow, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
38722
+ export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, submitIndexNow, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
37895
38723
 
37896
38724
  //# sourceMappingURL=index.js.map