seo 0.13.3 → 0.14.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.
Files changed (88) hide show
  1. package/PRIVACY.md +9 -1
  2. package/dist/cli.js +84 -80
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{dist-BbTqJIE_.js → dist-4Yam8ps7.js} +1 -1
  5. package/dist/dist-B_2zww72.js +427 -0
  6. package/dist/dist-B_2zww72.js.map +1 -0
  7. package/dist/{esm-hHlvm1W0.js → esm-BmuxgZyN.js} +95 -188
  8. package/dist/{esm-hHlvm1W0.js.map → esm-BmuxgZyN.js.map} +1 -1
  9. package/dist/index.d.ts +73 -406
  10. package/dist/index.js +1448 -564
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.d.ts +0 -14
  13. package/dist/mcp.js +1020 -807
  14. package/dist/mcp.js.map +1 -1
  15. package/dist/provider-sdk.d.ts +0 -2
  16. package/dist/provider-sdk.js +1 -2
  17. package/package.json +6 -5
  18. package/site-kit/README.md +13 -0
  19. package/site-kit/guide/build.md +70 -0
  20. package/site-kit/guide/data.md +49 -0
  21. package/site-kit/guide/golden-page.md +50 -0
  22. package/site-kit/guide/grow.md +58 -0
  23. package/site-kit/guide/launch.md +55 -0
  24. package/site-kit/guide/market.md +57 -0
  25. package/site-kit/guide/plan.md +49 -0
  26. package/site-kit/guide/scale.md +41 -0
  27. package/site-kit/guide/setup.md +59 -0
  28. package/site-kit/guide/writing.md +55 -0
  29. package/site-kit/starter/.github/workflows/refresh-data.yml +88 -0
  30. package/site-kit/starter/.node-version +1 -0
  31. package/site-kit/starter/AGENTS.md +65 -0
  32. package/site-kit/starter/README.md +11 -0
  33. package/site-kit/starter/astro.config.mjs +31 -0
  34. package/site-kit/starter/gitignore +7 -0
  35. package/site-kit/starter/llms.config.mjs +38 -0
  36. package/site-kit/starter/package.json +31 -0
  37. package/site-kit/starter/pnpm-workspace.yaml +5 -0
  38. package/site-kit/starter/public/_headers +5 -0
  39. package/site-kit/starter/public/favicon.svg +1 -0
  40. package/site-kit/starter/public/fonts/InterLatinVF.woff2 +0 -0
  41. package/site-kit/starter/public/fonts/JetBrainsMonoLatinVF.woff2 +0 -0
  42. package/site-kit/starter/public/fonts/LICENSE-inter.txt +93 -0
  43. package/site-kit/starter/public/fonts/LICENSE-jetbrains-mono.txt +93 -0
  44. package/site-kit/starter/scripts/deploy.mjs +60 -0
  45. package/site-kit/starter/scripts/markdown-rewrites.mjs +33 -0
  46. package/site-kit/starter/scripts/refresh-data.mjs +60 -0
  47. package/site-kit/starter/scripts/validate-data.mjs +47 -0
  48. package/site-kit/starter/site.config.json +9 -0
  49. package/site-kit/starter/src/components/Breadcrumbs.astro +27 -0
  50. package/site-kit/starter/src/components/Button.astro +46 -0
  51. package/site-kit/starter/src/components/Card.astro +54 -0
  52. package/site-kit/starter/src/components/CodeBlock.astro +62 -0
  53. package/site-kit/starter/src/components/FactTable.astro +45 -0
  54. package/site-kit/starter/src/components/FaqAccordion.astro +143 -0
  55. package/site-kit/starter/src/components/Footer.astro +34 -0
  56. package/site-kit/starter/src/components/FormInput.astro +18 -0
  57. package/site-kit/starter/src/components/FormSelect.astro +34 -0
  58. package/site-kit/starter/src/components/Header.astro +17 -0
  59. package/site-kit/starter/src/components/HugeIcon.astro +36 -0
  60. package/site-kit/starter/src/components/Logo.astro +21 -0
  61. package/site-kit/starter/src/components/PageHeader.astro +33 -0
  62. package/site-kit/starter/src/components/SectionHeading.astro +18 -0
  63. package/site-kit/starter/src/components/ui/button.ts +51 -0
  64. package/site-kit/starter/src/data/parks.json +106 -0
  65. package/site-kit/starter/src/data/sources.json +11 -0
  66. package/site-kit/starter/src/integrations/og-images.ts +49 -0
  67. package/site-kit/starter/src/integrations/sitemaps.ts +104 -0
  68. package/site-kit/starter/src/layouts/Base.astro +69 -0
  69. package/site-kit/starter/src/lib/cn.ts +5 -0
  70. package/site-kit/starter/src/lib/narrative.ts +115 -0
  71. package/site-kit/starter/src/lib/og/card.ts +85 -0
  72. package/site-kit/starter/src/lib/og/path.ts +14 -0
  73. package/site-kit/starter/src/lib/og/render.ts +30 -0
  74. package/site-kit/starter/src/lib/parks.ts +84 -0
  75. package/site-kit/starter/src/lib/seo.ts +97 -0
  76. package/site-kit/starter/src/pages/404.astro +30 -0
  77. package/site-kit/starter/src/pages/about.astro +39 -0
  78. package/site-kit/starter/src/pages/index.astro +52 -0
  79. package/site-kit/starter/src/pages/parks/[slug].astro +95 -0
  80. package/site-kit/starter/src/pages/regions/[region].astro +58 -0
  81. package/site-kit/starter/src/pages/robots.txt.ts +16 -0
  82. package/site-kit/starter/src/site.ts +29 -0
  83. package/site-kit/starter/src/styles/globals.css +561 -0
  84. package/site-kit/starter/tsconfig.json +5 -0
  85. package/site-kit/starter/wrangler.jsonc +16 -0
  86. package/skills/seo/SKILL.md +19 -1
  87. package/dist/dist-DBI-BioS.js +0 -420
  88. package/dist/dist-DBI-BioS.js.map +0 -1
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-hHlvm1W0.js";
2
+ import { n as version, t as load } from "./esm-BmuxgZyN.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";
@@ -28,7 +28,7 @@ import { execFile } from "node:child_process";
28
28
  import pino from "pino";
29
29
  import pretty from "pino-pretty";
30
30
  import { request } from "node:https";
31
- //#region packages/core/src/analytics/providers.ts
31
+ // packages/core/src/analytics/providers.ts
32
32
  const ANALYTICS_PROVIDER_IDS = ["google", "clicky"];
33
33
  const ANALYTICS_PROVIDER_DETAILS = {
34
34
  google: {
@@ -56,8 +56,7 @@ function analyticsConnectionLabel(connection) {
56
56
  if (connection.provider === "clicky") return `${label} site ${connection.siteId}`;
57
57
  return `${label} account`;
58
58
  }
59
- //#endregion
60
- //#region packages/core/src/rules.ts
59
+ // packages/core/src/rules.ts
61
60
  const RULE_DEFINITIONS = [
62
61
  {
63
62
  id: "missing_title",
@@ -999,8 +998,7 @@ function recommendationForRule(ruleId) {
999
998
  function hasRule(ruleId) {
1000
999
  return RULES_BY_ID.has(ruleId);
1001
1000
  }
1002
- //#endregion
1003
- //#region packages/core/src/analyze/agent-report.ts
1001
+ // packages/core/src/analyze/agent-report.ts
1004
1002
  const ACTION_KEYS = /* @__PURE__ */ new Set([
1005
1003
  "actions",
1006
1004
  "findings",
@@ -1399,8 +1397,7 @@ function agentActionsView(report, options = {}) {
1399
1397
  ...Object.keys(retainedEvidence).length ? { retainedEvidence } : {}
1400
1398
  };
1401
1399
  }
1402
- //#endregion
1403
- //#region packages/core/src/providers/errors.ts
1400
+ // packages/core/src/providers/errors.ts
1404
1401
  const PROVIDER_ERROR_CODES = {
1405
1402
  configuration: "AUTH_CONFIG_REQUIRED",
1406
1403
  authentication: "AUTH_CONFIG_REQUIRED",
@@ -1437,8 +1434,7 @@ var ProviderError = class extends Error {
1437
1434
  };
1438
1435
  }
1439
1436
  };
1440
- //#endregion
1441
- //#region packages/core/src/errors.ts
1437
+ // packages/core/src/errors.ts
1442
1438
  const errorMetadata = {
1443
1439
  ACCESS_DENIED: {
1444
1440
  exitCode: 4,
@@ -1564,8 +1560,7 @@ async function collectGa4AccountSummaries(fetchPage) {
1564
1560
  }
1565
1561
  throw new Error(`Google Analytics account summary discovery exceeded 20 pages.`);
1566
1562
  }
1567
- //#endregion
1568
- //#region packages/core/src/http/read-json.ts
1563
+ // packages/core/src/http/read-json.ts
1569
1564
  const GZIP = [31, 139];
1570
1565
  async function gunzip(bytes) {
1571
1566
  const stream = new Response(bytes).body?.pipeThrough(new DecompressionStream("gzip"));
@@ -1615,8 +1610,7 @@ async function readJson(response, label, options = {}) {
1615
1610
  async function readJsonOrUndefined(response, label) {
1616
1611
  return readJson(response, label).catch(() => void 0);
1617
1612
  }
1618
- //#endregion
1619
- //#region packages/core/src/google/api.ts
1613
+ // packages/core/src/google/api.ts
1620
1614
  var GoogleApiError = class extends SeoError {
1621
1615
  status;
1622
1616
  constructor(code, message, status) {
@@ -1745,8 +1739,7 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1745
1739
  body
1746
1740
  });
1747
1741
  }
1748
- //#endregion
1749
- //#region packages/core/src/bing/api.ts
1742
+ // packages/core/src/bing/api.ts
1750
1743
  const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1751
1744
  const MAX_BYTES$4 = 8e6;
1752
1745
  function bingCall(auth, options) {
@@ -1776,7 +1769,7 @@ function bingCall(auth, options) {
1776
1769
  return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$4 })).d;
1777
1770
  };
1778
1771
  }
1779
- function day$2(value) {
1772
+ function day$3(value) {
1780
1773
  if (typeof value !== "string") return null;
1781
1774
  const legacy = /^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/.exec(value);
1782
1775
  const date = legacy ? new Date(Number(legacy[1])) : new Date(value);
@@ -1797,7 +1790,7 @@ async function bingTraffic(call, site) {
1797
1790
  const rows = [];
1798
1791
  let invalidRows = 0;
1799
1792
  for (const row of list$2(await call("GetRankAndTrafficStats", { siteUrl: site }), "traffic list")) {
1800
- const date = day$2(row.Date);
1793
+ const date = day$3(row.Date);
1801
1794
  const clicks = count$3(row.Clicks);
1802
1795
  const impressions = count$3(row.Impressions);
1803
1796
  if (!date || clicks === null || impressions === null) {
@@ -1820,7 +1813,7 @@ async function keyed(call, method, params) {
1820
1813
  const rows = [];
1821
1814
  let invalidRows = 0;
1822
1815
  for (const row of list$2(await call(method, params), "row list")) {
1823
- const date = day$2(row.Date);
1816
+ const date = day$3(row.Date);
1824
1817
  const key = typeof row.Query === "string" ? row.Query.trim() : "";
1825
1818
  const clicks = count$3(row.Clicks);
1826
1819
  const impressions = count$3(row.Impressions);
@@ -1857,8 +1850,7 @@ function bingSiteFor(sites, target) {
1857
1850
  if (!wanted) return null;
1858
1851
  return sites.filter((site) => host(site.url) === wanted).sort((a, b) => Number(b.verified) - Number(a.verified) || Number(b.url.startsWith("https://")) - Number(a.url.startsWith("https://")) || a.url.localeCompare(b.url, "en"))[0] ?? null;
1859
1852
  }
1860
- //#endregion
1861
- //#region packages/core/src/gsc/dates.ts
1853
+ // packages/core/src/gsc/dates.ts
1862
1854
  const GSC_TIME_ZONE = "America/Los_Angeles";
1863
1855
  const GSC_FINAL_DATA_LAG_DAYS = 4;
1864
1856
  function dateInTimeZone(now, timeZone) {
@@ -1887,8 +1879,7 @@ function finalGscDateRange(days = 28, now = /* @__PURE__ */ new Date()) {
1887
1879
  endDate
1888
1880
  };
1889
1881
  }
1890
- //#endregion
1891
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
1882
+ // packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
1892
1883
  function integerOption$1(input) {
1893
1884
  if (input.value === void 0) return input.fallback;
1894
1885
  if (!Number.isInteger(input.value) || input.value < input.minimum || input.value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
@@ -1914,8 +1905,7 @@ function explicitDateRange(input, maximumDays) {
1914
1905
  days
1915
1906
  };
1916
1907
  }
1917
- //#endregion
1918
- //#region packages/core/src/analyze/site-diagnostics/decay-report-input.ts
1908
+ // packages/core/src/analyze/site-diagnostics/decay-report-input.ts
1919
1909
  function decayNumberOption(input) {
1920
1910
  const value = input.value ?? input.fallback;
1921
1911
  if (!Number.isFinite(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between ${input.minimum} and ${input.maximum}.`);
@@ -1971,8 +1961,7 @@ function decayComparisonRange(current, comparison = "previous-period") {
1971
1961
  endDate
1972
1962
  };
1973
1963
  }
1974
- //#endregion
1975
- //#region packages/core/src/updates/ranking-history.ts
1964
+ // packages/core/src/updates/ranking-history.ts
1976
1965
  const RANKING_HISTORY = [
1977
1966
  {
1978
1967
  id: "XhUDXP7A67iHCD2kmbVu",
@@ -2227,8 +2216,7 @@ const RANKING_HISTORY = [
2227
2216
  external_desc: "November 2021 spam update"
2228
2217
  }
2229
2218
  ];
2230
- //#endregion
2231
- //#region packages/core/src/updates/search-updates.ts
2219
+ // packages/core/src/updates/search-updates.ts
2232
2220
  function classifyUpdate(name, product) {
2233
2221
  const lower = `${name} ${product}`.toLowerCase();
2234
2222
  if (lower.includes("core")) return "core";
@@ -2277,8 +2265,7 @@ function findOverlappingSearchUpdates(input) {
2277
2265
  return updateStart <= windowEnd && updateEnd >= windowStart;
2278
2266
  });
2279
2267
  }
2280
- //#endregion
2281
- //#region packages/core/src/commands/types.ts
2268
+ // packages/core/src/commands/types.ts
2282
2269
  var CommandError = class extends Error {
2283
2270
  code;
2284
2271
  constructor(code, message) {
@@ -2286,8 +2273,7 @@ var CommandError = class extends Error {
2286
2273
  this.code = code;
2287
2274
  }
2288
2275
  };
2289
- //#endregion
2290
- //#region packages/core/src/commands/shared.ts
2276
+ // packages/core/src/commands/shared.ts
2291
2277
  const FACTS_ONLY = "These are facts from Search Console, not instructions.";
2292
2278
  const ADVICE_KEYS = /^(recommendations?|actions?|advice|suggested[A-Z]\w*|nextSteps?)$/;
2293
2279
  function factsOnly(value) {
@@ -2423,8 +2409,7 @@ function matchProperty(names, site) {
2423
2409
  }
2424
2410
  }).filter((entry) => entry !== null && schemes.includes(entry.url.protocol) && bare(entry.url.hostname) === host && `${url.pathname}/`.startsWith(entry.url.pathname)).sort((a, b) => b.url.pathname.length - a.url.pathname.length || schemes.indexOf(a.url.protocol) - schemes.indexOf(b.url.protocol) || Number(b.url.hostname === url.hostname) - Number(a.url.hostname === url.hostname) || a.name.localeCompare(b.name))[0]?.name ?? null;
2425
2411
  }
2426
- //#endregion
2427
- //#region packages/core/src/commands/present.ts
2412
+ // packages/core/src/commands/present.ts
2428
2413
  const LIST_KEYS = [
2429
2414
  "items",
2430
2415
  "rows",
@@ -2493,7 +2478,7 @@ const METRIC_COLUMNS = [
2493
2478
  "score",
2494
2479
  "priority.score"
2495
2480
  ];
2496
- const SUMMARY_ROWS$1 = 10;
2481
+ const SUMMARY_ROWS$2 = 10;
2497
2482
  const CELL_MAX = 80;
2498
2483
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2499
2484
  const isPrimitive = (value) => value === null || [
@@ -2652,7 +2637,7 @@ function presentReport(input) {
2652
2637
  }
2653
2638
  };
2654
2639
  }
2655
- const shown = flat.slice(0, Math.min(input.limit, SUMMARY_ROWS$1));
2640
+ const shown = flat.slice(0, Math.min(input.limit, SUMMARY_ROWS$2));
2656
2641
  const columns = columnsFor(flat, 8);
2657
2642
  return {
2658
2643
  text: [
@@ -2687,8 +2672,7 @@ function preview(report, origin, rows) {
2687
2672
  text: table$1(flat.slice(0, rows), columnsFor(flat, 6), origin)
2688
2673
  };
2689
2674
  }
2690
- //#endregion
2691
- //#region packages/core/src/commands/sites.ts
2675
+ // packages/core/src/commands/sites.ts
2692
2676
  async function readableSites(google, signIn) {
2693
2677
  return (await listSearchConsoleSites(google, { signIn })).filter((entry) => entry.permissionLevel !== "siteUnverifiedUser").map((entry) => ({
2694
2678
  siteUrl: entry.siteUrl,
@@ -2762,8 +2746,7 @@ async function runSites(input) {
2762
2746
  }
2763
2747
  };
2764
2748
  }
2765
- //#endregion
2766
- //#region packages/core/src/commands/linked.ts
2749
+ // packages/core/src/commands/linked.ts
2767
2750
  const MAX_ROWS$2 = 1e5;
2768
2751
  const PERMISSION_RANK = {
2769
2752
  siteOwner: 0,
@@ -2866,8 +2849,7 @@ var LinkedAccounts = class {
2866
2849
  });
2867
2850
  }
2868
2851
  };
2869
- //#endregion
2870
- //#region packages/core/src/paths.ts
2852
+ // packages/core/src/paths.ts
2871
2853
  let memoizedPaths;
2872
2854
  function defaultConfigDir(input) {
2873
2855
  if (input.platform === "win32") return join(input.env.APPDATA ?? join(input.home, "AppData", "Roaming"), "seo", "Config");
@@ -2962,8 +2944,7 @@ function fileSize(path) {
2962
2944
  return 0;
2963
2945
  }
2964
2946
  }
2965
- //#endregion
2966
- //#region packages/core/src/storage/files.ts
2947
+ // packages/core/src/storage/files.ts
2967
2948
  function ensureParentDir(path, mode = 448) {
2968
2949
  mkdirSync(dirname(path), {
2969
2950
  recursive: true,
@@ -3001,8 +2982,7 @@ function safeRemove(path) {
3001
2982
  force: true
3002
2983
  });
3003
2984
  }
3004
- //#endregion
3005
- //#region packages/core/src/telemetry-failures.ts
2985
+ // packages/core/src/telemetry-failures.ts
3006
2986
  const TELEMETRY_ERROR_CATEGORIES = [
3007
2987
  "auth",
3008
2988
  "crawl_timeout",
@@ -3277,8 +3257,7 @@ function classifyTelemetryFailure(error) {
3277
3257
  function isTelemetryOperation(value) {
3278
3258
  return typeof value === "string" && TELEMETRY_OPERATIONS.includes(value);
3279
3259
  }
3280
- //#endregion
3281
- //#region packages/core/src/telemetry-shared.ts
3260
+ // packages/core/src/telemetry-shared.ts
3282
3261
  const TELEMETRY_ENDPOINT = "https://seoskill.dev/api/t";
3283
3262
  const TELEMETRY_SCHEMA_VERSION = 2;
3284
3263
  function isoWeek(date) {
@@ -3297,11 +3276,9 @@ function agentFromClientName(name) {
3297
3276
  if (value.includes("codex") || value.includes("openai")) return "codex";
3298
3277
  return "unknown";
3299
3278
  }
3300
- //#endregion
3301
- //#region packages/core/src/version.ts
3279
+ // packages/core/src/version.ts
3302
3280
  const SEO_VERSION = version;
3303
- //#endregion
3304
- //#region packages/core/src/telemetry.ts
3281
+ // packages/core/src/telemetry.ts
3305
3282
  const TELEMETRY_TIMEOUT_MS = 2e3;
3306
3283
  const TELEMETRY_EVENTS = [
3307
3284
  "first_run",
@@ -3665,8 +3642,7 @@ function telemetryFailureInput(error) {
3665
3642
  };
3666
3643
  return classifyTelemetryFailure(error);
3667
3644
  }
3668
- //#endregion
3669
- //#region packages/core/src/cloud.ts
3645
+ // packages/core/src/cloud.ts
3670
3646
  const CLOUD_URL = "https://seoskill.dev";
3671
3647
  const PRIVATE = 384;
3672
3648
  const EARLY = 6e4;
@@ -3822,8 +3798,7 @@ async function cloudLogout() {
3822
3798
  async function openCloudLink(url) {
3823
3799
  await open(url).catch(() => void 0);
3824
3800
  }
3825
- //#endregion
3826
- //#region packages/core/src/types/auth.ts
3801
+ // packages/core/src/types/auth.ts
3827
3802
  const tokenSchema = z.object({
3828
3803
  provider: z.literal("google"),
3829
3804
  account_email: z.string().email().max(320),
@@ -3852,8 +3827,7 @@ const tokenStoreSchema = z.object({
3852
3827
  message: "The active Google account must exist in accounts."
3853
3828
  });
3854
3829
  });
3855
- //#endregion
3856
- //#region packages/core/src/providers/contracts.ts
3830
+ // packages/core/src/providers/contracts.ts
3857
3831
  const providerIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
3858
3832
  const providerCapabilitySchema = z.enum([
3859
3833
  "keyword-metrics",
@@ -3871,8 +3845,7 @@ const providerCapabilitySchema = z.enum([
3871
3845
  "ai-mentions",
3872
3846
  "ai-prompt-observation"
3873
3847
  ]);
3874
- //#endregion
3875
- //#region packages/core/src/types/config.ts
3848
+ // packages/core/src/types/config.ts
3876
3849
  const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
3877
3850
  const analyticsProviderSchema = z.enum(["google", "clicky"]);
3878
3851
  const analyticsExtensionIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
@@ -3925,8 +3898,7 @@ async function setKeyringPassword(service, account, password) {
3925
3898
  async function deleteKeyringPassword(service, account) {
3926
3899
  return (await resolveKeyring()).deletePassword(service, account);
3927
3900
  }
3928
- //#endregion
3929
- //#region packages/core/src/storage/config.ts
3901
+ // packages/core/src/storage/config.ts
3930
3902
  const KEYRING_SERVICE$1 = "seo";
3931
3903
  const PRIVATE_FILE_MODE$2 = 384;
3932
3904
  function tightenConfigPermissions(path) {
@@ -4208,8 +4180,7 @@ function getPrivacySnapshot() {
4208
4180
  }
4209
4181
  ];
4210
4182
  }
4211
- //#endregion
4212
- //#region packages/core/src/storage/lock.ts
4183
+ // packages/core/src/storage/lock.ts
4213
4184
  async function withFileLock(path, task) {
4214
4185
  ensureParentDir(path);
4215
4186
  const fastLocks = process.env.SEO_LOCK_FAST === "1";
@@ -4238,14 +4209,12 @@ async function withFileLock(path, task) {
4238
4209
  await release();
4239
4210
  }
4240
4211
  }
4241
- //#endregion
4242
- //#region packages/core/src/gsc/shared-client.generated.ts
4212
+ // packages/core/src/gsc/shared-client.generated.ts
4243
4213
  const SHARED_OAUTH_CLIENT = {
4244
4214
  clientId: "548418788053-q7p964rqvjiv8ooamciprku6bpu34u9d.apps.googleusercontent.com",
4245
4215
  clientSecret: "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"
4246
4216
  };
4247
- //#endregion
4248
- //#region packages/core/src/gsc/auth/client-config.ts
4217
+ // packages/core/src/gsc/auth/client-config.ts
4249
4218
  function getAuthModeStatus() {
4250
4219
  const byo = readOauthClient();
4251
4220
  return {
@@ -4277,8 +4246,7 @@ function getClientConfig(source) {
4277
4246
  function missingOAuthClientMessage(source) {
4278
4247
  return source === "shared" ? "The stored Google login uses the shared seo app, but this build is missing that OAuth client. Report this package issue at https://github.com/iannuttall/seo/issues, or use `seo auth setup-client` with your own desktop OAuth client." : "The stored Google login uses a BYO OAuth client that is no longer configured. Restore the same client with `seo auth setup-client`, or run `seo auth logout` before signing in with a different client.";
4279
4248
  }
4280
- //#endregion
4281
- //#region packages/core/src/gsc/auth/types.ts
4249
+ // packages/core/src/gsc/auth/types.ts
4282
4250
  const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
4283
4251
  const GOOGLE_READONLY_SCOPE_LABELS = {
4284
4252
  "https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
@@ -4293,8 +4261,7 @@ const GOOGLE_SCOPE = [
4293
4261
  "openid",
4294
4262
  "email"
4295
4263
  ].join(" ");
4296
- //#endregion
4297
- //#region packages/core/src/gsc/auth/service-account.ts
4264
+ // packages/core/src/gsc/auth/service-account.ts
4298
4265
  const GOOGLE_TOKEN_ENDPOINT$1 = "https://oauth2.googleapis.com/token";
4299
4266
  const TOKEN_BUFFER_MS = 6e4;
4300
4267
  function authConfigError(message) {
@@ -4440,8 +4407,7 @@ var ServiceAccountAccessTokenClient = class {
4440
4407
  return this.token.value;
4441
4408
  }
4442
4409
  };
4443
- //#endregion
4444
- //#region packages/core/src/gsc/auth/token-endpoint.ts
4410
+ // packages/core/src/gsc/auth/token-endpoint.ts
4445
4411
  const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
4446
4412
  var GoogleTokenEndpointError = class extends Error {
4447
4413
  status;
@@ -4495,8 +4461,7 @@ async function exchangeGoogleCode(input, fetchImpl = fetch) {
4495
4461
  }
4496
4462
  return readJson(response, "Google sign-in");
4497
4463
  }
4498
- //#endregion
4499
- //#region packages/core/src/gsc/auth/authorized-client.ts
4464
+ // packages/core/src/gsc/auth/authorized-client.ts
4500
4465
  const REFRESH_BUFFER_MS = 6e4;
4501
4466
  function authRequired(accountEmail) {
4502
4467
  return new SeoError("AUTH_REQUIRED", accountEmail ? `Google account ${accountEmail} is not connected. Run \`seo auth login\` first.` : notSignedIn());
@@ -4648,8 +4613,7 @@ async function authStatus() {
4648
4613
  serviceAccount
4649
4614
  };
4650
4615
  }
4651
- //#endregion
4652
- //#region packages/core/src/gsc/auth/callback-page.ts
4616
+ // packages/core/src/gsc/auth/callback-page.ts
4653
4617
  function callbackCopy(options) {
4654
4618
  if (options.status === "connected") return {
4655
4619
  title: "Google account connected",
@@ -4688,8 +4652,7 @@ function oauthCallbackPage(options = { status: "connected" }) {
4688
4652
  </body>
4689
4653
  </html>`;
4690
4654
  }
4691
- //#endregion
4692
- //#region packages/core/src/gsc/auth/loopback-callback.ts
4655
+ // packages/core/src/gsc/auth/loopback-callback.ts
4693
4656
  const OAUTH_CALLBACK_TIMEOUT_MS = 3e5;
4694
4657
  function timeoutError() {
4695
4658
  return new SeoError("AUTH_REQUIRED", "Google sign-in timed out. No account was connected. Run `seo auth login` again and finish the browser steps within five minutes. Keep the terminal open while you sign in.");
@@ -4745,8 +4708,7 @@ function waitForCode(input) {
4745
4708
  });
4746
4709
  });
4747
4710
  }
4748
- //#endregion
4749
- //#region packages/core/src/gsc/auth/user-email.ts
4711
+ // packages/core/src/gsc/auth/user-email.ts
4750
4712
  function decodeJwtEmail(idToken) {
4751
4713
  if (!idToken) return;
4752
4714
  const [, payload] = idToken.split(".");
@@ -4762,8 +4724,7 @@ async function fetchUserEmail(accessToken) {
4762
4724
  if (!response.ok) return;
4763
4725
  return (await readJson(response, "Google user info")).email;
4764
4726
  }
4765
- //#endregion
4766
- //#region packages/core/src/gsc/auth/loopback.ts
4727
+ // packages/core/src/gsc/auth/loopback.ts
4767
4728
  function missingSharedClientLoginMessage() {
4768
4729
  return "This build does not have the shared seo Google app configured. If this is an installed public package, report it at https://github.com/iannuttall/seo/issues. For local development, run `seo auth setup-client` or set SEO_GOOGLE_CLIENT_ID / SEO_GOOGLE_CLIENT_SECRET. Legacy GSC_CLIENT_ID / GSC_CLIENT_SECRET also work.";
4769
4730
  }
@@ -4838,8 +4799,7 @@ async function loginWithLoopback(clientConfig = getClientConfig()) {
4838
4799
  server.close();
4839
4800
  }
4840
4801
  }
4841
- //#endregion
4842
- //#region packages/core/src/gsc/client/fetch.ts
4802
+ // packages/core/src/gsc/client/fetch.ts
4843
4803
  async function authedFetch(client, url, init) {
4844
4804
  const accessToken = await client.getAccessToken();
4845
4805
  if (!accessToken) throw new SeoError("AUTH_EXPIRED", "Could not obtain a Google access token. Run `seo auth login` again.");
@@ -4860,8 +4820,7 @@ function googleFetchFor(client) {
4860
4820
  return (url, init) => authedFetch(client, url, init);
4861
4821
  }
4862
4822
  const LOCAL_SIGN_IN = { signIn: "Run `seo auth login` again." };
4863
- //#endregion
4864
- //#region packages/core/src/storage/cache-maintenance.ts
4823
+ // packages/core/src/storage/cache-maintenance.ts
4865
4824
  const MEBIBYTE$2 = 1048576;
4866
4825
  const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
4867
4826
  const CACHE_MAX_AGE_MS = 2592e6;
@@ -5038,8 +4997,7 @@ function runCacheMaintenance(database, options = {}) {
5038
4997
  compaction
5039
4998
  };
5040
4999
  }
5041
- //#endregion
5042
- //#region packages/core/src/storage/sqlite.ts
5000
+ // packages/core/src/storage/sqlite.ts
5043
5001
  function withoutDriverMetadata(row) {
5044
5002
  if (!row || typeof row !== "object" || Array.isArray(row)) return row;
5045
5003
  const { _metadata: _driverMetadata, ...data } = row;
@@ -5084,8 +5042,7 @@ var SqliteDatabase = class {
5084
5042
  return this;
5085
5043
  }
5086
5044
  };
5087
- //#endregion
5088
- //#region packages/core/src/storage/database.ts
5045
+ // packages/core/src/storage/database.ts
5089
5046
  const PRIVATE_DATABASE_FILE_MODE = 384;
5090
5047
  const DATABASE_FILE_SUFFIXES = [
5091
5048
  "",
@@ -5368,8 +5325,7 @@ function clearCache(provider, olderThanMs) {
5368
5325
  compactCacheDatabase(database, { allowFullVacuum: true });
5369
5326
  return removed;
5370
5327
  }
5371
- //#endregion
5372
- //#region packages/core/src/ga4/client.ts
5328
+ // packages/core/src/ga4/client.ts
5373
5329
  function ga4RequestCanUseCache(body) {
5374
5330
  return body.dateRanges.every((range) => /^\d{4}-\d{2}-\d{2}$/.test(range.startDate) && /^\d{4}-\d{2}-\d{2}$/.test(range.endDate));
5375
5331
  }
@@ -5450,8 +5406,7 @@ function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
5450
5406
  })) warnings.push(`${label} was sampled by Google Analytics.`);
5451
5407
  return warnings;
5452
5408
  }
5453
- //#endregion
5454
- //#region packages/core/src/analyze/ai-referrals-sources.ts
5409
+ // packages/core/src/analyze/ai-referrals-sources.ts
5455
5410
  const AI_REFERRAL_SOURCES_VERSION = "ai-referral-sources@1";
5456
5411
  const AI_REFERRAL_SOURCES = [
5457
5412
  {
@@ -5537,8 +5492,7 @@ function aiReferralSourceForValue(value) {
5537
5492
  if (!hostname) return void 0;
5538
5493
  return AI_REFERRAL_SOURCES.find((source) => source.domains.some((domain) => matchesDomain(hostname, domain)));
5539
5494
  }
5540
- //#endregion
5541
- //#region packages/core/src/analyze/ai-referrals-analysis.ts
5495
+ // packages/core/src/analyze/ai-referrals-analysis.ts
5542
5496
  function compareText$19(left, right) {
5543
5497
  return left < right ? -1 : left > right ? 1 : 0;
5544
5498
  }
@@ -5710,13 +5664,12 @@ function analyzeAiReferralRows(input) {
5710
5664
  ]
5711
5665
  };
5712
5666
  }
5713
- //#endregion
5714
- //#region packages/core/src/analyze/ai-referrals-query.ts
5715
- const PAGE_SIZE$3 = 25e3;
5667
+ // packages/core/src/analyze/ai-referrals-query.ts
5668
+ const PAGE_SIZE$4 = 25e3;
5716
5669
  async function fetchAiReferralRows(input) {
5717
- const pageSize = input.pageSize ?? PAGE_SIZE$3;
5670
+ const pageSize = input.pageSize ?? PAGE_SIZE$4;
5718
5671
  if (!Number.isInteger(input.maxRows) || input.maxRows < 1) throw new Error("maxRows must be a positive whole number.");
5719
- if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > PAGE_SIZE$3) throw new Error(`pageSize must be a whole number between 1 and ${PAGE_SIZE$3}.`);
5672
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > PAGE_SIZE$4) throw new Error(`pageSize must be a whole number between 1 and ${PAGE_SIZE$4}.`);
5720
5673
  const rows = [];
5721
5674
  const warnings = /* @__PURE__ */ new Set();
5722
5675
  const retainedKeys = /* @__PURE__ */ new Set();
@@ -5786,8 +5739,7 @@ function sessionSourceInListFilter(values) {
5786
5739
  }
5787
5740
  } };
5788
5741
  }
5789
- //#endregion
5790
- //#region packages/core/src/analyze/ai-referrals.ts
5742
+ // packages/core/src/analyze/ai-referrals.ts
5791
5743
  const DEFAULT_MAX_ROWS$1 = 1e5;
5792
5744
  const MAX_ROWS$1 = 1e5;
5793
5745
  const DEFAULT_RESULT_LIMIT = 25;
@@ -5964,8 +5916,7 @@ async function aiReferralsReport(input, dependencies = {}) {
5964
5916
  totalUsersEvidence
5965
5917
  });
5966
5918
  }
5967
- //#endregion
5968
- //#region packages/core/src/robots-directives.ts
5919
+ // packages/core/src/robots-directives.ts
5969
5920
  function normalizedDirective(value) {
5970
5921
  const directive = value.trim().toLowerCase().split(/\s+/)[0];
5971
5922
  if (directive === "none") return ["noindex", "nofollow"];
@@ -6224,8 +6175,7 @@ function resolveStreamedSegments($) {
6224
6175
  function hasNextAppRouterPayload($) {
6225
6176
  return $("script:not([src])").toArray().some((script) => $(script).text().includes("self.__next_f"));
6226
6177
  }
6227
- //#endregion
6228
- //#region packages/core/src/extract/canonical.ts
6178
+ // packages/core/src/extract/canonical.ts
6229
6179
  function headerValue$4(headers, name) {
6230
6180
  const lower = name.toLowerCase();
6231
6181
  return Object.entries(headers).find(([key]) => key.toLowerCase() === lower)?.[1];
@@ -6374,11 +6324,10 @@ function extractHttpCanonicalEvidence(headers, baseUrl) {
6374
6324
  function extractCanonicalEvidence($, headers, baseUrl) {
6375
6325
  return evidenceFromCandidates([...htmlCandidates($, baseUrl), ...headerCandidates(headers, baseUrl)]);
6376
6326
  }
6377
- //#endregion
6378
- //#region packages/core/src/extract/content-sketch.ts
6327
+ // packages/core/src/extract/content-sketch.ts
6379
6328
  const SHINGLE_SIZE = 5;
6380
6329
  const MAX_HASHES = 32;
6381
- function words$2(value) {
6330
+ function words$3(value) {
6382
6331
  return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
6383
6332
  }
6384
6333
  function sampleIndexes(total, limit) {
@@ -6389,7 +6338,7 @@ function digest(value) {
6389
6338
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
6390
6339
  }
6391
6340
  function contentSketch(value) {
6392
- const tokens = words$2(value);
6341
+ const tokens = words$3(value);
6393
6342
  const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
6394
6343
  return {
6395
6344
  version: 1,
@@ -6399,8 +6348,7 @@ function contentSketch(value) {
6399
6348
  hashes
6400
6349
  };
6401
6350
  }
6402
- //#endregion
6403
- //#region packages/core/src/extract/main-content.ts
6351
+ // packages/core/src/extract/main-content.ts
6404
6352
  function parseDocument$1(html) {
6405
6353
  const { document } = parseHTML(html);
6406
6354
  if (document.documentElement?.localName === "html") return document;
@@ -6526,8 +6474,7 @@ async function extractMainMarkdown(fetchResult) {
6526
6474
  truncated: true
6527
6475
  };
6528
6476
  }
6529
- //#endregion
6530
- //#region packages/core/src/extract/google-rich-results.ts
6477
+ // packages/core/src/extract/google-rich-results.ts
6531
6478
  const SUPPORTED_TYPES = /* @__PURE__ */ new Set([
6532
6479
  "Article",
6533
6480
  "BlogPosting",
@@ -6804,8 +6751,7 @@ function selectGoogleRichResultAssessments(assessments, limit = 50) {
6804
6751
  }
6805
6752
  };
6806
6753
  }
6807
- //#endregion
6808
- //#region packages/core/src/extract/structured-data.ts
6754
+ // packages/core/src/extract/structured-data.ts
6809
6755
  function schemaTypeFromIri(value) {
6810
6756
  try {
6811
6757
  const iri = new URL(value);
@@ -7042,8 +6988,7 @@ function extractStructuredData($, baseUrl, options = {}) {
7042
6988
  hasDate: parsed.hasDate || $("[itemprop~=\"datePublished\"], [itemprop~=\"dateModified\"], [property~=\"datePublished\"], [property~=\"dateModified\"]").toArray().some((element) => isValidStructuredDate($(element).attr("content") ?? $(element).attr("datetime") ?? $(element).text()))
7043
6989
  };
7044
6990
  }
7045
- //#endregion
7046
- //#region packages/core/src/extract/page-extractor.ts
6991
+ // packages/core/src/extract/page-extractor.ts
7047
6992
  function metaRefresh($, finalUrl) {
7048
6993
  for (const element of $("meta[http-equiv]").toArray()) {
7049
6994
  if ($(element).attr("http-equiv")?.trim().toLowerCase() !== "refresh") continue;
@@ -7461,8 +7406,7 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
7461
7406
  ]
7462
7407
  };
7463
7408
  }
7464
- //#endregion
7465
- //#region packages/core/src/fetch/crawler-identity.ts
7409
+ // packages/core/src/fetch/crawler-identity.ts
7466
7410
  const SEO_CRAWLER_TOKEN = "SEO-Skill";
7467
7411
  const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
7468
7412
  const SEO_CRAWLER_IDENTITY = {
@@ -7472,8 +7416,7 @@ const SEO_CRAWLER_IDENTITY = {
7472
7416
  userAgent: SEO_CRAWLER_USER_AGENT,
7473
7417
  documentationUrl: "https://seoskill.dev/docs/crawler/"
7474
7418
  };
7475
- //#endregion
7476
- //#region packages/core/src/fetch/access-block.ts
7419
+ // packages/core/src/fetch/access-block.ts
7477
7420
  const CLOUDFLARE_SKIP_DOCS = "https://developers.cloudflare.com/waf/custom-rules/skip/";
7478
7421
  function isHeadersLike(headers) {
7479
7422
  return "get" in headers && typeof headers.get === "function";
@@ -7543,8 +7486,7 @@ function detectAccessBlock(input) {
7543
7486
  }
7544
7487
  };
7545
7488
  }
7546
- //#endregion
7547
- //#region packages/core/src/fetch/http-client.ts
7489
+ // packages/core/src/fetch/http-client.ts
7548
7490
  var ResponseSizeLimitError = class extends Error {
7549
7491
  maxBytes;
7550
7492
  constructor(maxBytes, label = "Response") {
@@ -7611,8 +7553,7 @@ async function readBoundedResponseText(response, maxBytes, label = "Response") {
7611
7553
  }
7612
7554
  return Buffer.concat(chunks, total).toString("utf8");
7613
7555
  }
7614
- //#endregion
7615
- //#region packages/core/src/fetch/page-fetcher/rate-controls.ts
7556
+ // packages/core/src/fetch/page-fetcher/rate-controls.ts
7616
7557
  const HOST_QUEUES = /* @__PURE__ */ new Map();
7617
7558
  const HOST_HEALTH = /* @__PURE__ */ new Map();
7618
7559
  var OriginBackpressureError = class extends Error {
@@ -7757,8 +7698,7 @@ function recordHostFetch(input) {
7757
7698
  }
7758
7699
  return snapshotForHost(input.host);
7759
7700
  }
7760
- //#endregion
7761
- //#region packages/core/src/fetch/page-fetcher/redirects.ts
7701
+ // packages/core/src/fetch/page-fetcher/redirects.ts
7762
7702
  async function fetchWithRedirectChain(url, signal) {
7763
7703
  const redirectChain = [];
7764
7704
  let currentUrl = url;
@@ -7783,8 +7723,7 @@ async function fetchWithRedirectChain(url, signal) {
7783
7723
  }
7784
7724
  throw new Error(`Too many redirects for ${url}`);
7785
7725
  }
7786
- //#endregion
7787
- //#region packages/core/src/fetch/page-fetcher/robots.ts
7726
+ // packages/core/src/fetch/page-fetcher/robots.ts
7788
7727
  const MAX_ROBOTS_RESPONSE_BYTES = 1048576;
7789
7728
  var RobotsAccessError = class extends Error {
7790
7729
  reason;
@@ -7935,8 +7874,7 @@ function createRobotsSession(input = {}) {
7935
7874
  }
7936
7875
  };
7937
7876
  }
7938
- //#endregion
7939
- //#region packages/core/src/fetch/page-fetcher/robots-evidence.ts
7877
+ // packages/core/src/fetch/page-fetcher/robots-evidence.ts
7940
7878
  function pageRobotsEvidence(robots) {
7941
7879
  return {
7942
7880
  url: robots.url,
@@ -7957,8 +7895,7 @@ function diagnosticRobotsEvidence(robots) {
7957
7895
  error: robots.error
7958
7896
  };
7959
7897
  }
7960
- //#endregion
7961
- //#region packages/core/src/fetch/page-fetcher/plain.ts
7898
+ // packages/core/src/fetch/page-fetcher/plain.ts
7962
7899
  const MAX_PAGE_RESPONSE_BYTES = 5242880;
7963
7900
  function encodePageFetchCacheEvidence(result) {
7964
7901
  return JSON.stringify({
@@ -8100,8 +8037,7 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
8100
8037
  });
8101
8038
  return result;
8102
8039
  }
8103
- //#endregion
8104
- //#region packages/core/src/fetch/page-fetcher/browser-path.ts
8040
+ // packages/core/src/fetch/page-fetcher/browser-path.ts
8105
8041
  const PLAYWRIGHT_CORE_VERSION = "1.61.1";
8106
8042
  function systemBrowserCandidates(input) {
8107
8043
  const macApplications = ["/Applications", join(input.homeDirectory, "Applications")];
@@ -8199,8 +8135,7 @@ function browserUnavailableMessage(resolution) {
8199
8135
  "Set SEO_BROWSER_EXECUTABLE_PATH to use a managed browser executable."
8200
8136
  ].join(" ");
8201
8137
  }
8202
- //#endregion
8203
- //#region packages/core/src/fetch/page-fetcher/rendered.ts
8138
+ // packages/core/src/fetch/page-fetcher/rendered.ts
8204
8139
  const DEFAULT_RENDER_TIMEOUT_MS = 2e4;
8205
8140
  const NETWORK_IDLE_TIMEOUT_MS = 3e3;
8206
8141
  const MAX_RENDER_CONTEXTS = 4;
@@ -8430,8 +8365,7 @@ function looksLikeSpa(html) {
8430
8365
  const lower = html.toLowerCase();
8431
8366
  return /<div[^>]+id="root"[^>]*>\s*<\/div>/.test(lower) || /<div[^>]+id="__next"[^>]*>\s*<\/div>/.test(lower) || /<app-root[^>]*>\s*<\/app-root>/.test(lower) || /enable javascript/i.test(lower);
8432
8367
  }
8433
- //#endregion
8434
- //#region packages/core/src/fetch/page-fetcher/rendering-difference.ts
8368
+ // packages/core/src/fetch/page-fetcher/rendering-difference.ts
8435
8369
  const MAX_HEADINGS = 50;
8436
8370
  const MAX_TEXT_VALUE_LENGTH = 500;
8437
8371
  function safeText(value) {
@@ -8536,8 +8470,7 @@ function renderingDocumentDifference(raw, rendered) {
8536
8470
  ].filter((field) => JSON.stringify(rawDocument[field]) !== JSON.stringify(renderedDocument[field]))
8537
8471
  };
8538
8472
  }
8539
- //#endregion
8540
- //#region packages/core/src/fetch/page-fetcher/status-probe.ts
8473
+ // packages/core/src/fetch/page-fetcher/status-probe.ts
8541
8474
  async function statusProbe(url, opts) {
8542
8475
  const startedAt = Date.now();
8543
8476
  const host = new URL(url).host;
@@ -8615,8 +8548,7 @@ async function fetchPageStatus(url, opts = {}) {
8615
8548
  const rate = normalizeRateControls(opts.rate);
8616
8549
  return await queueForHost(new URL(url).host, rate).add(() => statusProbe(url, opts));
8617
8550
  }
8618
- //#endregion
8619
- //#region packages/core/src/fetch/page-fetcher.ts
8551
+ // packages/core/src/fetch/page-fetcher.ts
8620
8552
  function normalizeJavaScriptRenderingMode(input) {
8621
8553
  if (input === true) return "on";
8622
8554
  if (input === false) return "off";
@@ -8693,8 +8625,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
8693
8625
  }
8694
8626
  });
8695
8627
  }
8696
- //#endregion
8697
- //#region packages/core/src/gsc/property-url.ts
8628
+ // packages/core/src/gsc/property-url.ts
8698
8629
  function invalid(message) {
8699
8630
  throw new SeoError("INVALID_INPUT", message);
8700
8631
  }
@@ -8887,8 +8818,7 @@ function finalizeUrlInspectionQuota(input) {
8887
8818
  WHERE credential_key = ? AND property_site_url = ? AND quota_date = ?`).run(dailyExhausted ? 1 : 0, consumed, row.count, input.outcome === "released" ? row.count : 0, exhausted ? 1 : 0, (input.blockedUntil ?? new Date(input.reservation.resetAt)).getTime(), exhausted ? 1 : 0, now.getTime(), now.getTime(), input.reservation.credentialKey, input.reservation.property, input.reservation.quotaDate);
8888
8819
  }).immediate();
8889
8820
  }
8890
- //#endregion
8891
- //#region packages/core/src/gsc/client/inspection.ts
8821
+ // packages/core/src/gsc/client/inspection.ts
8892
8822
  function credentialKey(input) {
8893
8823
  return createHash("sha256").update(`${input.clientId}\n${input.accountEmail.toLowerCase()}`).digest("base64url");
8894
8824
  }
@@ -8973,8 +8903,7 @@ async function inspectUrl(input) {
8973
8903
  if (!response.ok) throw inspectionResponseError(response, input);
8974
8904
  return await readJson(response, "URL Inspection");
8975
8905
  }
8976
- //#endregion
8977
- //#region packages/core/src/gsc/client/search-analytics.ts
8906
+ // packages/core/src/gsc/client/search-analytics.ts
8978
8907
  function searchDateRange(days) {
8979
8908
  return finalGscDateRange(days);
8980
8909
  }
@@ -9140,8 +9069,7 @@ async function queryPagesTopQueriesBatch(site, pageUrls, range = 28) {
9140
9069
  retainedRowLimitReached: result.rowsFetched >= retainedRowLimit
9141
9070
  };
9142
9071
  }
9143
- //#endregion
9144
- //#region packages/core/src/gsc/client/sites.ts
9072
+ // packages/core/src/gsc/client/sites.ts
9145
9073
  async function listSites(_refresh = false, accountEmail) {
9146
9074
  const entries = !accountEmail && cloudLinked() ? await allCloudSites() : await listSearchConsoleSites(googleFetchFor((await getAuthorized(accountEmail)).client), LOCAL_SIGN_IN);
9147
9075
  writeToCache((database) => {
@@ -9160,8 +9088,7 @@ async function allCloudSites() {
9160
9088
  }
9161
9089
  return [...best.values()];
9162
9090
  }
9163
- //#endregion
9164
- //#region packages/core/src/analyze/page-technical-signals.ts
9091
+ // packages/core/src/analyze/page-technical-signals.ts
9165
9092
  function normalizePageUrl(value) {
9166
9093
  try {
9167
9094
  const parsed = new URL(value);
@@ -9194,8 +9121,7 @@ function pageTechnicalSignals(input) {
9194
9121
  if (input.page?.canonical && !samePageUrl(input.page.finalUrl, absoluteUrl(input.page.canonical, input.page.finalUrl))) signals.push("canonical-mismatch");
9195
9122
  return signals;
9196
9123
  }
9197
- //#endregion
9198
- //#region packages/core/src/analyze/title-width.ts
9124
+ // packages/core/src/analyze/title-width.ts
9199
9125
  const SERP_TITLE_REFERENCE_WIDTH_PX = 580;
9200
9126
  const SERP_TITLE_WIDTH_PROFILE = {
9201
9127
  id: "arial-20-v1",
@@ -9326,8 +9252,7 @@ function estimateSerpTitleWidth(title) {
9326
9252
  fallbackGraphemes: fallbackGraphemes.slice(0, 10)
9327
9253
  };
9328
9254
  }
9329
- //#endregion
9330
- //#region packages/core/src/analyze/audit-page.ts
9255
+ // packages/core/src/analyze/audit-page.ts
9331
9256
  const AUDIT_PAGE_RULE_IDS = [
9332
9257
  "missing_title",
9333
9258
  "title_too_wide",
@@ -9487,8 +9412,7 @@ async function auditPage(input, dependencies = defaultDependencies$12) {
9487
9412
  report.recommendations = buildRecommendations(report);
9488
9413
  return report;
9489
9414
  }
9490
- //#endregion
9491
- //#region packages/core/src/analyze/content-coverage-analysis.ts
9415
+ // packages/core/src/analyze/content-coverage-analysis.ts
9492
9416
  const STOPWORDS = /* @__PURE__ */ new Set([
9493
9417
  "a",
9494
9418
  "an",
@@ -9714,8 +9638,7 @@ async function verifyQueryContentWith(input) {
9714
9638
  };
9715
9639
  }
9716
9640
  }
9717
- //#endregion
9718
- //#region packages/core/src/analyze/content-coverage.ts
9641
+ // packages/core/src/analyze/content-coverage.ts
9719
9642
  function verifyQueryContent(input) {
9720
9643
  return verifyQueryContentWith({
9721
9644
  ...input,
@@ -9723,8 +9646,7 @@ function verifyQueryContent(input) {
9723
9646
  extract: input.extract ?? extractPage
9724
9647
  });
9725
9648
  }
9726
- //#endregion
9727
- //#region packages/core/src/brand.ts
9649
+ // packages/core/src/brand.ts
9728
9650
  const COUNTRY_CODE_SECOND_LEVELS = /* @__PURE__ */ new Set([
9729
9651
  "ac",
9730
9652
  "co",
@@ -9832,8 +9754,7 @@ function shouldExcludeBrandQuery(input) {
9832
9754
  const terms = input.brandTerms?.length ? input.brandTerms : deriveBrandTerms({ siteUrl: input.siteUrl });
9833
9755
  return isBrandQuery(input.query, terms);
9834
9756
  }
9835
- //#endregion
9836
- //#region packages/core/src/analyze/shared.ts
9757
+ // packages/core/src/analyze/shared.ts
9837
9758
  const CTR_BASELINE = {
9838
9759
  1: .27,
9839
9760
  2: .15,
@@ -9868,8 +9789,7 @@ function jaccard(a, b) {
9868
9789
  function defaultDateRange$1(days = 28, now = /* @__PURE__ */ new Date()) {
9869
9790
  return finalGscDateRange(days, now);
9870
9791
  }
9871
- //#endregion
9872
- //#region packages/core/src/analyze/opportunity-primitives.ts
9792
+ // packages/core/src/analyze/opportunity-primitives.ts
9873
9793
  const MIN_BENCHMARK_URL_IMPRESSIONS = 30;
9874
9794
  const MIN_BENCHMARK_IMPRESSIONS = 1e3;
9875
9795
  const MIN_BENCHMARK_URL_SAMPLES = 5;
@@ -10282,8 +10202,7 @@ function queryOpportunityRecommendation(input) {
10282
10202
  expectedImpact: `About ${Math.round(input.estimatedClickLift)} extra clicks in the selected GSC window if CTR reaches the benchmark.`
10283
10203
  };
10284
10204
  }
10285
- //#endregion
10286
- //#region packages/core/src/analyze/page-opportunities-evidence.ts
10205
+ // packages/core/src/analyze/page-opportunities-evidence.ts
10287
10206
  function verificationFor(input) {
10288
10207
  const signals = pageTechnicalSignals(input);
10289
10208
  if (signals.length > 0) return {
@@ -10348,8 +10267,7 @@ function benchmarkDetails$2(benchmark, excludedTargetRows) {
10348
10267
  excludedTargetRows
10349
10268
  };
10350
10269
  }
10351
- //#endregion
10352
- //#region packages/core/src/analyze/query-quality.ts
10270
+ // packages/core/src/analyze/query-quality.ts
10353
10271
  function hasLetter(value) {
10354
10272
  return /\p{L}/u.test(value);
10355
10273
  }
@@ -10620,8 +10538,7 @@ function countQueryFlags(items) {
10620
10538
  flaggedRows
10621
10539
  };
10622
10540
  }
10623
- //#endregion
10624
- //#region packages/core/src/analyze/page-opportunities-selection.ts
10541
+ // packages/core/src/analyze/page-opportunities-selection.ts
10625
10542
  const DEFAULT_MIN_IMPRESSIONS$1 = 10;
10626
10543
  const DEFAULT_LIMIT$3 = 25;
10627
10544
  const MAX_LIMIT$3 = 100;
@@ -10714,8 +10631,7 @@ function selectBenchmarkRows$1(input) {
10714
10631
  });
10715
10632
  });
10716
10633
  }
10717
- //#endregion
10718
- //#region packages/core/src/analyze/page-opportunities-analysis.ts
10634
+ // packages/core/src/analyze/page-opportunities-analysis.ts
10719
10635
  function compareText$18(left, right) {
10720
10636
  return left < right ? -1 : left > right ? 1 : 0;
10721
10637
  }
@@ -10814,8 +10730,7 @@ function analyzePageOpportunitiesFromRows(input) {
10814
10730
  }
10815
10731
  };
10816
10732
  }
10817
- //#endregion
10818
- //#region packages/core/src/analyze/page-opportunities-presentation.ts
10733
+ // packages/core/src/analyze/page-opportunities-presentation.ts
10819
10734
  const actionPriority = new Map([
10820
10735
  "technical-check",
10821
10736
  "content-gap",
@@ -10879,8 +10794,7 @@ function pageOpportunityRecommendations(analysis) {
10879
10794
  if (gaps >= 3) recommendations.push(`Group the ${gaps} verified content gaps into useful sections rather than adding one thin paragraph per query.`);
10880
10795
  return recommendations;
10881
10796
  }
10882
- //#endregion
10883
- //#region packages/core/src/analyze/page-opportunities.ts
10797
+ // packages/core/src/analyze/page-opportunities.ts
10884
10798
  const MAX_BENCHMARK_ROWS = 1e5;
10885
10799
  const defaultDependencies$11 = {
10886
10800
  searchAnalytics: querySearchAnalytics,
@@ -11081,8 +10995,7 @@ async function pageOpportunitiesReport(input, dependencies = defaultDependencies
11081
10995
  recommendations: pageOpportunityRecommendations(analysis)
11082
10996
  };
11083
10997
  }
11084
- //#endregion
11085
- //#region packages/core/src/analyze/content-optimization.ts
10998
+ // packages/core/src/analyze/content-optimization.ts
11086
10999
  function intentFor(query) {
11087
11000
  const value = query.toLowerCase();
11088
11001
  if (/\b(vs|versus|alternative|alternatives|compare|comparison|best)\b/.test(value)) return "comparison";
@@ -11230,8 +11143,7 @@ function contentOptimizationFromPageOpportunities(sourceReport) {
11230
11143
  async function contentOptimizationReport(input) {
11231
11144
  return contentOptimizationFromPageOpportunities(await pageOpportunitiesReport(input));
11232
11145
  }
11233
- //#endregion
11234
- //#region packages/core/src/analyze/crawler/page-eligibility.ts
11146
+ // packages/core/src/analyze/crawler/page-eligibility.ts
11235
11147
  function comparableUrl(value) {
11236
11148
  if (!value) return void 0;
11237
11149
  try {
@@ -11258,8 +11170,7 @@ function isAuditableHtmlPage(page) {
11258
11170
  const contentAuditAllowed = page.contentAuditAllowed ?? (!page.blocked && page.robotsTxt?.allowed !== false);
11259
11171
  return isSuccessfulResponse(page) && isHtmlPage(page) && !isRedirectedPage(page) && contentAuditAllowed;
11260
11172
  }
11261
- //#endregion
11262
- //#region packages/core/src/analyze/crawler/audit.ts
11173
+ // packages/core/src/analyze/crawler/audit.ts
11263
11174
  function sameUrl(a, b) {
11264
11175
  if (!a || !b) return false;
11265
11176
  try {
@@ -11733,8 +11644,7 @@ function auditCrawlPages(pages, opts = {}) {
11733
11644
  }
11734
11645
  return issues;
11735
11646
  }
11736
- //#endregion
11737
- //#region packages/core/src/analyze/crawler/crawl-skip-reasons.ts
11647
+ // packages/core/src/analyze/crawler/crawl-skip-reasons.ts
11738
11648
  const CRAWL_SKIP_REASONS = [
11739
11649
  "asset-url",
11740
11650
  "configured-exclusion",
@@ -11809,8 +11719,7 @@ function crawlSkipReasonCountsFromRecord(input = {}) {
11809
11719
  count
11810
11720
  })) }).skipReasons;
11811
11721
  }
11812
- //#endregion
11813
- //#region packages/core/src/export/diagnosis-segment-csv.ts
11722
+ // packages/core/src/export/diagnosis-segment-csv.ts
11814
11723
  const dimensions = [
11815
11724
  "page",
11816
11725
  "query",
@@ -11911,8 +11820,7 @@ function diagnosisSegmentCsvFiles(report) {
11911
11820
  }))
11912
11821
  ];
11913
11822
  }
11914
- //#endregion
11915
- //#region packages/core/src/export/csv.ts
11823
+ // packages/core/src/export/csv.ts
11916
11824
  const CSV_SCHEMAS = {
11917
11825
  "priorities.csv": [
11918
11826
  "rank",
@@ -12377,8 +12285,7 @@ function refreshPrioritiesCsvFiles(report) {
12377
12285
  ...prefixCsvFiles("diagnosis", diagnoseCsvFiles(report.output.diagnosis))
12378
12286
  ]);
12379
12287
  }
12380
- //#endregion
12381
- //#region packages/core/src/analyze/crawler/top-fixes.ts
12288
+ // packages/core/src/analyze/crawler/top-fixes.ts
12382
12289
  const SEVERITY_SCORE = {
12383
12290
  high: 5e3,
12384
12291
  medium: 1200,
@@ -12529,8 +12436,7 @@ function reviewObservations(report, filters = {}) {
12529
12436
  recommendation: "review"
12530
12437
  });
12531
12438
  }
12532
- //#endregion
12533
- //#region packages/core/src/analyze/crawler/export.ts
12439
+ // packages/core/src/analyze/crawler/export.ts
12534
12440
  const issueHeaders = [
12535
12441
  "rule_id",
12536
12442
  "title",
@@ -12842,8 +12748,7 @@ function escapeHtml(value) {
12842
12748
  function escapeXml(value) {
12843
12749
  return escapeHtml(value);
12844
12750
  }
12845
- //#endregion
12846
- //#region packages/core/src/analyze/crawler/report.ts
12751
+ // packages/core/src/analyze/crawler/report.ts
12847
12752
  const MAX_CRAWL_PAGES = 1e4;
12848
12753
  const MAX_CRAWL_DEPTH = 64;
12849
12754
  const MAX_CRAWL_CONCURRENCY = 16;
@@ -13282,8 +13187,7 @@ function normalizeLoadedCrawlReport(report) {
13282
13187
  caveats: sanitizeMessages(report.caveats).sort(compareText$17)
13283
13188
  };
13284
13189
  }
13285
- //#endregion
13286
- //#region packages/core/src/analyze/crawler/site-checks-schema.ts
13190
+ // packages/core/src/analyze/crawler/site-checks-schema.ts
13287
13191
  const crawlSiteChecksSchema = z.object({ soft404: z.object({
13288
13192
  status: z.enum([
13289
13193
  "pass",
@@ -13301,8 +13205,7 @@ const crawlSiteChecksSchema = z.object({ soft404: z.object({
13301
13205
  probeLimit: z.number().int().positive(),
13302
13206
  complete: z.boolean()
13303
13207
  }) });
13304
- //#endregion
13305
- //#region packages/core/src/analyze/crawler/site-crawl-ai-schema.ts
13208
+ // packages/core/src/analyze/crawler/site-crawl-ai-schema.ts
13306
13209
  const crawlAiAvailabilitySchema = z.enum([
13307
13210
  "available",
13308
13211
  "absent",
@@ -13387,8 +13290,7 @@ const crawlAiSignalsSchema = z.object({
13387
13290
  contentSignal: z.string().optional()
13388
13291
  }).optional()
13389
13292
  });
13390
- //#endregion
13391
- //#region packages/core/src/analyze/crawler/schemas.ts
13293
+ // packages/core/src/analyze/crawler/schemas.ts
13392
13294
  const crawlerRuleSeveritySchema = z.enum([
13393
13295
  "low",
13394
13296
  "medium",
@@ -14346,8 +14248,7 @@ const crawlerJsonSchemas = {
14346
14248
  requestObservation: z.toJSONSchema(crawlRequestObservationSchema),
14347
14249
  pageSnapshot: z.toJSONSchema(crawlPageSnapshotSchema)
14348
14250
  };
14349
- //#endregion
14350
- //#region packages/core/src/analyze/crawler/agent-ready.ts
14251
+ // packages/core/src/analyze/crawler/agent-ready.ts
14351
14252
  function needsSignIn(page) {
14352
14253
  return page.status === 401 || page.status === 403 || Boolean(page.softAuthenticationGate);
14353
14254
  }
@@ -14398,8 +14299,7 @@ function agentReadyIssues(input) {
14398
14299
  }
14399
14300
  return issues;
14400
14301
  }
14401
- //#endregion
14402
- //#region packages/core/src/analyze/crawler/crawl-caveats.ts
14302
+ // packages/core/src/analyze/crawler/crawl-caveats.ts
14403
14303
  function crawlCaveats(input) {
14404
14304
  const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
14405
14305
  if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
@@ -14407,8 +14307,7 @@ function crawlCaveats(input) {
14407
14307
  if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
14408
14308
  return caveats;
14409
14309
  }
14410
- //#endregion
14411
- //#region packages/core/src/analyze/crawler/crawl-control.ts
14310
+ // packages/core/src/analyze/crawler/crawl-control.ts
14412
14311
  const CRAWL_CANCELLED = Symbol("crawl-cancelled");
14413
14312
  function abortController(input) {
14414
14313
  const controller = new AbortController();
@@ -14430,8 +14329,7 @@ function cancellationRace(signal) {
14430
14329
  signal.addEventListener("abort", () => resolve(CRAWL_CANCELLED), { once: true });
14431
14330
  });
14432
14331
  }
14433
- //#endregion
14434
- //#region packages/core/src/analyze/crawler/crawl-memory.ts
14332
+ // packages/core/src/analyze/crawler/crawl-memory.ts
14435
14333
  const MEBIBYTE$1 = 1048576;
14436
14334
  const MIN_CRAWL_MEMORY_LIMIT = 384 * MEBIBYTE$1;
14437
14335
  const MAX_CRAWL_MEMORY_LIMIT = 640 * MEBIBYTE$1;
@@ -14450,8 +14348,7 @@ function crawlRssLimitBytes(totalMemoryBytes) {
14450
14348
  function crawlMemoryPressure(input) {
14451
14349
  return input.memoryUsage.heapUsed + input.memoryUsage.external >= crawlMemoryLimitBytes(input.totalMemoryBytes) || input.memoryUsage.rss >= crawlRssLimitBytes(input.totalMemoryBytes);
14452
14350
  }
14453
- //#endregion
14454
- //#region packages/core/src/analyze/crawler/crawl-url-queue.ts
14351
+ // packages/core/src/analyze/crawler/crawl-url-queue.ts
14455
14352
  function before(left, right) {
14456
14353
  return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
14457
14354
  }
@@ -14530,8 +14427,7 @@ var CrawlUrlQueue = class {
14530
14427
  }
14531
14428
  }
14532
14429
  };
14533
- //#endregion
14534
- //#region packages/core/src/analyze/crawler/external-link-checks.ts
14430
+ // packages/core/src/analyze/crawler/external-link-checks.ts
14535
14431
  const EXTERNAL_LINK_SELECTION_LIMIT = 200;
14536
14432
  const EXTERNAL_LINK_STATES = [
14537
14433
  "available",
@@ -14719,8 +14615,7 @@ async function verifyExternalLinks(input) {
14719
14615
  warnings
14720
14616
  };
14721
14617
  }
14722
- //#endregion
14723
- //#region packages/core/src/analyze/crawler/link-graph.ts
14618
+ // packages/core/src/analyze/crawler/link-graph.ts
14724
14619
  function resolveFetchedAlias(url, aliases) {
14725
14620
  const visited = /* @__PURE__ */ new Set();
14726
14621
  let current = url;
@@ -14740,8 +14635,7 @@ function resolveLinkCountAliases(counts, aliases) {
14740
14635
  }
14741
14636
  return resolved;
14742
14637
  }
14743
- //#endregion
14744
- //#region packages/core/src/analyze/crawler/request-evidence.ts
14638
+ // packages/core/src/analyze/crawler/request-evidence.ts
14745
14639
  function observationFromPage(requestedUrl, page) {
14746
14640
  const observation = {
14747
14641
  requestedUrl,
@@ -14764,8 +14658,7 @@ function observationFromPage(requestedUrl, page) {
14764
14658
  extraction: page.extractionStatus ?? "complete"
14765
14659
  };
14766
14660
  }
14767
- //#endregion
14768
- //#region packages/core/src/analyze/crawler/site-checks.ts
14661
+ // packages/core/src/analyze/crawler/site-checks.ts
14769
14662
  const PROBE_COUNT = 2;
14770
14663
  function probeUrls(startUrl) {
14771
14664
  const origin = new URL(startUrl).origin;
@@ -14833,8 +14726,7 @@ async function collectCrawlSiteChecks(input) {
14833
14726
  }]
14834
14727
  };
14835
14728
  }
14836
- //#endregion
14837
- //#region packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
14729
+ // packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
14838
14730
  const MAX_AUXILIARY_RESPONSE_BYTES = 2097152;
14839
14731
  const LLMS_LINK_LIMIT = 20;
14840
14732
  const AI_BOTS = [
@@ -14936,7 +14828,7 @@ function llmsLinks(text, base) {
14936
14828
  } catch {}
14937
14829
  return links;
14938
14830
  }
14939
- async function checkLlmsTxt(input) {
14831
+ async function checkLlmsTxt$1(input) {
14940
14832
  const result = await get$1(input, new URL("/llms.txt", input.url).toString());
14941
14833
  const availability = result.availability === "available" && isHtml(result.contentType) ? "absent" : result.availability;
14942
14834
  const exists = availability === "available";
@@ -15064,7 +14956,7 @@ async function collectCrawlAiSignals(input) {
15064
14956
  const allows = input.respectRobots ? robots.allows : () => true;
15065
14957
  const startAllowed = allows(input.url);
15066
14958
  const [llmsTxt, agentResources, markdownNegotiation] = await Promise.all([
15067
- checkLlmsTxt(input),
14959
+ checkLlmsTxt$1(input),
15068
14960
  checkAgentResources(input),
15069
14961
  startAllowed ? checkMarkdownNegotiation(input) : Promise.resolve({
15070
14962
  url: input.url,
@@ -15128,8 +15020,7 @@ function linkRelations(header) {
15128
15020
  for (const match of (header ?? "").matchAll(/rel\s*=\s*"?([^";,]+)"?/gi)) for (const value of (match[1] ?? "").trim().toLowerCase().split(/\s+/)) if (value) relations.add(value);
15129
15021
  return [...relations].sort();
15130
15022
  }
15131
- //#endregion
15132
- //#region packages/core/src/analyze/crawler/site-crawl-providers.ts
15023
+ // packages/core/src/analyze/crawler/site-crawl-providers.ts
15133
15024
  function crawlMetricsWindow(now) {
15134
15025
  return {
15135
15026
  ...defaultDateRange$2(28, now),
@@ -15325,8 +15216,7 @@ async function joinSearchMetrics(input) {
15325
15216
  ...warning ? { warning } : {}
15326
15217
  };
15327
15218
  }
15328
- //#endregion
15329
- //#region packages/core/src/analyze/crawler/site-crawl-data-sources.ts
15219
+ // packages/core/src/analyze/crawler/site-crawl-data-sources.ts
15330
15220
  function crawlProviderLimits(input) {
15331
15221
  const normalize = (value, fallback, label) => {
15332
15222
  const limit = value ?? fallback;
@@ -15396,8 +15286,7 @@ async function crawlDataSources(input) {
15396
15286
  });
15397
15287
  return dataSources;
15398
15288
  }
15399
- //#endregion
15400
- //#region packages/core/src/analyze/monitoring/crawl-page.ts
15289
+ // packages/core/src/analyze/monitoring/crawl-page.ts
15401
15290
  function resolvedUrl(href, base) {
15402
15291
  if (!href) return void 0;
15403
15292
  try {
@@ -15591,6 +15480,9 @@ async function crawlOne(url, opts = {}) {
15591
15480
  warning: `${url}: ${message}`
15592
15481
  };
15593
15482
  }
15483
+ return snapshotFetchedPage(url, fetched);
15484
+ }
15485
+ async function snapshotFetchedPage(url, fetched) {
15594
15486
  const contentType = headerValue(fetched.headers, "content-type");
15595
15487
  if (fetched.diagnostics.accessBlock) {
15596
15488
  const page = unextractedPage(fetched, contentType);
@@ -15793,8 +15685,7 @@ async function crawlStatusOnly(url, opts = {}) {
15793
15685
  };
15794
15686
  }
15795
15687
  }
15796
- //#endregion
15797
- //#region packages/core/src/analyze/monitoring/sitemaps.ts
15688
+ // packages/core/src/analyze/monitoring/sitemaps.ts
15798
15689
  const INVALID_LOC_SAMPLE_LIMIT = 10;
15799
15690
  const LASTMOD_SAMPLE_LIMIT = 10;
15800
15691
  const MAX_SITEMAP_BYTES = 52428800;
@@ -16205,8 +16096,7 @@ async function fetchSitemapUrls(input) {
16205
16096
  warnings
16206
16097
  };
16207
16098
  }
16208
- //#endregion
16209
- //#region packages/core/src/providers/cache.ts
16099
+ // packages/core/src/providers/cache.ts
16210
16100
  function stableValue(value) {
16211
16101
  if (Array.isArray(value)) return value.map(stableValue);
16212
16102
  if (!value || typeof value !== "object") return value;
@@ -16292,8 +16182,7 @@ function writeProviderCache(key, input, options = {}) {
16292
16182
  taskIds: normalizedTaskIds
16293
16183
  };
16294
16184
  }
16295
- //#endregion
16296
- //#region packages/core/src/storage/provider-secrets.ts
16185
+ // packages/core/src/storage/provider-secrets.ts
16297
16186
  const KEYRING_SERVICE = "seo";
16298
16187
  const PRIVATE_FILE_MODE$1 = 384;
16299
16188
  const PROVIDER_SECRET_NAMES = {
@@ -16323,7 +16212,7 @@ function tightenFilePermissions(path) {
16323
16212
  if (error.code !== "ENOENT") throw error;
16324
16213
  }
16325
16214
  }
16326
- function readFile$1() {
16215
+ function readFile$2() {
16327
16216
  const path = getSeoCliPaths().providerSecretsFile;
16328
16217
  tightenFilePermissions(path);
16329
16218
  const raw = readJsonFile(path);
@@ -16344,7 +16233,7 @@ function writeFile$1(file) {
16344
16233
  writeJsonAtomic(path, file, PRIVATE_FILE_MODE$1);
16345
16234
  }
16346
16235
  function deleteFileSecret(name) {
16347
- const file = readFile$1();
16236
+ const file = readFile$2();
16348
16237
  if (!(name in file.secrets)) return;
16349
16238
  delete file.secrets[name];
16350
16239
  writeFile$1(file);
@@ -16363,7 +16252,7 @@ async function readProviderSecret(input) {
16363
16252
  source: "keychain"
16364
16253
  };
16365
16254
  } catch {}
16366
- const value = readFile$1().secrets[input.name];
16255
+ const value = readFile$2().secrets[input.name];
16367
16256
  return value ? {
16368
16257
  value,
16369
16258
  source: "file"
@@ -16377,7 +16266,7 @@ async function writeProviderSecret(name, value) {
16377
16266
  deleteFileSecret(name);
16378
16267
  return "keychain";
16379
16268
  } catch {}
16380
- const file = readFile$1();
16269
+ const file = readFile$2();
16381
16270
  file.secrets[name] = normalized;
16382
16271
  writeFile$1(file);
16383
16272
  return "file";
@@ -16398,8 +16287,7 @@ async function deleteManagedProviderSecrets() {
16398
16287
  const failed = results.flatMap((result, index) => result.status === "rejected" ? [MANAGED_PROVIDER_SECRET_NAMES[index]] : []);
16399
16288
  if (failed.length > 0) throw new Error(`Reset could not remove ${failed.length} saved provider credential record${failed.length === 1 ? "" : "s"}. Unlock the keychain or repair the local provider secrets file, then run \`seo reset --yes\` again.`, { cause: new AggregateError(results.flatMap((result) => result.status === "rejected" ? [result.reason] : [])) });
16400
16289
  }
16401
- //#endregion
16402
- //#region packages/core/src/clicky/credentials.ts
16290
+ // packages/core/src/clicky/credentials.ts
16403
16291
  const CLICKY_SITEKEY_ENV = "SEO_CLICKY_SITEKEY";
16404
16292
  const CLICKY_SITE_KEYS_SECRET = PROVIDER_SECRET_NAMES.clickySiteKeys;
16405
16293
  const storedSiteKeysSchema = z.object({
@@ -16485,8 +16373,7 @@ async function deleteClickySiteKey(siteIdValue) {
16485
16373
  siteKeys: stored.siteKeys
16486
16374
  }));
16487
16375
  }
16488
- //#endregion
16489
- //#region packages/core/src/provider-extensions/contracts.ts
16376
+ // packages/core/src/provider-extensions/contracts.ts
16490
16377
  const identifierPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
16491
16378
  const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/u;
16492
16379
  const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
@@ -16637,8 +16524,7 @@ const providerRegistrationSchema = z.object({
16637
16524
  actionIds.add(action.id);
16638
16525
  }
16639
16526
  });
16640
- //#endregion
16641
- //#region packages/core/src/provider-extensions/credentials.ts
16527
+ // packages/core/src/provider-extensions/credentials.ts
16642
16528
  const storedCredentialsSchema = z.object({
16643
16529
  version: z.literal(1),
16644
16530
  providers: z.record(providerExtensionIdSchema, z.record(z.string().length(64).regex(/^[a-f0-9]+$/u), z.record(z.string().min(1).max(64), z.string().min(1).max(4096))))
@@ -16708,8 +16594,7 @@ async function deleteProviderExtensionCredentials(input) {
16708
16594
  }
16709
16595
  await writeProviderSecret(PROVIDER_SECRET_NAMES.providerExtensionCredentials, JSON.stringify(stored));
16710
16596
  }
16711
- //#endregion
16712
- //#region packages/core/src/providers/transport.ts
16597
+ // packages/core/src/providers/transport.ts
16713
16598
  function schemaIssueSummary(error) {
16714
16599
  const issues = error.issues.slice(0, 3).map((issue) => {
16715
16600
  return `${issue.path.flatMap((segment) => {
@@ -16826,8 +16711,7 @@ async function providerRequestJson(input) {
16826
16711
  }
16827
16712
  return parsed.data;
16828
16713
  }
16829
- //#endregion
16830
- //#region packages/core/src/provider-extensions/registry.ts
16714
+ // packages/core/src/provider-extensions/registry.ts
16831
16715
  var ProviderExtensionRegistry = class {
16832
16716
  #providers = /* @__PURE__ */ new Map();
16833
16717
  hostFor(input) {
@@ -16860,8 +16744,7 @@ var ProviderExtensionRegistry = class {
16860
16744
  return [...this.#providers.values()].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
16861
16745
  }
16862
16746
  };
16863
- //#endregion
16864
- //#region packages/core/src/provider-extensions/store.ts
16747
+ // packages/core/src/provider-extensions/store.ts
16865
16748
  const PRIVATE_FILE_MODE = 384;
16866
16749
  function emptyStore() {
16867
16750
  return {
@@ -16912,8 +16795,7 @@ function removeInstalledProviderPackage(id) {
16912
16795
  });
16913
16796
  return removed;
16914
16797
  }
16915
- //#endregion
16916
- //#region packages/core/src/provider-extensions/loader.ts
16798
+ // packages/core/src/provider-extensions/loader.ts
16917
16799
  const installedPackageJsonSchema = z.object({
16918
16800
  name: z.string(),
16919
16801
  version: z.string(),
@@ -16992,8 +16874,7 @@ async function loadInstalledProviderExtensions(options = {}) {
16992
16874
  failures
16993
16875
  };
16994
16876
  }
16995
- //#endregion
16996
- //#region packages/core/src/provider-extensions/runtime.ts
16877
+ // packages/core/src/provider-extensions/runtime.ts
16997
16878
  const DEFAULT_MAX_REQUESTS = 8;
16998
16879
  const MAX_RESPONSE_BYTES$1 = 10485760;
16999
16880
  const MAX_REQUEST_BODY_BYTES = 65536;
@@ -17064,8 +16945,7 @@ async function verifyProviderExtension(input) {
17064
16945
  throw safeProviderAdapterError(input.providerId, error, input.credentials);
17065
16946
  }
17066
16947
  }
17067
- //#endregion
17068
- //#region packages/core/src/provider-extensions/analytics.ts
16948
+ // packages/core/src/provider-extensions/analytics.ts
17069
16949
  const CACHE_TTL_MS$1 = 864e5;
17070
16950
  const landingPageResultSchema = z.object({
17071
16951
  metric: z.literal("landing-page-visits"),
@@ -17146,8 +17026,7 @@ async function runAnalyticsProviderLandingPages(input) {
17146
17026
  });
17147
17027
  return parsed;
17148
17028
  }
17149
- //#endregion
17150
- //#region packages/core/src/analyze/workflows/analytics-value.ts
17029
+ // packages/core/src/analyze/workflows/analytics-value.ts
17151
17030
  const defaultDependencies$10 = {
17152
17031
  runGa4Report,
17153
17032
  analyticsLandingPages: runAnalyticsProviderLandingPages
@@ -17374,8 +17253,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
17374
17253
  }
17375
17254
  }[connection.provider](connection);
17376
17255
  }
17377
- //#endregion
17378
- //#region packages/core/src/analyze/crawler/site-crawl-dependencies.ts
17256
+ // packages/core/src/analyze/crawler/site-crawl-dependencies.ts
17379
17257
  function resolveCrawlSiteDependencies(dependencies = {}) {
17380
17258
  const hasInjectedPerPageSearchProvider = Boolean(dependencies.queryPageMetrics || dependencies.queryPageTopQuery);
17381
17259
  const hasInjectedLegacyBulkSearchProvider = Boolean(dependencies.queryPagesMetrics || dependencies.queryPagesTopQueries);
@@ -17400,8 +17278,7 @@ function resolveCrawlSiteDependencies(dependencies = {}) {
17400
17278
  totalMemory: dependencies.totalMemory ?? totalmem
17401
17279
  };
17402
17280
  }
17403
- //#endregion
17404
- //#region packages/core/src/analyze/crawler/sitemap-discovery.ts
17281
+ // packages/core/src/analyze/crawler/sitemap-discovery.ts
17405
17282
  const MAX_SITEMAP_ROOTS = 20;
17406
17283
  function isSameOrigin(url, origin) {
17407
17284
  try {
@@ -17489,8 +17366,7 @@ async function sitemapSeeds(input) {
17489
17366
  }
17490
17367
  };
17491
17368
  }
17492
- //#endregion
17493
- //#region packages/core/src/analyze/crawler/site-crawl.ts
17369
+ // packages/core/src/analyze/crawler/site-crawl.ts
17494
17370
  function normalizeUrl$1(value, base) {
17495
17371
  try {
17496
17372
  const url = base ? new URL(value, base) : new URL(value);
@@ -18094,8 +17970,7 @@ async function crawlSite(input, dependencies) {
18094
17970
  await renderer?.close();
18095
17971
  }
18096
17972
  }
18097
- //#endregion
18098
- //#region packages/core/src/brand-evidence.ts
17973
+ // packages/core/src/brand-evidence.ts
18099
17974
  const NAVIGATIONAL_STEM_THRESHOLDS = {
18100
17975
  maximumPosition: 2,
18101
17976
  minimumCtr: .2,
@@ -18227,8 +18102,7 @@ function resolveReportBrandTerms(input) {
18227
18102
  heuristic: true
18228
18103
  };
18229
18104
  }
18230
- //#endregion
18231
- //#region packages/core/src/analyze/min-impressions-policy.ts
18105
+ // packages/core/src/analyze/min-impressions-policy.ts
18232
18106
  const POSITION_MIN_IMPRESSIONS_SCALE = {
18233
18107
  share: 2e-4,
18234
18108
  floor: 10,
@@ -18280,8 +18154,7 @@ function minImpressionsCaveat(input) {
18280
18154
  const oneIn = Math.round(1 / policy.scale.share).toLocaleString("en-US");
18281
18155
  return `Minimum impressions per query/page row: ${policy.effective}, scaled from ${policy.basisImpressions} retained impressions (1 in ${oneIn}, between ${policy.scale.floor} and ${policy.scale.ceiling}); ${below}. This scaling is a heuristic.`;
18282
18156
  }
18283
- //#endregion
18284
- //#region packages/core/src/analyze/page-fragments.ts
18157
+ // packages/core/src/analyze/page-fragments.ts
18285
18158
  function compareCodepoints(left, right) {
18286
18159
  return left < right ? -1 : left > right ? 1 : 0;
18287
18160
  }
@@ -18357,8 +18230,7 @@ function collapseFragmentPageRows(rows, options = {}) {
18357
18230
  }
18358
18231
  };
18359
18232
  }
18360
- //#endregion
18361
- //#region packages/core/src/analyze/site-ctr-curve.ts
18233
+ // packages/core/src/analyze/site-ctr-curve.ts
18362
18234
  const SITE_CTR_CURVE_FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
18363
18235
  const SITE_CTR_CURVE_METHOD = {
18364
18236
  method: "position_bucket_url_p75_v2",
@@ -18487,8 +18359,7 @@ function createSiteCtrCurve(rows) {
18487
18359
  fallbackSource: SITE_CTR_CURVE_FALLBACK_SOURCE
18488
18360
  });
18489
18361
  }
18490
- //#endregion
18491
- //#region packages/core/src/analyze/ctr-underperformers-analysis.ts
18362
+ // packages/core/src/analyze/ctr-underperformers-analysis.ts
18492
18363
  const CTR_MAX_MIN_IMPRESSIONS = 1e9;
18493
18364
  const CTR_DEFAULT_LIMIT = 25;
18494
18365
  const CTR_MAX_LIMIT = 100;
@@ -18626,8 +18497,7 @@ function analyzeCtrUnderperformersFromRows(input) {
18626
18497
  benchmarkByPosition: benchmarkContext.byPosition
18627
18498
  };
18628
18499
  }
18629
- //#endregion
18630
- //#region packages/core/src/analyze/ctr-underperformers-report.ts
18500
+ // packages/core/src/analyze/ctr-underperformers-report.ts
18631
18501
  const MAX_GSC_ROWS$3 = 1e5;
18632
18502
  const DEFAULT_DAYS$3 = 28;
18633
18503
  const MAX_DAYS$2 = 548;
@@ -18761,8 +18631,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18761
18631
  ] : dataStatus === "partial" ? ["No CTR-only action is recommended from the validated retained rows. Refresh or inspect the provider evidence before treating this as an all-clear."] : ["No CTR-only action is recommended from this report. Use striking-distance or page-opportunities if you want more growth ideas."]
18762
18632
  };
18763
18633
  }
18764
- //#endregion
18765
- //#region packages/core/src/analyze/ctr-underperformers.ts
18634
+ // packages/core/src/analyze/ctr-underperformers.ts
18766
18635
  const defaultDependencies$9 = {
18767
18636
  searchAnalytics: querySearchAnalytics,
18768
18637
  now: () => /* @__PURE__ */ new Date()
@@ -18770,16 +18639,14 @@ const defaultDependencies$9 = {
18770
18639
  function ctrUnderperformersReport(input, dependencies = defaultDependencies$9) {
18771
18640
  return ctrUnderperformersReportWith(input, dependencies);
18772
18641
  }
18773
- //#endregion
18774
- //#region packages/core/src/phrasing.ts
18642
+ // packages/core/src/phrasing.ts
18775
18643
  function plural(count, singular, pluralLabel = `${singular}s`) {
18776
18644
  return count === 1 ? singular : pluralLabel;
18777
18645
  }
18778
18646
  function countLabel(count, singular, pluralLabel = `${singular}s`) {
18779
18647
  return `${count.toLocaleString("en-GB")} ${plural(count, singular, pluralLabel)}`;
18780
18648
  }
18781
- //#endregion
18782
- //#region packages/core/src/analyze/diagnosis-priorities.ts
18649
+ // packages/core/src/analyze/diagnosis-priorities.ts
18783
18650
  function topDelta(report) {
18784
18651
  const top = report.items[0];
18785
18652
  if (!top) return "No segment movement found.";
@@ -18837,8 +18704,7 @@ function buildDiagnosisPriorities(input) {
18837
18704
  }
18838
18705
  return priorities.slice(0, 6);
18839
18706
  }
18840
- //#endregion
18841
- //#region packages/core/src/analyze/diagnosis-status.ts
18707
+ // packages/core/src/analyze/diagnosis-status.ts
18842
18708
  function diagnosisPartialReasons(report) {
18843
18709
  const skippedSections = new Set(report.skippedSections?.map((section) => section.section));
18844
18710
  const segmentReasons = [
@@ -18890,8 +18756,7 @@ function diagnosisDataStatus(input) {
18890
18756
  if (input.criticalStatuses.length > 0 && input.criticalStatuses.every((status) => status === "skipped")) return "unavailable";
18891
18757
  return input.skippedSections || input.partialReasons ? "partial" : "complete";
18892
18758
  }
18893
- //#endregion
18894
- //#region packages/core/src/analyze/segment-impact-rows.ts
18759
+ // packages/core/src/analyze/segment-impact-rows.ts
18895
18760
  function fixed(value) {
18896
18761
  return Number(value.toFixed(3));
18897
18762
  }
@@ -18984,8 +18849,7 @@ function compareRetainedSegmentRows(input) {
18984
18849
  unmatched
18985
18850
  };
18986
18851
  }
18987
- //#endregion
18988
- //#region packages/core/src/analyze/segment-impact-analysis.ts
18852
+ // packages/core/src/analyze/segment-impact-analysis.ts
18989
18853
  function verdict$4(input) {
18990
18854
  if (input.status === "empty") return "Search Console returned no retained segment rows for either window.";
18991
18855
  if (!input.matchedRows) return "No segment was retained in both windows, so no movement delta was inferred.";
@@ -19096,8 +18960,7 @@ function analyzeSegmentRows(input) {
19096
18960
  ]
19097
18961
  };
19098
18962
  }
19099
- //#endregion
19100
- //#region packages/core/src/analyze/segment-impact-input.ts
18963
+ // packages/core/src/analyze/segment-impact-input.ts
19101
18964
  const DAY_MS$1 = 864e5;
19102
18965
  const MAX_WINDOW_DAYS = 240;
19103
18966
  function subtractDays(value, days) {
@@ -19152,8 +19015,7 @@ function resolveSegmentRanges(input) {
19152
19015
  function validateSegmentSite(site) {
19153
19016
  if (!site.trim()) throw new SeoError("INVALID_INPUT", "site must not be empty.");
19154
19017
  }
19155
- //#endregion
19156
- //#region packages/core/src/analyze/segment-impact-fallback.ts
19018
+ // packages/core/src/analyze/segment-impact-fallback.ts
19157
19019
  function unavailableSegmentImpactReport(input) {
19158
19020
  const days = segmentRangeDays(input.after);
19159
19021
  const before = segmentComparisonRange(input.after, days);
@@ -19232,8 +19094,7 @@ function unavailableSegmentImpactReport(input) {
19232
19094
  caveats: ["No segment movement conclusion was produced because the provider evidence was unavailable."]
19233
19095
  };
19234
19096
  }
19235
- //#endregion
19236
- //#region packages/core/src/analyze/segment-impact.ts
19097
+ // packages/core/src/analyze/segment-impact.ts
19237
19098
  const DEFAULT_MAX_ROWS = 1e5;
19238
19099
  const defaultDependencies$8 = {
19239
19100
  searchAnalytics: querySearchAnalytics,
@@ -19342,8 +19203,7 @@ async function segmentImpact(input, dependencies = defaultDependencies$8) {
19342
19203
  generatedAt: now.toISOString()
19343
19204
  });
19344
19205
  }
19345
- //#endregion
19346
- //#region packages/core/src/imports/records.ts
19206
+ // packages/core/src/imports/records.ts
19347
19207
  const DEFAULT_IMPORT_STREAM_BYTES = 5e7;
19348
19208
  async function importFile(input) {
19349
19209
  const path = resolve(input.file);
@@ -19550,8 +19410,7 @@ async function readJsonArray(input) {
19550
19410
  if (!Array.isArray(parsed)) throw new SeoError("INVALID_INPUT", `${input.label} JSON must be an array of objects.`);
19551
19411
  return parsed;
19552
19412
  }
19553
- //#endregion
19554
- //#region packages/core/src/providers/imports/research-columns.ts
19413
+ // packages/core/src/providers/imports/research-columns.ts
19555
19414
  const RESEARCH_IMPORT_COLUMN_KEYS = [
19556
19415
  "keyword",
19557
19416
  "url",
@@ -20166,8 +20025,7 @@ async function importResearchRows(source, now = /* @__PURE__ */ new Date()) {
20166
20025
  warnings
20167
20026
  };
20168
20027
  }
20169
- //#endregion
20170
- //#region packages/core/src/providers/imports/research-provider.ts
20028
+ // packages/core/src/providers/imports/research-provider.ts
20171
20029
  function compareText$11(left, right) {
20172
20030
  return left < right ? -1 : left > right ? 1 : 0;
20173
20031
  }
@@ -20208,8 +20066,7 @@ async function combineResearchRows(sources, now = /* @__PURE__ */ new Date()) {
20208
20066
  warnings
20209
20067
  };
20210
20068
  }
20211
- //#endregion
20212
- //#region packages/core/src/analyze/page-patterns.ts
20069
+ // packages/core/src/analyze/page-patterns.ts
20213
20070
  const LOCAL_QUERY_TERMS = [
20214
20071
  "near me",
20215
20072
  "today",
@@ -20380,8 +20237,7 @@ function dominantTemplateFamily(rows) {
20380
20237
  share: top.count / rows.length
20381
20238
  };
20382
20239
  }
20383
- //#endregion
20384
- //#region packages/core/src/analyze/site-diagnostics/cannibal-analysis-primitives.ts
20240
+ // packages/core/src/analyze/site-diagnostics/cannibal-analysis-primitives.ts
20385
20241
  function compareCannibalText(left, right) {
20386
20242
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
20387
20243
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
@@ -20426,8 +20282,7 @@ function addCannibalRow(group, url, row) {
20426
20282
  position: weightedPosition / impressions
20427
20283
  });
20428
20284
  }
20429
- //#endregion
20430
- //#region packages/core/src/analyze/site-diagnostics/cannibal-item.ts
20285
+ // packages/core/src/analyze/site-diagnostics/cannibal-item.ts
20431
20286
  function cannibalReviewContext(query, rows) {
20432
20287
  const context = [];
20433
20288
  if (isQuotedQuery(query)) context.push("quoted-query");
@@ -20505,8 +20360,7 @@ function createCannibalItem(input) {
20505
20360
  function compareCannibalItems(left, right) {
20506
20361
  return right.priority.score - left.priority.score || right.priority.demandImpressions - left.priority.demandImpressions || compareCannibalText(left.query, right.query);
20507
20362
  }
20508
- //#endregion
20509
- //#region packages/core/src/analyze/site-diagnostics/cannibal-import.ts
20363
+ // packages/core/src/analyze/site-diagnostics/cannibal-import.ts
20510
20364
  const MAX_SUPPRESSIONS$1 = 100;
20511
20365
  function siteScope(site) {
20512
20366
  const withoutScDomain = site.trim().replace(/^sc-domain:/iu, "");
@@ -20784,8 +20638,7 @@ async function cannibalImportReport(input, dependencies = {}) {
20784
20638
  recommendations: analysis.items.length ? ["Review the highest-volume candidates first. Confirm whether the URLs serve the same intent and inspect technical state before changing canonicals, redirects, or content.", "Run this report against Search Console for the same site to confirm real query exposure before consolidating URLs."] : partialEvidence ? ["No multi-URL action is recommended from the retained imported rows. Inspect or re-export the provider data before treating this result as an all-clear."] : ["No multi-URL action is recommended from the retained imported rows."]
20785
20639
  };
20786
20640
  }
20787
- //#endregion
20788
- //#region packages/core/src/storage/ledger.ts
20641
+ // packages/core/src/storage/ledger.ts
20789
20642
  var SessionLedger = class {
20790
20643
  entries = /* @__PURE__ */ new Map();
20791
20644
  gscCalls = 0;
@@ -20815,8 +20668,7 @@ var SessionLedger = class {
20815
20668
  return providerBits.join(" · ");
20816
20669
  }
20817
20670
  };
20818
- //#endregion
20819
- //#region packages/core/src/analyze/site-diagnostics/cannibal-analysis.ts
20671
+ // packages/core/src/analyze/site-diagnostics/cannibal-analysis.ts
20820
20672
  const MINIMUM_PAGE_SHARE = .1;
20821
20673
  const MINIMUM_PAGE_IMPRESSIONS = 10;
20822
20674
  const MAXIMUM_DOMINANT_SHARE = .8;
@@ -20978,10 +20830,20 @@ function analyzeCannibalRows(input) {
20978
20830
  templates: summarizeTemplates(returnedItems.flatMap((item) => item.pages))
20979
20831
  };
20980
20832
  }
20833
+ /**
20834
+ * Empty, partial, filtered or complete. Partial evidence (a possibly capped
20835
+ * source, invalid rows, or a candidate with no query-level demand row) never
20836
+ * becomes filtered, so it cannot read as an all-clear.
20837
+ */
20838
+ function cannibalDataStatus(selection, possiblyTruncated) {
20839
+ if (selection.sourceRows === 0) return "empty";
20840
+ if (possiblyTruncated || selection.invalidRows + selection.propertyInvalidRows > 0 || selection.missingPropertyQueries > 0) return "partial";
20841
+ return selection.eligibleClusters === 0 ? "filtered" : "complete";
20842
+ }
20843
+ const CANNIBAL_MINIMUM_PAGE_IMPRESSIONS = MINIMUM_PAGE_IMPRESSIONS;
20981
20844
  const CANNIBAL_MINIMUM_PAGE_SHARE = MINIMUM_PAGE_SHARE;
20982
20845
  const CANNIBAL_MAXIMUM_DOMINANT_SHARE = MAXIMUM_DOMINANT_SHARE;
20983
- //#endregion
20984
- //#region packages/core/src/analyze/site-diagnostics/cannibal-report.ts
20846
+ // packages/core/src/analyze/site-diagnostics/cannibal-report.ts
20985
20847
  const MAX_GSC_ROWS$2 = 1e5;
20986
20848
  function verdict$3(input) {
20987
20849
  if (input.status === "empty") return "Search Console returned no retained query/page rows for this window.";
@@ -21050,7 +20912,7 @@ async function cannibalReportWith(input, dependencies) {
21050
20912
  const possiblyTruncated = pageTruncated || propertyTruncated;
21051
20913
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
21052
20914
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
21053
- const dataStatus = analysis.selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation || analysis.selection.missingPropertyQueries > 0 ? "partial" : analysis.selection.eligibleClusters === 0 ? "filtered" : "complete";
20915
+ const dataStatus = cannibalDataStatus(analysis.selection, possiblyTruncated);
21054
20916
  return {
21055
20917
  schemaVersion: 1,
21056
20918
  dataSource: "search-console-api",
@@ -21134,8 +20996,7 @@ async function cannibalReportWith(input, dependencies) {
21134
20996
  ledgerSummary: ledger.summary()
21135
20997
  };
21136
20998
  }
21137
- //#endregion
21138
- //#region packages/core/src/analyze/site-diagnostics/cannibal.ts
20999
+ // packages/core/src/analyze/site-diagnostics/cannibal.ts
21139
21000
  const defaultDependencies$7 = {
21140
21001
  searchAnalytics: querySearchAnalytics,
21141
21002
  now: () => /* @__PURE__ */ new Date()
@@ -21160,8 +21021,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$7) {
21160
21021
  }
21161
21022
  return cannibalReportWith(input, dependencies);
21162
21023
  }
21163
- //#endregion
21164
- //#region packages/core/src/analyze/site-diagnostics/decay-analysis-primitives.ts
21024
+ // packages/core/src/analyze/site-diagnostics/decay-analysis-primitives.ts
21165
21025
  function compareDecayText(left, right) {
21166
21026
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
21167
21027
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
@@ -21300,8 +21160,7 @@ function boundedDecayNumber(value, fallback, min, max) {
21300
21160
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21301
21161
  return Math.min(max, Math.max(min, value));
21302
21162
  }
21303
- //#endregion
21304
- //#region packages/core/src/analyze/site-diagnostics/decay-analysis.ts
21163
+ // packages/core/src/analyze/site-diagnostics/decay-analysis.ts
21305
21164
  function analyzeDecay(input) {
21306
21165
  const minDropPct = boundedDecayNumber(input.minDropPct, 20, 0, 100);
21307
21166
  const minPreviousClicks = boundedDecayNumber(input.minPreviousClicks, 2, 0, 1e9);
@@ -21424,8 +21283,7 @@ function analyzeDecay(input) {
21424
21283
  templates: summarizeTemplates(returned)
21425
21284
  };
21426
21285
  }
21427
- //#endregion
21428
- //#region packages/core/src/analyze/site-diagnostics/decaying-report.ts
21286
+ // packages/core/src/analyze/site-diagnostics/decaying-report.ts
21429
21287
  const MAX_GSC_ROWS$1 = 1e5;
21430
21288
  function verdict$2(report) {
21431
21289
  if (report.dataStatus === "empty") return "Search Console returned no retained query/page rows for either comparison window.";
@@ -21596,8 +21454,7 @@ async function decayingReportWith(input, dependencies) {
21596
21454
  ]
21597
21455
  };
21598
21456
  }
21599
- //#endregion
21600
- //#region packages/core/src/analyze/site-diagnostics/decaying.ts
21457
+ // packages/core/src/analyze/site-diagnostics/decaying.ts
21601
21458
  const defaultDependencies$6 = {
21602
21459
  searchAnalytics: querySearchAnalytics,
21603
21460
  now: () => /* @__PURE__ */ new Date()
@@ -21605,8 +21462,7 @@ const defaultDependencies$6 = {
21605
21462
  function decayingReport(input, dependencies = defaultDependencies$6) {
21606
21463
  return decayingReportWith(input, dependencies);
21607
21464
  }
21608
- //#endregion
21609
- //#region packages/core/src/analyze/site-diagnostics/quick-win-groups.ts
21465
+ // packages/core/src/analyze/site-diagnostics/quick-win-groups.ts
21610
21466
  function normalizedQuery(value) {
21611
21467
  return value.normalize("NFKC").toLocaleLowerCase("und").replace(/\s+/gu, " ").trim();
21612
21468
  }
@@ -21651,8 +21507,7 @@ function groupQuickWins(items) {
21651
21507
  return group;
21652
21508
  }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$10(left.id, right.id));
21653
21509
  }
21654
- //#endregion
21655
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
21510
+ // packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
21656
21511
  function boundedInteger$3(value, fallback, minimum, maximum) {
21657
21512
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21658
21513
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
@@ -21752,8 +21607,7 @@ function compareQuickWinItems(left, right) {
21752
21607
  function roundedSum(values) {
21753
21608
  return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
21754
21609
  }
21755
- //#endregion
21756
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis.ts
21610
+ // packages/core/src/analyze/site-diagnostics/quick-wins-analysis.ts
21757
21611
  const MAX_MIN_IMPRESSIONS$2 = 1e9;
21758
21612
  const DEFAULT_LIMIT$2 = 25;
21759
21613
  const MAX_LIMIT$2 = 100;
@@ -21860,8 +21714,7 @@ function analyzeQuickWinsFromRows(input) {
21860
21714
  benchmarkByPosition: benchmarkContext.byPosition
21861
21715
  };
21862
21716
  }
21863
- //#endregion
21864
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
21717
+ // packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
21865
21718
  function compareText$8(left, right) {
21866
21719
  return left < right ? -1 : left > right ? 1 : 0;
21867
21720
  }
@@ -21910,8 +21763,7 @@ function quickWinTemplateSummaries(items) {
21910
21763
  sampleUrls: [...item.urls].sort(compareText$8).slice(0, 3)
21911
21764
  })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$8(left.id, right.id)).slice(0, 5);
21912
21765
  }
21913
- //#endregion
21914
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
21766
+ // packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
21915
21767
  const DEFAULT_DAYS$2 = 28;
21916
21768
  const MAX_DAYS$1 = 548;
21917
21769
  const DEFAULT_VERIFY_LIMIT$1 = 5;
@@ -22097,8 +21949,7 @@ async function quickWinsReportWith(input, dependencies) {
22097
21949
  warnings
22098
21950
  };
22099
21951
  }
22100
- //#endregion
22101
- //#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
21952
+ // packages/core/src/analyze/site-diagnostics/quick-wins.ts
22102
21953
  const defaultDependencies$5 = {
22103
21954
  searchAnalytics: querySearchAnalytics,
22104
21955
  fetch: fetchPage,
@@ -22108,8 +21959,7 @@ const defaultDependencies$5 = {
22108
21959
  function quickWinsReport(input, dependencies = defaultDependencies$5) {
22109
21960
  return quickWinsReportWith(input, dependencies);
22110
21961
  }
22111
- //#endregion
22112
- //#region packages/core/src/analyze/striking-distance-analysis-primitives.ts
21962
+ // packages/core/src/analyze/striking-distance-analysis-primitives.ts
22113
21963
  function compareText$7(left, right) {
22114
21964
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
22115
21965
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
@@ -22216,8 +22066,7 @@ function groupStrikingDistanceItems(items) {
22216
22066
  };
22217
22067
  }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$7(left.id, right.id));
22218
22068
  }
22219
- //#endregion
22220
- //#region packages/core/src/analyze/striking-distance-analysis.ts
22069
+ // packages/core/src/analyze/striking-distance-analysis.ts
22221
22070
  const MAX_MIN_IMPRESSIONS$1 = 1e9;
22222
22071
  const DEFAULT_LIMIT$1 = 25;
22223
22072
  const MAX_LIMIT$1 = 100;
@@ -22356,8 +22205,7 @@ function analyzeStrikingDistanceRows(input) {
22356
22205
  items
22357
22206
  };
22358
22207
  }
22359
- //#endregion
22360
- //#region packages/core/src/analyze/striking-distance-report.ts
22208
+ // packages/core/src/analyze/striking-distance-report.ts
22361
22209
  const DEFAULT_DAYS$1 = 28;
22362
22210
  const MAX_DAYS = 548;
22363
22211
  const DEFAULT_VERIFY_LIMIT = 5;
@@ -22512,8 +22360,7 @@ async function strikingDistanceWith(input, dependencies) {
22512
22360
  recommendations: analysis.groups.length ? analysis.groups.slice(0, 5).map((group) => group.recommendation.action) : ["No action is recommended from this report. Lower the minimum impressions or widen the date window if you need long-tail inspection."]
22513
22361
  };
22514
22362
  }
22515
- //#endregion
22516
- //#region packages/core/src/analyze/striking-distance.ts
22363
+ // packages/core/src/analyze/striking-distance.ts
22517
22364
  const defaultDependencies$4 = {
22518
22365
  searchAnalytics: querySearchAnalytics,
22519
22366
  verifyContent: verifyQueryContent,
@@ -22522,8 +22369,7 @@ const defaultDependencies$4 = {
22522
22369
  function strikingDistance(input, dependencies = defaultDependencies$4) {
22523
22370
  return strikingDistanceWith(input, dependencies);
22524
22371
  }
22525
- //#endregion
22526
- //#region packages/core/src/updates/search-status.ts
22372
+ // packages/core/src/updates/search-status.ts
22527
22373
  const INCIDENTS_URL = "https://status.search.google.com/incidents.json";
22528
22374
  async function fetchSearchStatusIncidents() {
22529
22375
  const response = await fetch$1(INCIDENTS_URL, { signal: AbortSignal.timeout(8e3) });
@@ -22533,8 +22379,7 @@ async function fetchSearchStatusIncidents() {
22533
22379
  async function listSearchUpdates(input = {}) {
22534
22380
  return searchUpdatesFromIncidents(withRankingHistory(await fetchSearchStatusIncidents()), input);
22535
22381
  }
22536
- //#endregion
22537
- //#region packages/core/src/analyze/traffic-anomaly-analysis.ts
22382
+ // packages/core/src/analyze/traffic-anomaly-analysis.ts
22538
22383
  /** The step-change heuristic and its thresholds, returned with the report. */
22539
22384
  const STEP_CHANGE_RULES = {
22540
22385
  method: "best-split",
@@ -22890,8 +22735,7 @@ function analyzeTrafficAnomalyRows(input) {
22890
22735
  }
22891
22736
  };
22892
22737
  }
22893
- //#endregion
22894
- //#region packages/core/src/analyze/traffic-anomaly-report.ts
22738
+ // packages/core/src/analyze/traffic-anomaly-report.ts
22895
22739
  async function trafficAnomalyWith(input, dependencies) {
22896
22740
  const days = input.days ?? 90;
22897
22741
  const recentDays = input.recentDays ?? 7;
@@ -22934,8 +22778,7 @@ async function trafficAnomalyWith(input, dependencies) {
22934
22778
  detection: analyzed.detection
22935
22779
  };
22936
22780
  }
22937
- //#endregion
22938
- //#region packages/core/src/analyze/traffic-anomaly.ts
22781
+ // packages/core/src/analyze/traffic-anomaly.ts
22939
22782
  const defaultDependencies$3 = {
22940
22783
  searchAnalytics: querySearchAnalytics,
22941
22784
  now: () => /* @__PURE__ */ new Date()
@@ -22982,8 +22825,7 @@ async function updateCorrelation(input) {
22982
22825
  })
22983
22826
  };
22984
22827
  }
22985
- //#endregion
22986
- //#region packages/core/src/analyze/diagnose-property.ts
22828
+ // packages/core/src/analyze/diagnose-property.ts
22987
22829
  function errorReason(error) {
22988
22830
  return error instanceof Error ? error.message : String(error);
22989
22831
  }
@@ -23519,8 +23361,7 @@ async function diagnoseProperty(input, dependencies = {}) {
23519
23361
  quickWins
23520
23362
  };
23521
23363
  }
23522
- //#endregion
23523
- //#region packages/core/src/decision-model.ts
23364
+ // packages/core/src/decision-model.ts
23524
23365
  var DecisionModelError = class extends Error {
23525
23366
  kind;
23526
23367
  retryAfterMs;
@@ -23673,8 +23514,7 @@ var DecisionRunner = class {
23673
23514
  return null;
23674
23515
  }
23675
23516
  };
23676
- //#endregion
23677
- //#region packages/core/src/analyze/internal-links-index.ts
23517
+ // packages/core/src/analyze/internal-links-index.ts
23678
23518
  const PAGE_TERMS = 300;
23679
23519
  const VECTOR_TERMS = 100;
23680
23520
  var SiteIndex = class {
@@ -23801,7 +23641,7 @@ const GENERIC_ANCHORS = /^(click here|here|read more|learn more|more|this|this p
23801
23641
  function normalise(value) {
23802
23642
  return value.normalize("NFKD").replace(/\p{Diacritic}/gu, "").normalize("NFC").replace(/([\p{L}\p{N}])[’'`]([\p{L}\p{N}])/gu, "$1$2").toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").replace(/\s+/g, " ").trim();
23803
23643
  }
23804
- const words$1 = (value) => normalise(value).split(" ").filter(Boolean);
23644
+ const words$2 = (value) => normalise(value).split(" ").filter(Boolean);
23805
23645
  const FUNCTION_WORDS = new Set("a an and are as at be but by for from has have how if in into is it its of on or that the their then there these they this to was we were what when where which who why will with you your our can do does not just more most also than too very about after before over under again all any each other some such no nor only own same so up out off one two both every few several many much should would could because while since although though until unless whether however".split(" "));
23806
23646
  const NOT_PLURAL = /* @__PURE__ */ new Set([
23807
23647
  "always",
@@ -23862,7 +23702,7 @@ function stem(word) {
23862
23702
  if (one.length > 6 && one.endsWith("ing")) return one.slice(0, -3);
23863
23703
  return one;
23864
23704
  }
23865
- const terms = (text) => words$1(text).filter((word) => word.length > 1 && !FUNCTION_WORDS.has(word) && !/^\d+$/.test(word)).map(stem);
23705
+ const terms = (text) => words$2(text).filter((word) => word.length > 1 && !FUNCTION_WORDS.has(word) && !/^\d+$/.test(word)).map(stem);
23866
23706
  const slugWords = (url) => {
23867
23707
  try {
23868
23708
  return new URL(url).pathname.split(/[/._-]+/).filter(Boolean).join(" ");
@@ -24148,8 +23988,7 @@ function placementPage(input) {
24148
23988
  sentences: pageSentences(input.html)
24149
23989
  };
24150
23990
  }
24151
- //#endregion
24152
- //#region packages/core/src/analyze/internal-links-crawl.ts
23991
+ // packages/core/src/analyze/internal-links-crawl.ts
24153
23992
  const DEFAULT_MAX_PAGES = 500;
24154
23993
  const MAX_MAX_PAGES = 5e3;
24155
23994
  const FETCH_CONCURRENCY = 6;
@@ -24521,8 +24360,7 @@ async function crawl(input, robots, sentences, renderer, warnings) {
24521
24360
  warnings
24522
24361
  };
24523
24362
  }
24524
- //#endregion
24525
- //#region packages/core/src/analyze/internal-links-judge.ts
24363
+ // packages/core/src/analyze/internal-links-judge.ts
24526
24364
  const PAGE_TYPES = {
24527
24365
  pillar_guide: "A long, broad guide that covers a whole topic and would naturally link out to many narrower pages",
24528
24366
  support_article: "A focused article, post, tutorial, or explainer about one narrower subject",
@@ -24746,8 +24584,7 @@ function decideWithoutModel(placement, similarity) {
24746
24584
  anchor: placement.anchors.find((option) => option.sentence === sentence?.id)?.text ?? null
24747
24585
  };
24748
24586
  }
24749
- //#endregion
24750
- //#region packages/core/src/analyze/internal-links-report.ts
24587
+ // packages/core/src/analyze/internal-links-report.ts
24751
24588
  const DAY$1 = 864e5;
24752
24589
  const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
24753
24590
  function internalLinksReportRange(input, fallbackDays, now) {
@@ -24775,8 +24612,7 @@ function internalLinksVerdict(report) {
24775
24612
  const capped = report.crawl.capReached ? ` The crawl stopped at its limit of ${report.crawl.pageLimit} pages, so pages beyond it were not checked.` : "";
24776
24613
  return `${judged} ${scope}${counts.heldBack ? `; ${counts.heldBack} held back by the evidence floors` : ""}.${capped}`;
24777
24614
  }
24778
- //#endregion
24779
- //#region packages/core/src/analyze/internal-links.ts
24615
+ // packages/core/src/analyze/internal-links.ts
24780
24616
  const GSC_DAYS = 90;
24781
24617
  const MAX_GSC_ROWS = 1e5;
24782
24618
  const MAX_PAGES_PER_QUERY = 25;
@@ -25285,8 +25121,7 @@ function caveats$1(report) {
25285
25121
  "Similarity, sentence match and anchors are text heuristics, and a model probability is a judgement of reader value. None of them predicts rankings or traffic."
25286
25122
  ];
25287
25123
  }
25288
- //#endregion
25289
- //#region packages/core/src/analyze/url-templates.ts
25124
+ // packages/core/src/analyze/url-templates.ts
25290
25125
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25291
25126
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25292
25127
  function compareText$6(left, right) {
@@ -25415,8 +25250,7 @@ function clusterPseoTemplates(urls, opts = {}) {
25415
25250
  sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
25416
25251
  })).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$6(a.signature, b.signature)).slice(0, opts.limit ?? 50);
25417
25252
  }
25418
- //#endregion
25419
- //#region packages/core/src/analyze/monitoring/index-coverage.ts
25253
+ // packages/core/src/analyze/monitoring/index-coverage.ts
25420
25254
  const DEFAULT_ITEMS_PER_SECTION = 100;
25421
25255
  const MAX_ITEMS_PER_SECTION = 1e3;
25422
25256
  const DEFAULT_TEMPLATE_LIMIT = 50;
@@ -25759,8 +25593,7 @@ function analyzeIndexCoverageSignals(input) {
25759
25593
  })
25760
25594
  };
25761
25595
  }
25762
- //#endregion
25763
- //#region packages/core/src/analyze/monitoring/index-coverage-report.ts
25596
+ // packages/core/src/analyze/monitoring/index-coverage-report.ts
25764
25597
  const MAX_SITEMAP_DOCUMENTS = 20;
25765
25598
  const MAX_SEARCH_ROWS = 25e4;
25766
25599
  const MAX_SITEMAP_URLS = 25e4;
@@ -25890,8 +25723,7 @@ async function indexCoverageSignals(input, dependencies = defaultDependencies$1)
25890
25723
  }
25891
25724
  };
25892
25725
  }
25893
- //#endregion
25894
- //#region packages/core/src/analyze/monitoring/redirect-trace.ts
25726
+ // packages/core/src/analyze/monitoring/redirect-trace.ts
25895
25727
  function resolveNextUrl(location, baseUrl) {
25896
25728
  try {
25897
25729
  const next = new URL(location, baseUrl);
@@ -25993,8 +25825,7 @@ async function redirectTrace(input) {
25993
25825
  warnings
25994
25826
  };
25995
25827
  }
25996
- //#endregion
25997
- //#region packages/core/src/analyze/monitoring/link-recover.ts
25828
+ // packages/core/src/analyze/monitoring/link-recover.ts
25998
25829
  function primaryIssue(issues) {
25999
25830
  return [
26000
25831
  "final-5xx",
@@ -26131,8 +25962,7 @@ async function linkRecover(input) {
26131
25962
  warnings: [...new Set(warnings)].slice(0, 50)
26132
25963
  };
26133
25964
  }
26134
- //#endregion
26135
- //#region packages/core/src/analyze/performance-analysis.ts
25965
+ // packages/core/src/analyze/performance-analysis.ts
26136
25966
  const FIELD_THRESHOLDS = {
26137
25967
  CLS: {
26138
25968
  good: .1,
@@ -26340,8 +26170,7 @@ function performanceActions(report) {
26340
26170
  });
26341
26171
  return actions;
26342
26172
  }
26343
- //#endregion
26344
- //#region packages/core/src/analyze/performance-crux.ts
26173
+ // packages/core/src/analyze/performance-crux.ts
26345
26174
  const CRUX_METRICS = [
26346
26175
  "cumulative_layout_shift",
26347
26176
  "interaction_to_next_paint",
@@ -26470,8 +26299,7 @@ async function fetchCruxFieldData(input) {
26470
26299
  };
26471
26300
  }
26472
26301
  }
26473
- //#endregion
26474
- //#region packages/core/src/analyze/performance-lighthouse.ts
26302
+ // packages/core/src/analyze/performance-lighthouse.ts
26475
26303
  const execFileAsync$1 = promisify(execFile);
26476
26304
  const INSIGHT_BYTE_BUDGET = 24e3;
26477
26305
  function compactRecord(value) {
@@ -26662,8 +26490,7 @@ async function runLighthouse(input) {
26662
26490
  }
26663
26491
  return report;
26664
26492
  }
26665
- //#endregion
26666
- //#region packages/core/src/analyze/performance.ts
26493
+ // packages/core/src/analyze/performance.ts
26667
26494
  const CACHE_TTL_MS = 864e5;
26668
26495
  function reportId(input) {
26669
26496
  return `perf_${createHash("sha256").update(JSON.stringify({
@@ -26853,8 +26680,7 @@ async function performanceAudit(input) {
26853
26680
  })) cacheSet(report, now.getTime());
26854
26681
  return report;
26855
26682
  }
26856
- //#endregion
26857
- //#region packages/core/src/analyze/query-cluster-primitives.ts
26683
+ // packages/core/src/analyze/query-cluster-primitives.ts
26858
26684
  const CLUSTER_THRESHOLD = .5;
26859
26685
  const MAX_TOKEN_BUCKET = 250;
26860
26686
  const MAX_TOKEN_SHARE = .08;
@@ -26970,8 +26796,7 @@ function* iterateQueryClusters(rows) {
26970
26796
  yield group;
26971
26797
  }
26972
26798
  }
26973
- //#endregion
26974
- //#region packages/core/src/analyze/query-cluster-analysis.ts
26799
+ // packages/core/src/analyze/query-cluster-analysis.ts
26975
26800
  function quotedLabel(label) {
26976
26801
  return label.includes("\"") ? `'${label}'` : `"${label}"`;
26977
26802
  }
@@ -27191,8 +27016,7 @@ function analyzeQueryClustersFromRows(input) {
27191
27016
  limit
27192
27017
  };
27193
27018
  }
27194
- //#endregion
27195
- //#region packages/core/src/analyze/query-cluster.ts
27019
+ // packages/core/src/analyze/query-cluster.ts
27196
27020
  function* retainedQueryClusterRows(input) {
27197
27021
  for (const row of input.rows) {
27198
27022
  const query = row.keys[0] ?? "";
@@ -27299,15 +27123,13 @@ function buildQueryClusterReportFromRows(input) {
27299
27123
  recommendations: clusters.filter((cluster) => cluster.recommendation).slice(0, 5).map((cluster) => cluster.recommendation ?? "")
27300
27124
  };
27301
27125
  }
27302
- //#endregion
27303
- //#region packages/core/src/analyze/reports/dates.ts
27126
+ // packages/core/src/analyze/reports/dates.ts
27304
27127
  function rangeDays(range) {
27305
27128
  const start = Date.parse(`${range.startDate}T00:00:00Z`);
27306
27129
  const end = Date.parse(`${range.endDate}T00:00:00Z`);
27307
27130
  return Math.floor((end - start) / 864e5) + 1;
27308
27131
  }
27309
- //#endregion
27310
- //#region packages/core/src/analyze/reports/markdown.ts
27132
+ // packages/core/src/analyze/reports/markdown.ts
27311
27133
  function renderMarkdown(report) {
27312
27134
  const lines = [
27313
27135
  `# SEO report: ${report.site}`,
@@ -27331,8 +27153,7 @@ function renderMarkdown(report) {
27331
27153
  for (const priority of report.priorities) lines.push(`- ${priority.title} (${priority.confidence}): ${priority.action}`);
27332
27154
  return lines.join("\n");
27333
27155
  }
27334
- //#endregion
27335
- //#region packages/core/src/analyze/reports/sections.ts
27156
+ // packages/core/src/analyze/reports/sections.ts
27336
27157
  function skippedReason(report, section) {
27337
27158
  return report.skippedSections?.find((item) => item.section === section)?.reason;
27338
27159
  }
@@ -27467,8 +27288,7 @@ function contentOpportunityBullets(report) {
27467
27288
  if (!bullets.length) bullets.push("No material content opportunity stood out in this window. Keep monitoring, but do not force content edits from this report alone.");
27468
27289
  return bullets;
27469
27290
  }
27470
- //#endregion
27471
- //#region packages/core/src/analyze/reports/narrative.ts
27291
+ // packages/core/src/analyze/reports/narrative.ts
27472
27292
  function reportCaveats(input) {
27473
27293
  return [
27474
27294
  `Date window: ${input.period.startDate} to ${input.period.endDate}.`,
@@ -27550,8 +27370,7 @@ async function reportNarrative(input) {
27550
27370
  markdown: renderMarkdown(report)
27551
27371
  };
27552
27372
  }
27553
- //#endregion
27554
- //#region packages/core/src/analyze/second-page-analysis.ts
27373
+ // packages/core/src/analyze/second-page-analysis.ts
27555
27374
  const DEFAULT_MIN_IMPRESSIONS = 50;
27556
27375
  const MAX_MIN_IMPRESSIONS = 1e9;
27557
27376
  const DEFAULT_LIMIT = 10;
@@ -27755,8 +27574,7 @@ function analyzeSecondPageRows(input) {
27755
27574
  items
27756
27575
  };
27757
27576
  }
27758
- //#endregion
27759
- //#region packages/core/src/analyze/second-page-report.ts
27577
+ // packages/core/src/analyze/second-page-report.ts
27760
27578
  const MAX_SOURCE_ROWS = 1e5;
27761
27579
  function integerOption(input) {
27762
27580
  if (input.value === void 0) return input.fallback;
@@ -27973,8 +27791,7 @@ async function secondPageWith(input, dependencies) {
27973
27791
  warnings: uniqueWarnings(warnings)
27974
27792
  };
27975
27793
  }
27976
- //#endregion
27977
- //#region packages/core/src/analyze/second-page.ts
27794
+ // packages/core/src/analyze/second-page.ts
27978
27795
  const defaultDependencies = {
27979
27796
  searchAnalytics: querySearchAnalytics,
27980
27797
  fetch: fetchPage,
@@ -27984,8 +27801,7 @@ const defaultDependencies = {
27984
27801
  function secondPage(input, dependencies = defaultDependencies) {
27985
27802
  return secondPageWith(input, dependencies);
27986
27803
  }
27987
- //#endregion
27988
- //#region packages/core/src/analyze/workflows/priority-scoring.ts
27804
+ // packages/core/src/analyze/workflows/priority-scoring.ts
27989
27805
  const SOURCE_WEIGHT = {
27990
27806
  decay: 1.25,
27991
27807
  "quick-win": 1.15,
@@ -28054,8 +27870,7 @@ function scorePriority(input) {
28054
27870
  final: Number(final.toFixed(2))
28055
27871
  };
28056
27872
  }
28057
- //#endregion
28058
- //#region packages/core/src/analyze/workflows/report.ts
27873
+ // packages/core/src/analyze/workflows/report.ts
28059
27874
  function workflowReport(input) {
28060
27875
  return {
28061
27876
  workflow: input.workflow,
@@ -28067,8 +27882,7 @@ function workflowReport(input) {
28067
27882
  output: input.output
28068
27883
  };
28069
27884
  }
28070
- //#endregion
28071
- //#region packages/core/src/analyze/workflows/crawl-implementation-queue.ts
27885
+ // packages/core/src/analyze/workflows/crawl-implementation-queue.ts
28072
27886
  const CATEGORY_TO_PRIORITY = {
28073
27887
  canonical: "technical",
28074
27888
  content: "content",
@@ -28204,8 +28018,7 @@ async function crawlImplementationQueueWorkflow(input, dependencies) {
28204
28018
  }
28205
28019
  });
28206
28020
  }
28207
- //#endregion
28208
- //#region packages/core/src/analyze/workflows/diagnose-property.ts
28021
+ // packages/core/src/analyze/workflows/diagnose-property.ts
28209
28022
  async function diagnosePropertyWorkflow(input) {
28210
28023
  const narrative = await reportNarrative(input);
28211
28024
  const skippedCount = narrative.diagnosis.skippedSections?.length ?? 0;
@@ -28227,8 +28040,7 @@ async function diagnosePropertyWorkflow(input) {
28227
28040
  output: { narrative }
28228
28041
  });
28229
28042
  }
28230
- //#endregion
28231
- //#region packages/core/src/markdown.ts
28043
+ // packages/core/src/markdown.ts
28232
28044
  function tableCell(value) {
28233
28045
  return String(value ?? "-").replaceAll("\n", " ").replaceAll("|", "\\|");
28234
28046
  }
@@ -28255,8 +28067,7 @@ function renderPresentationTablesMarkdown(tables) {
28255
28067
  ];
28256
28068
  });
28257
28069
  }
28258
- //#endregion
28259
- //#region packages/core/src/analyze/workflows/presentation.ts
28070
+ // packages/core/src/analyze/workflows/presentation.ts
28260
28071
  function isRecord(value) {
28261
28072
  return typeof value === "object" && value !== null;
28262
28073
  }
@@ -28391,8 +28202,7 @@ function workflowPresentation(report, options = {}) {
28391
28202
  }] : []
28392
28203
  };
28393
28204
  }
28394
- //#endregion
28395
- //#region packages/core/src/analyze/workflows/markdown.ts
28205
+ // packages/core/src/analyze/workflows/markdown.ts
28396
28206
  function renderWorkflowMarkdown(report, options = {}) {
28397
28207
  const presentation = workflowPresentation(report, options);
28398
28208
  return [
@@ -28406,8 +28216,7 @@ function renderWorkflowMarkdown(report, options = {}) {
28406
28216
  ...renderPresentationTablesMarkdown(presentation.tables)
28407
28217
  ].join("\n").trim();
28408
28218
  }
28409
- //#endregion
28410
- //#region packages/core/src/analyze/workflows/priority-grouping.ts
28219
+ // packages/core/src/analyze/workflows/priority-grouping.ts
28411
28220
  const GROUPABLE_SOURCES = /* @__PURE__ */ new Set(["quick-win", "striking-distance"]);
28412
28221
  function queueTemplateFamilyId(template) {
28413
28222
  if (!template) return "no-template";
@@ -28503,8 +28312,7 @@ function groupBy(items, keyFor) {
28503
28312
  function groupPriorityQueue(items) {
28504
28313
  return groupBy(groupBy(items, targetGroupKey), groupKey).sort((a, b) => b.score - a.score);
28505
28314
  }
28506
- //#endregion
28507
- //#region packages/core/src/analyze/workflows/refresh-priorities.ts
28315
+ // packages/core/src/analyze/workflows/refresh-priorities.ts
28508
28316
  function templateCount(input) {
28509
28317
  const template = input.templates?.find((item) => item.id === input.id);
28510
28318
  return template?.urlCount ?? template?.count;
@@ -28711,8 +28519,7 @@ async function refreshPrioritiesWorkflow(input) {
28711
28519
  }
28712
28520
  });
28713
28521
  }
28714
- //#endregion
28715
- //#region packages/core/src/analyze/workflows/segments.ts
28522
+ // packages/core/src/analyze/workflows/segments.ts
28716
28523
  function compareText$3(left, right) {
28717
28524
  return left < right ? -1 : left > right ? 1 : 0;
28718
28525
  }
@@ -28740,8 +28547,7 @@ function splitSegments(report) {
28740
28547
  losers
28741
28548
  };
28742
28549
  }
28743
- //#endregion
28744
- //#region packages/core/src/analyze/workflows/update-postmortem.ts
28550
+ // packages/core/src/analyze/workflows/update-postmortem.ts
28745
28551
  function formatMagnitude(value) {
28746
28552
  return Math.abs(value).toLocaleString("en-GB");
28747
28553
  }
@@ -29013,8 +28819,7 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
29013
28819
  }
29014
28820
  });
29015
28821
  }
29016
- //#endregion
29017
- //#region packages/core/src/brand-detect.ts
28822
+ // packages/core/src/brand-detect.ts
29018
28823
  function defaultDateRange(days = 28) {
29019
28824
  return finalGscDateRange(days);
29020
28825
  }
@@ -29081,8 +28886,7 @@ async function detectBrandTerms(input) {
29081
28886
  candidates: ranked
29082
28887
  };
29083
28888
  }
29084
- //#endregion
29085
- //#region packages/core/src/clicky/api.ts
28889
+ // packages/core/src/clicky/api.ts
29086
28890
  const ENDPOINT$1 = "https://api.clicky.com/api/stats/4";
29087
28891
  const MAX_BYTES$3 = 5e6;
29088
28892
  var ClickyError = class extends Error {
@@ -29136,8 +28940,7 @@ async function clickyDomain(site) {
29136
28940
  return null;
29137
28941
  }
29138
28942
  }
29139
- //#endregion
29140
- //#region packages/core/src/analyze/ai-queries.ts
28943
+ // packages/core/src/analyze/ai-queries.ts
29141
28944
  const RULES = [
29142
28945
  ["agent_prompt", "^(search the web for|browse the web|find sources)|do not invent|return the [0-9]+ most relevant|include source urls|context: location|do not include location|question: |reply only with|the model you are using"],
29143
28946
  ["tracker", "\\. ?my location is |\\?\\. ?my( |$)|^evaluate the .+ on "],
@@ -29179,7 +28982,7 @@ function aiQueryKind(query) {
29179
28982
  return MATCHERS.find(([, pattern]) => pattern.test(text))?.[0] ?? null;
29180
28983
  }
29181
28984
  const STOP = new Set("the and for with that this what which who how why when where are is was can does do did you your our from about into than then them they their there have has had will would should could not but its it’s me my i am of to in on at by or an as be if so".split(" "));
29182
- function words(query) {
28985
+ function words$1(query) {
29183
28986
  return new Set(query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3 && !STOP.has(word)).map((word) => word.length > 4 && word.endsWith("s") ? word.slice(0, -1) : word));
29184
28987
  }
29185
28988
  const SAME_QUESTION = .6;
@@ -29222,7 +29025,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29222
29025
  const rows = [...matched.rows].sort((a, b) => b.impressions - a.impressions || String(a.keys[0]).localeCompare(String(b.keys[0])) || String(a.keys[1]).localeCompare(String(b.keys[1])));
29223
29026
  const usage = /* @__PURE__ */ new Map();
29224
29027
  const unique = new Set(rows.map((row) => String(row.keys[0] ?? "")));
29225
- for (const query of unique) for (const word of words(query)) usage.set(word, (usage.get(word) ?? 0) + 1);
29028
+ for (const query of unique) for (const word of words$1(query)) usage.set(word, (usage.get(word) ?? 0) + 1);
29226
29029
  const weight = (word) => Math.log((unique.size + 1) / (usage.get(word) ?? 1));
29227
29030
  const groups = [];
29228
29031
  const byWord = /* @__PURE__ */ new Map();
@@ -29242,7 +29045,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29242
29045
  continue;
29243
29046
  }
29244
29047
  const text = query.trim().toLowerCase();
29245
- const own = words(query);
29048
+ const own = words$1(query);
29246
29049
  let group = byText.get(text);
29247
29050
  if (!group) {
29248
29051
  let checked = 0;
@@ -29357,8 +29160,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29357
29160
  ]
29358
29161
  };
29359
29162
  }
29360
- //#endregion
29361
- //#region packages/core/src/commands/research-providers.ts
29163
+ // packages/core/src/commands/research-providers.ts
29362
29164
  function need(headers, provider) {
29363
29165
  const values = provider === "dataforseo" ? [headers.get("x-dataforseo-login"), headers.get("x-dataforseo-password")] : [headers.get("x-ahrefs-key")];
29364
29166
  if (values.some((value) => !value)) throw new CommandError("NO_KEY", `No ${provider} key. Hosted MCP: save it at https://seoskill.dev/account#keys. CLI and local MCP: run \`seo login\` to use the account's keys, or \`seo providers ${provider} connect\`.`);
@@ -29950,8 +29752,7 @@ async function runAiAnswers(input, headers, deps, more) {
29950
29752
  }
29951
29753
  };
29952
29754
  }
29953
- //#endregion
29954
- //#region packages/core/src/commands/analytics.ts
29755
+ // packages/core/src/commands/analytics.ts
29955
29756
  async function analyticsSignIn(accounts, property, signIn) {
29956
29757
  const only = accounts.length === 1 ? accounts[0] : void 0;
29957
29758
  if (only) return only.google;
@@ -30013,8 +29814,7 @@ async function runAnalyticsRows(input, google, options) {
30013
29814
  }
30014
29815
  };
30015
29816
  }
30016
- //#endregion
30017
- //#region packages/core/src/ga4/matching.ts
29817
+ // packages/core/src/ga4/matching.ts
30018
29818
  function hostnameFromUrl(value) {
30019
29819
  try {
30020
29820
  const url = new URL(value);
@@ -30056,8 +29856,7 @@ function ga4MatchReason(match, site) {
30056
29856
  const location = match.match === "domain" ? "is inside" : "matches";
30057
29857
  return `Matched Google Analytics web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
30058
29858
  }
30059
- //#endregion
30060
- //#region packages/core/src/commands/analytics-site.ts
29859
+ // packages/core/src/commands/analytics-site.ts
30061
29860
  const STREAM_CHECKS = 30;
30062
29861
  const AT_ONCE = 5;
30063
29862
  const TIED = 3;
@@ -30363,8 +30162,7 @@ async function analyticsBeside(access, site, range, before, pages) {
30363
30162
  };
30364
30163
  }
30365
30164
  }
30366
- //#endregion
30367
- //#region packages/core/src/commands/audit.ts
30165
+ // packages/core/src/commands/audit.ts
30368
30166
  const AUDIT_SECTION = "weekly-audit";
30369
30167
  const AUDIT_SECTION_TEXT = "What the weekly crawl of the site found (broken and redirected internal links, missing or duplicate titles, descriptions and H1s, noindex, canonicals to another page, sitemap URLs that do not return 200, thin pages, tags that JavaScript changes), with what is new, back or resolved since the crawl before. Only for sites added to the weekly site audit beta.";
30370
30168
  const AUDIT_RULES = {
@@ -30382,18 +30180,18 @@ const AUDIT_RULES = {
30382
30180
  thin_page: "Pages under 100 words (heuristic)",
30383
30181
  js_changed_tags: "Pages whose raw HTML has another title, canonical or robots tag than the page after JavaScript runs"
30384
30182
  };
30385
- const PAGE_SIZE$1 = 50;
30386
- const SUMMARY_ROWS = 20;
30387
- const day$1 = (ms) => new Date(ms).toISOString().slice(0, 10);
30388
- function relative$2(value, origin) {
30183
+ const PAGE_SIZE$2 = 50;
30184
+ const SUMMARY_ROWS$1 = 20;
30185
+ const day$2 = (ms) => new Date(ms).toISOString().slice(0, 10);
30186
+ function relative$3(value, origin) {
30389
30187
  return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
30390
30188
  }
30391
30189
  function evidence(finding, origin) {
30392
30190
  const details = JSON.parse(finding.details);
30393
30191
  const parts = [];
30394
30192
  if (typeof details.status === "number") parts.push(`${details.status}`);
30395
- if (typeof details.location === "string") parts.push(`to ${relative$2(details.location, origin)}`);
30396
- if (typeof details.canonical === "string") parts.push(`canonical ${relative$2(details.canonical, origin)}`);
30193
+ if (typeof details.location === "string") parts.push(`to ${relative$3(details.location, origin)}`);
30194
+ if (typeof details.canonical === "string") parts.push(`canonical ${relative$3(details.canonical, origin)}`);
30397
30195
  if (typeof details.words === "number") parts.push(`${details.words} words`);
30398
30196
  if (details.raw && typeof details.raw === "object") {
30399
30197
  const raw = details.raw;
@@ -30403,13 +30201,13 @@ function evidence(finding, origin) {
30403
30201
  "robots"
30404
30202
  ]) if (key in raw) {
30405
30203
  const value = raw[key];
30406
- parts.push(`raw ${key} ${typeof value === "string" ? key === "canonical" ? relative$2(value, origin) : value : "none"}`);
30204
+ parts.push(`raw ${key} ${typeof value === "string" ? key === "canonical" ? relative$3(value, origin) : value : "none"}`);
30407
30205
  }
30408
30206
  }
30409
30207
  const linking = typeof details.linking_pages === "number";
30410
30208
  const count = linking ? details.linking_pages : details.pages;
30411
30209
  if (typeof count === "number") {
30412
- const shown = (Array.isArray(details.examples) ? details.examples.map((url) => relative$2(url, origin)) : []).slice(0, 3);
30210
+ const shown = (Array.isArray(details.examples) ? details.examples.map((url) => relative$3(url, origin)) : []).slice(0, 3);
30413
30211
  const noun = `${linking ? "linking page" : "page"}${count === 1 ? "" : "s"}`;
30414
30212
  parts.push(`${count} ${noun}${shown.length ? ` (${shown.join(", ")}${count > shown.length ? ", and more" : ""})` : ""}`);
30415
30213
  }
@@ -30419,13 +30217,13 @@ function state(finding, since) {
30419
30217
  if (finding.state === "resolved") return "resolved";
30420
30218
  if (finding.reopened_at && finding.reopened_at >= since) return "back";
30421
30219
  if (finding.first_seen_at >= since) return "new";
30422
- return `open since ${day$1(finding.first_seen_at)}`;
30220
+ return `open since ${day$2(finding.first_seen_at)}`;
30423
30221
  }
30424
30222
  function table(rows, origin, since) {
30425
30223
  return ["state | finding | subject | evidence", ...rows.map((finding) => [
30426
30224
  state(finding, since),
30427
30225
  finding.rule,
30428
- relative$2(finding.subject, origin),
30226
+ relative$3(finding.subject, origin),
30429
30227
  evidence(finding, origin)
30430
30228
  ].join(" | "))].join("\n");
30431
30229
  }
@@ -30490,8 +30288,8 @@ function runAuditReport(input, snapshot, call) {
30490
30288
  const pages = summary.pages;
30491
30289
  const coverage = summary.complete ? `${pages.fetched} pages, the whole site the crawl could reach` : summary.status === "failed" ? `${pages.fetched} pages before the crawl stopped on errors, so it is partial` : `${pages.fetched} pages; it stopped at the 5,000-page limit${pages.links_past_cap ? ` with ${pages.links_past_cap} more links past it` : ""}, so it is partial`;
30492
30290
  const head = [
30493
- `Weekly site audit of ${site}, crawled ${day$1(snapshot.finished_at)}: ${coverage}.${pages.blocked ? ` ${pages.blocked === 1 ? "1 URL" : `${pages.blocked} URLs`} blocked by robots.txt ${pages.blocked === 1 ? "was" : "were"} not fetched.` : ""}`,
30494
- previous ? `Since the crawl of ${day$1(previous.finished_at)}: ${summary.findings.new} new, ${summary.findings.reopened} back, ${summary.findings.resolved} resolved. ${summary.findings.open} open now.` : `This is the first crawl, so every finding is new: ${summary.findings.open} open.`,
30291
+ `Weekly site audit of ${site}, crawled ${day$2(snapshot.finished_at)}: ${coverage}.${pages.blocked ? ` ${pages.blocked === 1 ? "1 URL" : `${pages.blocked} URLs`} blocked by robots.txt ${pages.blocked === 1 ? "was" : "were"} not fetched.` : ""}`,
30292
+ previous ? `Since the crawl of ${day$2(previous.finished_at)}: ${summary.findings.new} new, ${summary.findings.reopened} back, ${summary.findings.resolved} resolved. ${summary.findings.open} open now.` : `This is the first crawl, so every finding is new: ${summary.findings.open} open.`,
30495
30293
  renderingText(summary.rendering),
30496
30294
  summary.findings.resolution === "crawled-pages-only" ? summary.complete ? "Because some pages could not be rendered, a finding counts as resolved only when this crawl rendered its page; a finding not seen here is not an all-clear." : "Because the crawl is partial, a finding counts as resolved only when this crawl fetched its page again; a finding not seen here is not an all-clear." : null,
30497
30295
  summary.findings.capped_rules.length ? `Capped at 1,000 findings each, so these counts are lower bounds: ${summary.findings.capped_rules.join(", ")}.` : null,
@@ -30503,8 +30301,8 @@ function runAuditReport(input, snapshot, call) {
30503
30301
  "change | url | date | reason now | top page",
30504
30302
  ...index.rows.map((row) => [
30505
30303
  row.change === "dropped" ? "dropped out" : "back in",
30506
- relative$2(row.url, origin),
30507
- day$1(row.at),
30304
+ relative$3(row.url, origin),
30305
+ day$2(row.at),
30508
30306
  row.coverage ?? "",
30509
30307
  row.top ? "yes" : "no"
30510
30308
  ].join(" | ")),
@@ -30512,7 +30310,7 @@ function runAuditReport(input, snapshot, call) {
30512
30310
  ].filter(Boolean).join("\n") : null;
30513
30311
  if (detail === "rows") {
30514
30312
  const offset = input.offset ?? 0;
30515
- const slice = findings.slice(offset, offset + PAGE_SIZE$1);
30313
+ const slice = findings.slice(offset, offset + PAGE_SIZE$2);
30516
30314
  const end = offset + slice.length;
30517
30315
  return {
30518
30316
  text: [
@@ -30546,7 +30344,7 @@ function runAuditReport(input, snapshot, call) {
30546
30344
  head,
30547
30345
  indexText,
30548
30346
  byRule.length ? ["finding | open | what it is", ...byRule.map(([rule, count]) => `${rule} | ${count} | ${AUDIT_RULES[rule] ?? rule}`)].join("\n") : "No findings in this crawl.",
30549
- fresh.length ? `New, back or resolved this week, 1 to ${Math.min(SUMMARY_ROWS, fresh.length)} of ${fresh.length}:\n${table(fresh.slice(0, SUMMARY_ROWS), origin, since)}` : previous ? "Nothing new, back or resolved since the crawl before." : null,
30347
+ fresh.length ? `New, back or resolved this week, 1 to ${Math.min(SUMMARY_ROWS$1, fresh.length)} of ${fresh.length}:\n${table(fresh.slice(0, SUMMARY_ROWS$1), origin, since)}` : previous ? "Nothing new, back or resolved since the crawl before." : null,
30550
30348
  changes.compared ? `Between the two crawls: ${changes.new_urls} new URLs, ${changes.gone_urls} gone, ${changes.status_changed} changed status, ${changes.title_changed} changed title.` : null,
30551
30349
  `More: ${call("report", {
30552
30350
  site,
@@ -30560,7 +30358,7 @@ function runAuditReport(input, snapshot, call) {
30560
30358
  site,
30561
30359
  finished_at: snapshot.finished_at,
30562
30360
  summary,
30563
- fresh: fresh.slice(0, SUMMARY_ROWS)
30361
+ fresh: fresh.slice(0, SUMMARY_ROWS$1)
30564
30362
  }
30565
30363
  };
30566
30364
  }
@@ -30568,12 +30366,12 @@ function auditPreview(snapshot) {
30568
30366
  if (!snapshot?.summary || snapshot.finished_at === null) return snapshot ? "The first weekly crawl has not finished." : null;
30569
30367
  const { findings } = snapshot.summary;
30570
30368
  const index = snapshot.index_changes;
30571
- return `${index && (index.dropped || index.returned) ? `${index.dropped} URLs dropped out of Google's index this week and ${index.returned} came back. ` : ""}Last crawl ${day$1(snapshot.finished_at)}: ${findings.open} open findings, ${findings.new + findings.reopened} new or back, ${findings.resolved} resolved.`;
30369
+ return `${index && (index.dropped || index.returned) ? `${index.dropped} URLs dropped out of Google's index this week and ${index.returned} came back. ` : ""}Last crawl ${day$2(snapshot.finished_at)}: ${findings.open} open findings, ${findings.new + findings.reopened} new or back, ${findings.resolved} resolved.`;
30572
30370
  }
30573
30371
  const WEEK_MS = 6048e5;
30574
- const day = (ms) => ms === null ? "" : new Date(ms).toISOString().slice(0, 10);
30372
+ const day$1 = (ms) => ms === null ? "" : new Date(ms).toISOString().slice(0, 10);
30575
30373
  const count$2 = (value) => value.toLocaleString("en-US");
30576
- function relative$1(value, origin) {
30374
+ function relative$2(value, origin) {
30577
30375
  if (!value) return "";
30578
30376
  return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
30579
30377
  }
@@ -30587,15 +30385,15 @@ function rowsTable(rows, checks) {
30587
30385
  const since = checks.as_of - WEEK_MS;
30588
30386
  const changed = (row) => row.changed_at !== null && row.changed_at > since;
30589
30387
  return ["url | index change | reason now | before | changed | checked | clicks (28 days before) | in_sitemap | google_canonical | error", ...rows.map((row) => [
30590
- relative$1(row.url, checks.origin),
30591
- row.transition && row.transition_at !== null && row.transition_at > since ? `${row.transition === "dropped" ? "dropped out" : "back in"} ${day(row.transition_at)}` : "",
30388
+ relative$2(row.url, checks.origin),
30389
+ row.transition && row.transition_at !== null && row.transition_at > since ? `${row.transition === "dropped" ? "dropped out" : "back in"} ${day$1(row.transition_at)}` : "",
30592
30390
  row.coverage ?? (row.checked_at === null ? "not checked yet" : ""),
30593
30391
  changed(row) ? row.prev_coverage ?? "" : "",
30594
- changed(row) ? day(row.changed_at) : "",
30595
- day(row.checked_at),
30392
+ changed(row) ? day$1(row.changed_at) : "",
30393
+ day$1(row.checked_at),
30596
30394
  `${count$2(row.clicks)} (${count$2(row.previous_clicks)})${row.lost ? " lost" : ""}`,
30597
30395
  row.in_sitemap ? "yes" : "no",
30598
- row.google_canonical && row.google_canonical !== row.url ? relative$1(row.google_canonical, checks.origin) : "",
30396
+ row.google_canonical && row.google_canonical !== row.url ? relative$2(row.google_canonical, checks.origin) : "",
30599
30397
  row.error ?? ""
30600
30398
  ].join(" | "))].join("\n");
30601
30399
  }
@@ -30677,8 +30475,7 @@ function runAuditChecks(input, checks, call) {
30677
30475
  }
30678
30476
  };
30679
30477
  }
30680
- //#endregion
30681
- //#region packages/core/src/commands/bing.ts
30478
+ // packages/core/src/commands/bing.ts
30682
30479
  const BING_LIMITS = ["Bing and Google count clicks, impressions and positions their own way. Their numbers sit side by side here and are never added together.", "Bing lists only its top queries and pages, grouped by week. A query or page missing from Bing’s list is unknown, not zero."];
30683
30480
  const CLI_LINK = "If you have a shell, link the CLI to the same account once: run `npx -y seo login`, show the person the link it prints, and after they approve it run `npx -y seo login` again. `npx -y seo` commands then use the same Google accounts, Bing and keys.";
30684
30481
  const LOCAL_BING = `Bing blocks the hosted server, so Bing data comes from the CLI on a machine. ${CLI_LINK} Connect Bing on the account page if it is not connected, then run \`npx -y seo report --site <site> --section bing --json\`.`;
@@ -30994,8 +30791,7 @@ function bingOverviewText(report, heading, origin, rowsPerList) {
30994
30791
  `Limits:\n${report.caveats.map((line) => `- ${line}`).join("\n")}`
30995
30792
  ].filter(Boolean).join("\n\n");
30996
30793
  }
30997
- //#endregion
30998
- //#region packages/core/src/commands/clicky.ts
30794
+ // packages/core/src/commands/clicky.ts
30999
30795
  const CLICKY_KEY_PREFIX = "x-clicky-";
31000
30796
  function clickySites(keys) {
31001
30797
  const sites = [];
@@ -31149,8 +30945,7 @@ async function runClickyRows(input, keys, options) {
31149
30945
  }
31150
30946
  };
31151
30947
  }
31152
- //#endregion
31153
- //#region packages/core/src/commands/domain-rating.ts
30948
+ // packages/core/src/commands/domain-rating.ts
31154
30949
  const DR_ATTRIBUTION = "domain_rating: Domain Rating by Ahrefs (https://ahrefs.com/).";
31155
30950
  const ENDPOINT = "https://api.ahrefs.com/v3/public/domain-rating-free";
31156
30951
  const MAX_DOMAINS = 25;
@@ -31411,9 +31206,9 @@ function indexingRow(e, sources, inspectedAt) {
31411
31206
  ...live.first.error ? { live_error: live.first.error } : {}
31412
31207
  };
31413
31208
  }
31414
- const byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31209
+ const byText$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31415
31210
  function sortRows(rows) {
31416
- return [...rows].sort((a, b) => byText(a.reason, b.reason) || byText(a.url, b.url));
31211
+ return [...rows].sort((a, b) => byText$1(a.reason, b.reason) || byText$1(a.url, b.url));
31417
31212
  }
31418
31213
  function byReason(rows) {
31419
31214
  const groups = /* @__PURE__ */ new Map();
@@ -31427,7 +31222,7 @@ function byReason(rows) {
31427
31222
  counts[row.label] += 1;
31428
31223
  groups.set(row.reason, counts);
31429
31224
  }
31430
- return [...groups].sort((a, b) => byText(a[0], b[0])).map(([reason, counts]) => ({
31225
+ return [...groups].sort((a, b) => byText$1(a[0], b[0])).map(([reason, counts]) => ({
31431
31226
  reason,
31432
31227
  urls: counts.fix + counts.check + counts.expected + counts.indexed,
31433
31228
  ...counts
@@ -31441,7 +31236,7 @@ function knownUrls(input) {
31441
31236
  if (!sources.includes(source)) sources.push(source);
31442
31237
  found.set(url, sources);
31443
31238
  };
31444
- const pages = input.pages.filter((page) => page.impressions > 0 && !page.url.includes("#")).sort((a, b) => b.clicks - a.clicks || b.impressions - a.impressions || byText(a.url, b.url));
31239
+ const pages = input.pages.filter((page) => page.impressions > 0 && !page.url.includes("#")).sort((a, b) => b.clicks - a.clicks || b.impressions - a.impressions || byText$1(a.url, b.url));
31445
31240
  for (const url of input.lost) add(url, "lost_clicks");
31446
31241
  for (const page of pages) add(page.url, "search_console");
31447
31242
  for (const url of input.sitemap.urls) add(url, "sitemap");
@@ -31532,7 +31327,7 @@ async function inspectKnown(site, known, deps, asked = 100) {
31532
31327
  inspected: rows.length,
31533
31328
  fromSaved: reused.length,
31534
31329
  oldestSaved: oldest,
31535
- failed: failed.sort((a, b) => byText(a.url, b.url)),
31330
+ failed: failed.sort((a, b) => byText$1(a.url, b.url)),
31536
31331
  stoppedBy: quotaStop || quotaShort ? "quota" : known.urls.length > wanted.length ? "limit" : null,
31537
31332
  quota
31538
31333
  };
@@ -31599,8 +31394,441 @@ function indexingText(site, run, page, origin, askMore, now) {
31599
31394
  if (!page.summary && more) lines.push(`More rows: ${page.more(end)}.`);
31600
31395
  return lines;
31601
31396
  }
31602
- //#endregion
31603
- //#region packages/core/src/commands/page-facts.ts
31397
+ // packages/core/src/commands/issues.ts
31398
+ const ISSUES_SECTION = "issues";
31399
+ const ISSUES_SECTION_TEXT = "A list kept for the site between sessions: issues the striking-distance, low-ctr and quick-wins checks found (and the weekly crawl and Google index checks for an audited site), each with its id, the latest check and what the person marked it (open, fixed or ignored). Asking for it runs those checks again. To mark issues, add mark and issues instead of section. The hosted server keeps the list only for sites in the weekly site audit; for any other site it returns the list as JSON for the agent to save and pass back as saved.";
31400
+ const ISSUE_MARKS = [
31401
+ "open",
31402
+ "fixed",
31403
+ "ignored"
31404
+ ];
31405
+ const SAVED_MAX = 6e5;
31406
+ const SUBJECT_MAX = 300;
31407
+ const EVIDENCE_MAX = 200;
31408
+ const PAGE_SIZE$1 = 50;
31409
+ const SUMMARY_ROWS = 20;
31410
+ const SEARCH_CHECKS = [
31411
+ "striking-distance",
31412
+ "low-ctr",
31413
+ "quick-wins"
31414
+ ];
31415
+ function issueId(kind, subject) {
31416
+ let hash = 2166136261;
31417
+ for (const char of `${kind}\n${subject}`) {
31418
+ hash ^= char.codePointAt(0) ?? 0;
31419
+ hash = Math.imul(hash, 16777619) >>> 0;
31420
+ }
31421
+ return hash.toString(16).padStart(8, "0");
31422
+ }
31423
+ const clip = (value, max) => value.length > max ? `${value.slice(0, max - 1)}…` : value;
31424
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
31425
+ const pctText = (value) => {
31426
+ const n = num(value);
31427
+ return n === null ? null : `${Math.round(n * 1e3) / 10}%`;
31428
+ };
31429
+ function searchEvidence(item) {
31430
+ const position = num(item.position);
31431
+ const ctr = pctText(item.actualCtr ?? item.ctr);
31432
+ const expected = pctText(item.expectedCtr ?? item.targetCtr);
31433
+ return [
31434
+ position === null ? null : `position ${Math.round(position * 10) / 10}`,
31435
+ num(item.impressions) === null ? null : `${item.impressions} impressions`,
31436
+ num(item.clicks) === null ? null : `${item.clicks} clicks`,
31437
+ ctr ? `CTR ${ctr}${expected ? ` against ${expected} expected` : ""}` : null
31438
+ ].filter(Boolean).join(", ");
31439
+ }
31440
+ function searchComplete(report) {
31441
+ const status = report.dataStatus;
31442
+ if (status === "partial" || status === "unavailable") return `data ${status}`;
31443
+ if (report.source?.possiblyTruncated) return "Search Console rows possibly cut short";
31444
+ const selection = report.selection ?? {};
31445
+ const limited = Object.entries(selection).find(([key, value]) => key.startsWith("limited") && Number(value) > 0);
31446
+ if (limited) return `capped, ${limited[1]} more rows not returned`;
31447
+ return null;
31448
+ }
31449
+ function searchRun(kind, report) {
31450
+ const base = {
31451
+ kind,
31452
+ covers: (k) => k === kind
31453
+ };
31454
+ if (report instanceof Error) return {
31455
+ ...base,
31456
+ found: [],
31457
+ complete: false,
31458
+ addLimit: 0,
31459
+ status: `${kind}: could not run (${clip(report.message, 120)})`
31460
+ };
31461
+ const found = (report.items ?? []).filter((item) => typeof item.query === "string" && typeof item.url === "string").map((item) => ({
31462
+ kind,
31463
+ issue: {
31464
+ subject: `"${item.query}" on ${item.url}`,
31465
+ evidence: searchEvidence(item)
31466
+ }
31467
+ }));
31468
+ const short = searchComplete(report);
31469
+ return {
31470
+ ...base,
31471
+ found,
31472
+ complete: !short,
31473
+ addLimit: 10,
31474
+ status: `${kind}: ${found.length} ${found.length === 1 ? "row" : "rows"}${short ? ` (${short}, so an issue it did not return is not checked)` : ""}`
31475
+ };
31476
+ }
31477
+ function auditEvidence(details) {
31478
+ try {
31479
+ const parsed = JSON.parse(details);
31480
+ return Object.entries(parsed).filter(([, value]) => ["string", "number"].includes(typeof value)).map(([key, value]) => `${key} ${value}`).join(", ");
31481
+ } catch {
31482
+ return "";
31483
+ }
31484
+ }
31485
+ const AUDIT_KIND = "weekly-audit:";
31486
+ const INDEX_KIND = "index-drop";
31487
+ function auditRuns(snapshot) {
31488
+ const crawl = (k) => k.startsWith(AUDIT_KIND);
31489
+ const index = (k) => k === INDEX_KIND;
31490
+ if (snapshot === void 0) return [];
31491
+ const summary = snapshot?.summary;
31492
+ if (!snapshot || !summary || snapshot.finished_at === null) {
31493
+ const why = snapshot ? "the first crawl has not finished" : "the site is not in the weekly site audit";
31494
+ return [{
31495
+ kind: "weekly-audit",
31496
+ covers: crawl,
31497
+ found: [],
31498
+ complete: false,
31499
+ addLimit: 0,
31500
+ status: `weekly-audit: not checked, ${why}`
31501
+ }, {
31502
+ kind: INDEX_KIND,
31503
+ covers: index,
31504
+ found: [],
31505
+ complete: false,
31506
+ addLimit: 0,
31507
+ status: `${INDEX_KIND}: not checked, ${why}`
31508
+ }];
31509
+ }
31510
+ const open = snapshot.findings.filter((f) => f.state === "open");
31511
+ const gone = new Set(snapshot.findings.filter((f) => f.state === "resolved").map((f) => issueId(`${AUDIT_KIND}${f.rule}`, f.subject)));
31512
+ const capped = new Set(summary.findings.capped_rules.map((rule) => `${AUDIT_KIND}${rule}`));
31513
+ const complete = summary.complete && summary.findings.resolution === "all";
31514
+ const day = new Date(snapshot.finished_at).toISOString().slice(0, 10);
31515
+ const runs = [{
31516
+ kind: "weekly-audit",
31517
+ covers: crawl,
31518
+ found: open.map((f) => ({
31519
+ kind: `${AUDIT_KIND}${f.rule}`,
31520
+ issue: {
31521
+ subject: f.subject,
31522
+ evidence: auditEvidence(f.details)
31523
+ }
31524
+ })),
31525
+ complete,
31526
+ addLimit: 50,
31527
+ gone,
31528
+ partialKinds: capped,
31529
+ status: `weekly-audit: crawl of ${day}, ${open.length} open findings${complete ? "" : " (partial crawl, so a finding it did not see again is not checked)"}${capped.size ? `; capped rules, not checked when absent: ${[...capped].map((k) => k.slice(13)).join(", ")}` : ""}`
31530
+ }];
31531
+ const changes = snapshot.index_changes;
31532
+ if (changes) {
31533
+ const dropped = changes.rows.filter((row) => row.change === "dropped");
31534
+ runs.push({
31535
+ kind: INDEX_KIND,
31536
+ covers: index,
31537
+ found: dropped.map((row) => ({
31538
+ kind: INDEX_KIND,
31539
+ issue: {
31540
+ subject: row.url,
31541
+ evidence: `dropped out ${new Date(row.at).toISOString().slice(0, 10)}${row.coverage ? `, ${row.coverage}` : ""}${row.top ? ", top page" : ""}`
31542
+ }
31543
+ })),
31544
+ complete: false,
31545
+ addLimit: dropped.length,
31546
+ gone: new Set(changes.rows.filter((row) => row.change === "returned").map((row) => issueId(INDEX_KIND, row.url))),
31547
+ status: `${INDEX_KIND}: ${changes.dropped} dropped out of Google's index and ${changes.returned} came back in the last 7 days${changes.rows.length < changes.dropped + changes.returned ? `, ${changes.rows.length} rows read` : ""}`
31548
+ });
31549
+ }
31550
+ return runs;
31551
+ }
31552
+ function mergeIssues(stored, runs, now) {
31553
+ const byId = new Map(stored.map((issue) => [issue.id, { ...issue }]));
31554
+ for (const run of runs) {
31555
+ const seenIds = /* @__PURE__ */ new Set();
31556
+ let added = 0;
31557
+ for (const { kind, issue } of run.found) {
31558
+ const id = issueId(kind, issue.subject);
31559
+ if (seenIds.has(id)) continue;
31560
+ seenIds.add(id);
31561
+ const evidence = clip(issue.evidence, EVIDENCE_MAX);
31562
+ const existing = byId.get(id);
31563
+ if (existing) {
31564
+ Object.assign(existing, {
31565
+ evidence,
31566
+ last_found: now,
31567
+ seen: "found",
31568
+ checked_at: now
31569
+ });
31570
+ continue;
31571
+ }
31572
+ if (added >= run.addLimit) continue;
31573
+ added += 1;
31574
+ byId.set(id, {
31575
+ id,
31576
+ kind,
31577
+ subject: clip(issue.subject, SUBJECT_MAX),
31578
+ evidence,
31579
+ mark: "open",
31580
+ marked_at: null,
31581
+ note: null,
31582
+ first_found: now,
31583
+ last_found: now,
31584
+ seen: "found",
31585
+ checked_at: now
31586
+ });
31587
+ }
31588
+ for (const issue of byId.values()) {
31589
+ if (!run.covers(issue.kind) || seenIds.has(issue.id)) continue;
31590
+ if (run.gone?.has(issue.id)) Object.assign(issue, {
31591
+ seen: "not-found",
31592
+ checked_at: now
31593
+ });
31594
+ else if (run.complete && !run.partialKinds?.has(issue.kind)) Object.assign(issue, {
31595
+ seen: "not-found",
31596
+ checked_at: now
31597
+ });
31598
+ else issue.seen = "not-checked";
31599
+ }
31600
+ }
31601
+ return prune([...byId.values()]);
31602
+ }
31603
+ function prune(issues) {
31604
+ if (issues.length <= 500) return sortIssues(issues);
31605
+ return sortIssues([...issues].sort((a, b) => Number(b.mark !== "open") - Number(a.mark !== "open") || (b.last_found ?? "").localeCompare(a.last_found ?? "") || a.id.localeCompare(b.id)).slice(0, 500));
31606
+ }
31607
+ const MARK_ORDER = {
31608
+ open: 0,
31609
+ fixed: 1,
31610
+ ignored: 2
31611
+ };
31612
+ const SEEN_ORDER = {
31613
+ found: 0,
31614
+ "not-checked": 1,
31615
+ "not-found": 2
31616
+ };
31617
+ function sortIssues(issues) {
31618
+ return [...issues].sort((a, b) => MARK_ORDER[a.mark] - MARK_ORDER[b.mark] || SEEN_ORDER[a.seen] - SEEN_ORDER[b.seen] || a.kind.localeCompare(b.kind) || (a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : 0) || a.id.localeCompare(b.id));
31619
+ }
31620
+ const day = (iso) => iso ? iso.slice(0, 10) : "";
31621
+ function relative$1(subject, origin) {
31622
+ return subject.split(origin).join("") || subject;
31623
+ }
31624
+ function issuesTable(rows, origin) {
31625
+ return ["id | mark | latest check | kind | subject | evidence when last found | first found", ...rows.map((issue) => [
31626
+ issue.id,
31627
+ issue.mark === "open" ? "open" : `${issue.mark} ${day(issue.marked_at)}${issue.note ? ` (${issue.note})` : ""}`,
31628
+ issue.seen === "not-checked" ? `not checked${issue.checked_at ? `, last ${day(issue.checked_at)}` : ""}` : `${issue.seen === "found" ? "found" : "not found"} ${day(issue.checked_at)}`,
31629
+ issue.kind,
31630
+ relative$1(issue.subject, origin),
31631
+ issue.evidence,
31632
+ day(issue.first_found)
31633
+ ].join(" | "))].join("\n");
31634
+ }
31635
+ function counts(issues) {
31636
+ const by = (mark) => issues.filter((i) => i.mark === mark).length;
31637
+ return {
31638
+ open: by("open"),
31639
+ fixed: by("fixed"),
31640
+ ignored: by("ignored")
31641
+ };
31642
+ }
31643
+ function issuesPreview(issues, call, site) {
31644
+ const more = `${call("report", {
31645
+ site,
31646
+ section: ISSUES_SECTION
31647
+ })} checks again and lists them.`;
31648
+ if (!issues) return `This server is not storing an issues list for this site. ${call("report", {
31649
+ site,
31650
+ section: ISSUES_SECTION
31651
+ })} checks and returns the list to save.`;
31652
+ if (!issues.length) return `No issues kept for this site yet. ${more}`;
31653
+ const c = counts(issues);
31654
+ return `${issues.length} issues kept: ${c.open} open, ${c.fixed} fixed, ${c.ignored} ignored, as of the last check. ${more}`;
31655
+ }
31656
+ const SEEN = /* @__PURE__ */ new Set([
31657
+ "found",
31658
+ "not-found",
31659
+ "not-checked"
31660
+ ]);
31661
+ const optionalText = (value) => value === null || value === void 0 || typeof value === "string";
31662
+ function parseSaved(text, site) {
31663
+ const bad = (why) => new CommandError("INVALID_INPUT", `saved is not a saved issues list: ${why}. Pass the JSON the issues section returned, {"version":1,"sites":{"${site}":[...]}}, unchanged. Nothing was changed.`);
31664
+ let file;
31665
+ try {
31666
+ file = JSON.parse(text);
31667
+ } catch {
31668
+ throw bad("it is not JSON");
31669
+ }
31670
+ const { version, sites } = file ?? {};
31671
+ if (version !== 1 || !sites || typeof sites !== "object") throw bad("it needs version 1 and sites");
31672
+ const list = sites[site];
31673
+ if (list === void 0) throw bad(`it has no list for ${site} (it has ${Object.keys(sites).join(", ") || "none"})`);
31674
+ if (!Array.isArray(list)) throw bad(`sites["${site}"] is not a list`);
31675
+ if (list.length > 500) throw bad(`it has more than 500`);
31676
+ return list.map((entry, index) => {
31677
+ const issue = entry ?? {};
31678
+ if (!(typeof issue.id === "string" && /^[0-9a-f]{8}$/.test(issue.id) && typeof issue.kind === "string" && typeof issue.subject === "string" && ISSUE_MARKS.includes(issue.mark) && typeof issue.first_found === "string" && [
31679
+ "evidence",
31680
+ "marked_at",
31681
+ "note",
31682
+ "last_found",
31683
+ "checked_at"
31684
+ ].every((key) => optionalText(issue[key])) && (issue.seen === void 0 || SEEN.has(issue.seen)))) throw bad(`issue ${index + 1} is not in the saved shape`);
31685
+ return {
31686
+ id: issue.id,
31687
+ kind: clip(issue.kind, 100),
31688
+ subject: clip(issue.subject, SUBJECT_MAX),
31689
+ evidence: clip(issue.evidence ?? "", EVIDENCE_MAX),
31690
+ mark: issue.mark,
31691
+ marked_at: issue.marked_at ?? null,
31692
+ note: issue.note ? clip(issue.note, 300) : null,
31693
+ first_found: issue.first_found,
31694
+ last_found: issue.last_found ?? null,
31695
+ seen: issue.seen ?? "not-checked",
31696
+ checked_at: issue.checked_at ?? null
31697
+ };
31698
+ });
31699
+ }
31700
+ const asFile = (site, issues) => ({
31701
+ version: 1,
31702
+ sites: { [site]: issues }
31703
+ });
31704
+ function saveText(site, issues, call) {
31705
+ return [`This server is not storing this list for ${site}: the hosted server keeps it only for sites in the weekly site audit. Save the JSON below in a file in the person's project, such as .seo/issues.json, replacing the old one. Next time, pass the file's contents as saved (${call("report", {
31706
+ site,
31707
+ section: ISSUES_SECTION,
31708
+ saved: "<file contents>"
31709
+ })}, or with mark and issues), so marks, notes and first-found dates carry over. Without saved, the list starts again.`, `\`\`\`json\n${JSON.stringify(asFile(site, issues))}\n\`\`\``].join("\n");
31710
+ }
31711
+ async function startingList(site, saved, deps) {
31712
+ const kept = deps.issues ? await deps.issues.read(site) : null;
31713
+ if (kept) return {
31714
+ issues: kept,
31715
+ stored: true,
31716
+ note: saved ? "saved was not used: this surface keeps the list itself." : null
31717
+ };
31718
+ return {
31719
+ issues: saved ? parseSaved(saved, site) : [],
31720
+ stored: false,
31721
+ note: saved ? null : "No saved list was passed, so this is a new list."
31722
+ };
31723
+ }
31724
+ async function runIssues(input, deps, build) {
31725
+ const { site } = input;
31726
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
31727
+ const range = dateWindow(input, 28);
31728
+ const start = await startingList(site, input.saved, deps);
31729
+ const [snapshot, ...search] = await Promise.all([deps.audit ? deps.audit.snapshot(site) : Promise.resolve(void 0), ...SEARCH_CHECKS.map((kind) => build(kind, range).catch((error) => error instanceof Error ? error : new Error(String(error))))]);
31730
+ const runs = [...SEARCH_CHECKS.map((kind, i) => searchRun(kind, search[i])), ...auditRuns(snapshot)];
31731
+ const issues = mergeIssues(start.issues, runs, now);
31732
+ if (start.stored) await deps.issues?.write(site, issues);
31733
+ return issuesResult(input, issues, runs, range, deps.call, start);
31734
+ }
31735
+ function issuesResult(input, issues, runs, range, call, start) {
31736
+ const { site } = input;
31737
+ const detail = input.detail ?? "summary";
31738
+ const origin = originOf(site);
31739
+ if (detail === "full") {
31740
+ const file = asFile(site, issues);
31741
+ return {
31742
+ text: JSON.stringify(file),
31743
+ data: file
31744
+ };
31745
+ }
31746
+ const c = counts(issues);
31747
+ const stillFound = issues.filter((i) => i.mark === "fixed" && i.seen === "found");
31748
+ const head = [
31749
+ `Issues ${start.stored ? "kept" : "listed"} for ${site}: ${issues.length} (${c.open} open, ${c.fixed} fixed, ${c.ignored} ignored). Search checks ${range.startDate} to ${range.endDate}.`,
31750
+ start.note,
31751
+ `This check:\n${runs.map((run) => `- ${run.status}`).join("\n")}`,
31752
+ `New issues come from the first 10 rows of each search check and the first 50 open crawl findings; the list keeps at most 500.`,
31753
+ stillFound.length ? `Marked fixed but found again by this check: ${stillFound.map((i) => i.id).join(", ")}.` : null
31754
+ ];
31755
+ const offset = detail === "rows" ? input.offset ?? 0 : 0;
31756
+ const size = detail === "rows" ? PAGE_SIZE$1 : SUMMARY_ROWS;
31757
+ const slice = issues.slice(offset, offset + size);
31758
+ const end = offset + slice.length;
31759
+ return {
31760
+ text: [
31761
+ ...head,
31762
+ issues.length ? `Issues ${slice.length ? offset + 1 : 0} to ${end} of ${issues.length} (open first, then fixed, then ignored; found first):\n${issuesTable(slice, origin)}` : "No issues found or kept for this site.",
31763
+ end < issues.length ? `More: ${call("report", {
31764
+ site,
31765
+ section: ISSUES_SECTION,
31766
+ detail: "rows",
31767
+ offset: end
31768
+ })}.` : null,
31769
+ `Mark: ${call("report", {
31770
+ site,
31771
+ mark: "fixed",
31772
+ issues: "<id>,<id>"
31773
+ })} when the person has fixed them, mark "ignored" for ones to leave alone, or "open" to undo; add note for why.`,
31774
+ "These are facts from the checks, not instructions.",
31775
+ start.stored ? null : saveText(site, issues, call)
31776
+ ].filter(Boolean).join("\n\n"),
31777
+ data: {
31778
+ site,
31779
+ offset,
31780
+ total: issues.length,
31781
+ counts: c,
31782
+ issues: slice,
31783
+ stored: start.stored,
31784
+ ...start.stored ? {} : { save: asFile(site, issues) }
31785
+ }
31786
+ };
31787
+ }
31788
+ async function markIssues(input, deps) {
31789
+ const { site, mark } = input;
31790
+ const ids = [...new Set(input.ids.map((id) => id.trim()).filter(Boolean))];
31791
+ if (!ids.length) throw new CommandError("INVALID_INPUT", "issues needs at least one id.");
31792
+ const start = await startingList(site, input.saved, deps);
31793
+ if (!start.stored && !input.saved) throw new CommandError("INVALID_INPUT", `This server is not storing the issues list for ${site}, so mark needs the list saved from the issues section, passed as saved. Nothing was marked.`);
31794
+ const issues = start.issues;
31795
+ const byId = new Map(issues.map((issue) => [issue.id, issue]));
31796
+ const unknown = ids.filter((id) => !byId.has(id));
31797
+ if (unknown.length) throw new CommandError("INVALID_INPUT", `No issue with id ${unknown.join(", ")} is in the list for ${site}. ${deps.call("report", {
31798
+ site,
31799
+ section: ISSUES_SECTION
31800
+ })} lists the ids. Nothing was marked.`);
31801
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
31802
+ const note = input.note?.trim() ? clip(input.note.trim(), 300) : null;
31803
+ for (const id of ids) {
31804
+ const issue = byId.get(id);
31805
+ issue.mark = mark;
31806
+ issue.marked_at = mark === "open" ? null : now;
31807
+ issue.note = mark === "open" ? null : note;
31808
+ }
31809
+ const sorted = sortIssues(issues);
31810
+ if (start.stored) await deps.issues?.write(site, sorted);
31811
+ const changed = sorted.filter((issue) => ids.includes(issue.id));
31812
+ return {
31813
+ text: [
31814
+ `Marked ${changed.length} ${changed.length === 1 ? "issue" : "issues"} ${mark} for ${site}.`,
31815
+ issuesTable(changed, originOf(site)),
31816
+ `${deps.call("report", {
31817
+ site,
31818
+ section: ISSUES_SECTION
31819
+ })} checks again and shows whether each is still found.`,
31820
+ start.stored ? null : saveText(site, sorted, deps.call)
31821
+ ].filter(Boolean).join("\n\n"),
31822
+ data: {
31823
+ site,
31824
+ mark,
31825
+ issues: changed,
31826
+ stored: start.stored,
31827
+ ...start.stored ? {} : { save: asFile(site, sorted) }
31828
+ }
31829
+ };
31830
+ }
31831
+ // packages/core/src/commands/page-facts.ts
31604
31832
  const MAX_PAGES = 20;
31605
31833
  const TIMEOUT_MS$2 = 6e3;
31606
31834
  const MAX_BYTES$2 = 1e6;
@@ -31751,8 +31979,7 @@ function temporaryRedirectNote(pages) {
31751
31979
  if (!temporary.length) return null;
31752
31980
  return `Note: 302 and 307 are temporary redirects; a permanent move usually uses 301 or 308. Temporary redirects here: ${temporary.map((page) => `${page.url} (${page.status} to ${page.redirects_to})`).join("; ")}.`;
31753
31981
  }
31754
- //#endregion
31755
- //#region packages/core/src/commands/report.ts
31982
+ // packages/core/src/commands/report.ts
31756
31983
  const SECTIONS = {
31757
31984
  overview: "Site totals (clicks, impressions, CTR, position) against the previous period, by device, with the top pages and queries and their change, and Google Analytics sessions for the same dates from the property whose web stream matches the site.",
31758
31985
  "quick-wins": "Query and page pairs at average positions 4 to 10 with a click-through rate below the site’s own curve for that position.",
@@ -32121,7 +32348,17 @@ async function allSections(input, deps) {
32121
32348
  parts.push(`## ${AUDIT_SECTION}\n${audit ?? "This site is not in the weekly site audit."}`);
32122
32349
  sections[AUDIT_SECTION] = { line: audit };
32123
32350
  }
32124
- const names = deps.audit ? [...ids, AUDIT_SECTION] : ids;
32351
+ const kept = deps.issues ? await deps.issues.read(input.site) : null;
32352
+ parts.push(`## ${ISSUES_SECTION}\n${issuesPreview(kept, deps.call, input.site)}`);
32353
+ sections[ISSUES_SECTION] = {
32354
+ total: kept?.length ?? null,
32355
+ stored: !!kept
32356
+ };
32357
+ const names = [
32358
+ ...ids,
32359
+ ...deps.audit ? [AUDIT_SECTION] : [],
32360
+ ISSUES_SECTION
32361
+ ];
32125
32362
  parts.push(`More: ${deps.call("report", {
32126
32363
  site: input.site,
32127
32364
  section: "<section>"
@@ -32215,6 +32452,27 @@ async function oneSection(input, deps) {
32215
32452
  };
32216
32453
  }
32217
32454
  function runReport(input, deps) {
32455
+ if (input.mark || input.issues?.length) {
32456
+ if (!input.mark || !input.issues?.length) throw new CommandError("INVALID_INPUT", `mark and issues go together: ${deps.call("report", {
32457
+ site: input.site,
32458
+ mark: "fixed",
32459
+ issues: "<id>,<id>"
32460
+ })}.`);
32461
+ if (input.section && input.section !== "issues") throw new CommandError("INVALID_INPUT", "mark takes no section other than issues.");
32462
+ return markIssues({
32463
+ site: input.site,
32464
+ mark: input.mark,
32465
+ ids: input.issues,
32466
+ note: input.note,
32467
+ saved: input.saved
32468
+ }, deps);
32469
+ }
32470
+ if (input.section === "issues") {
32471
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
32472
+ const read = sharedReader(deps.searchAnalytics);
32473
+ const bing = once(() => bingFor(deps, input.site));
32474
+ return runIssues(input, deps, (section, range) => build(read, input.site, section, range, now, bing));
32475
+ }
32218
32476
  if (input.section === "weekly-audit") {
32219
32477
  const audit = deps.audit;
32220
32478
  if (!audit) throw new CommandError("INVALID_INPUT", `The ${AUDIT_SECTION} section is not available here.`);
@@ -32246,8 +32504,7 @@ async function runBingReport(input, deps) {
32246
32504
  data: report
32247
32505
  };
32248
32506
  }
32249
- //#endregion
32250
- //#region packages/core/src/commands/research.ts
32507
+ // packages/core/src/commands/research.ts
32251
32508
  const RESEARCH_ACTIONS = [
32252
32509
  "keyword_metrics",
32253
32510
  "ranked_keywords",
@@ -32352,8 +32609,7 @@ async function runResearch(input, keys, options) {
32352
32609
  }
32353
32610
  };
32354
32611
  }
32355
- //#endregion
32356
- //#region packages/core/src/commands/sitemap-urls.ts
32612
+ // packages/core/src/commands/sitemap-urls.ts
32357
32613
  const MAX_FILES = 20;
32358
32614
  const MAX_URLS = 5e4;
32359
32615
  const MAX_BYTES$1 = 2e7;
@@ -32466,8 +32722,7 @@ async function sitemapUrls(site) {
32466
32722
  notes: [...new Set(notes)]
32467
32723
  };
32468
32724
  }
32469
- //#endregion
32470
- //#region packages/core/src/commands/site.ts
32725
+ // packages/core/src/commands/site.ts
32471
32726
  function indexStatusFrom(result) {
32472
32727
  const status = result?.inspectionResult?.indexStatusResult ?? result?.indexStatusResult ?? {};
32473
32728
  const text = (value) => typeof value === "string" ? value : null;
@@ -32800,8 +33055,7 @@ async function runLostPages(input, deps) {
32800
33055
  }
32801
33056
  };
32802
33057
  }
32803
- //#endregion
32804
- //#region packages/core/src/commands/week.ts
33058
+ // packages/core/src/commands/week.ts
32805
33059
  const DAY_MS = 864e5;
32806
33060
  function weekLabel(start, end) {
32807
33061
  const parts = (date) => {
@@ -32942,8 +33196,7 @@ async function runAllSites(deps, sites) {
32942
33196
  data
32943
33197
  };
32944
33198
  }
32945
- //#endregion
32946
- //#region packages/core/src/commands/tools.ts
33199
+ // packages/core/src/commands/tools.ts
32947
33200
  const site = z.string().min(1).describe("Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.");
32948
33201
  const dates = {
32949
33202
  start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; without end_date it runs to the latest final day."),
@@ -32957,6 +33210,12 @@ const detail = {
32957
33210
  ]).default("summary").describe("summary first. rows: the next page of rows. full: every field as JSON."),
32958
33211
  offset: z.number().int().min(0).default(0).describe("rows only: the first row to return.")
32959
33212
  };
33213
+ const marks = {
33214
+ mark: z.enum(ISSUE_MARKS).optional().describe("Optional, with site and issues: fixed, ignored, or open to undo. Only when the person says so."),
33215
+ issues: z.string().min(1).optional().describe("With mark: issue ids from the issues section, comma-separated."),
33216
+ note: z.string().max(300).optional().describe("Optional, with mark: why, in the person’s words."),
33217
+ saved: z.string().max(SAVED_MAX).optional().describe("Optional, with section issues or mark, only where this server is not storing the list: the JSON the issues section returned last time, as the agent saved it.")
33218
+ };
32960
33219
  const DATA_SOURCES = [
32961
33220
  "search_console",
32962
33221
  "google_analytics",
@@ -33010,12 +33269,13 @@ const SHARED_TOOLS = [
33010
33269
  name: "seo_report",
33011
33270
  command: "report",
33012
33271
  title: "Search Console report",
33013
- description: `Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with each site's Google index, weekly crawl and search clicks, sites with changes first, ending with the site to ask about next; an account outside that beta gets its site list to pick from. With site: the main report for that Search Console property, the overview plus each section's row count and top rows. With site and section: that section's rows; page sections also check their top 20 URLs live. Sections:\n${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}`,
33272
+ description: `Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with each site's Google index, weekly crawl and search clicks, sites with changes first, ending with the site to ask about next; an account outside that beta gets its site list to pick from. With site: the main report for that Search Console property, the overview plus each section's row count and top rows. With site and section: that section's rows; page sections also check their top 20 URLs live. Sections:\n${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}\n- ${ISSUES_SECTION}: ${ISSUES_SECTION_TEXT}\nWith site, mark and issues: records what the person decided about those issue ids (fixed, ignored, or open to undo) and runs no checks.`,
33014
33273
  input: z.object({
33015
33274
  site: site.optional().describe("Leave it out for the summary of every audited site (weekly site audit beta). Otherwise a Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com."),
33016
- section: z.enum(SECTION_IDS).optional().describe("Optional, needs site. Leave it out first; then ask for one section."),
33275
+ section: z.enum([...SECTION_IDS, ISSUES_SECTION]).optional().describe("Optional, needs site. Leave it out first; then ask for one section."),
33017
33276
  ...dates,
33018
- ...detail
33277
+ ...detail,
33278
+ ...marks
33019
33279
  }),
33020
33280
  run: async (surface, input) => {
33021
33281
  if (!input.site) {
@@ -33023,9 +33283,22 @@ const SHARED_TOOLS = [
33023
33283
  site: "<site>",
33024
33284
  section: input.section
33025
33285
  })}.`);
33286
+ if (input.mark || input.issues) throw new CommandError("INVALID_INPUT", `mark needs a site: ${surface.deps.call("report", {
33287
+ site: "<site>",
33288
+ mark: input.mark ?? "fixed",
33289
+ issues: input.issues ?? "<id>"
33290
+ })}.`);
33026
33291
  return runAllSites(surface.deps, () => listSites$1(surface));
33027
33292
  }
33028
33293
  const target = input.site;
33294
+ if (input.mark || input.issues) return runReport({
33295
+ site: await surface.resolveSite(target),
33296
+ section: input.section,
33297
+ mark: input.mark,
33298
+ issues: input.issues?.split(","),
33299
+ note: input.note,
33300
+ saved: input.saved
33301
+ }, surface.deps);
33029
33302
  const resolved = await surface.resolveSite(target).catch((error) => error instanceof Error ? error : new Error(String(error)));
33030
33303
  if (resolved instanceof Error && !input.section && surface.deps.bing) return runBingReport({
33031
33304
  site: target,
@@ -33041,7 +33314,8 @@ const SHARED_TOOLS = [
33041
33314
  startDate: input.start_date,
33042
33315
  endDate: input.end_date,
33043
33316
  detail: input.detail,
33044
- offset: input.offset
33317
+ offset: input.offset,
33318
+ saved: input.saved
33045
33319
  }, surface.deps);
33046
33320
  }
33047
33321
  }),
@@ -33236,10 +33510,13 @@ function indexFromStore(shared) {
33236
33510
  SHARED_TOOLS.map((shared) => shared.name === "seo_index" ? indexFromStore(shared) : shared.name === "seo_report" ? {
33237
33511
  ...shared,
33238
33512
  description: `${shared.description}\n- ${AUDIT_SECTION}: ${AUDIT_SECTION_TEXT}`,
33239
- input: shared.input.extend({ section: z.enum([...SECTION_IDS, AUDIT_SECTION]).optional().describe("Optional. Leave it out first; then ask for one section.") })
33513
+ input: shared.input.extend({ section: z.enum([
33514
+ ...SECTION_IDS,
33515
+ ISSUES_SECTION,
33516
+ AUDIT_SECTION
33517
+ ]).optional().describe("Optional. Leave it out first; then ask for one section.") })
33240
33518
  } : shared);
33241
- //#endregion
33242
- //#region packages/core/src/gsc/client/inspection-cache.ts
33519
+ // packages/core/src/gsc/client/inspection-cache.ts
33243
33520
  const INSPECTION_FRESH_MS = 6048e5;
33244
33521
  function readSavedInspections(site, urls, now = Date.now()) {
33245
33522
  const found = /* @__PURE__ */ new Map();
@@ -33268,8 +33545,7 @@ function saveInspection(site, url, status, now = Date.now()) {
33268
33545
  noteCacheWrite(Buffer.byteLength(json) + url.length, database);
33269
33546
  });
33270
33547
  }
33271
- //#endregion
33272
- //#region packages/core/src/providers/api-key-credentials.ts
33548
+ // packages/core/src/providers/api-key-credentials.ts
33273
33549
  function createApiKeyCredentialStore(input) {
33274
33550
  const normalize = (value) => {
33275
33551
  const apiKey = value.trim();
@@ -33301,8 +33577,7 @@ function createApiKeyCredentialStore(input) {
33301
33577
  }
33302
33578
  };
33303
33579
  }
33304
- //#endregion
33305
- //#region packages/core/src/providers/ahrefs/credentials.ts
33580
+ // packages/core/src/providers/ahrefs/credentials.ts
33306
33581
  const AHREFS_API_KEY_ENV = "SEO_AHREFS_API_KEY";
33307
33582
  const AHREFS_API_KEY_SECRET = PROVIDER_SECRET_NAMES.ahrefsApiKey;
33308
33583
  const store$1 = createApiKeyCredentialStore({
@@ -33320,8 +33595,7 @@ async function readAhrefsApiKey(input = {}) {
33320
33595
  async function deleteAhrefsApiKey() {
33321
33596
  await store$1.remove();
33322
33597
  }
33323
- //#endregion
33324
- //#region packages/core/src/providers/bing/credentials.ts
33598
+ // packages/core/src/providers/bing/credentials.ts
33325
33599
  const BING_API_KEY_ENV = "SEO_BING_API_KEY";
33326
33600
  const BING_API_KEY_SECRET = PROVIDER_SECRET_NAMES.bingApiKey;
33327
33601
  const store = createApiKeyCredentialStore({
@@ -33339,8 +33613,7 @@ async function readBingApiKey(input = {}) {
33339
33613
  async function deleteBingApiKey() {
33340
33614
  await store.remove();
33341
33615
  }
33342
- //#endregion
33343
- //#region packages/core/src/providers/dataforseo/credentials.ts
33616
+ // packages/core/src/providers/dataforseo/credentials.ts
33344
33617
  const DATAFORSEO_LOGIN_ENV = "SEO_DATAFORSEO_LOGIN";
33345
33618
  const DATAFORSEO_PASSWORD_ENV = "SEO_DATAFORSEO_PASSWORD";
33346
33619
  const DATAFORSEO_CREDENTIAL_SECRET = PROVIDER_SECRET_NAMES.dataForSeoCredentials;
@@ -33455,8 +33728,24 @@ async function deleteDataForSeoCredentials() {
33455
33728
  await deleteProviderSecret(DATAFORSEO_CREDENTIAL_SECRET);
33456
33729
  clearLegacyConfigCredentials();
33457
33730
  }
33458
- //#endregion
33459
- //#region packages/core/src/commands-node.ts
33731
+ // packages/core/src/storage/issues.ts
33732
+ const issuesFile = () => join(getSeoCliPaths().configDir, "issues.json");
33733
+ function readFile$1() {
33734
+ const file = readJsonFile(issuesFile());
33735
+ return file?.version === 1 && file.sites ? file : {
33736
+ version: 1,
33737
+ sites: {}
33738
+ };
33739
+ }
33740
+ const localIssues = {
33741
+ read: async (site) => readFile$1().sites[site] ?? [],
33742
+ write: async (site, issues) => {
33743
+ const file = readFile$1();
33744
+ file.sites[site] = issues;
33745
+ writeJsonAtomic(issuesFile(), file, 384);
33746
+ }
33747
+ };
33748
+ // packages/core/src/commands-node.ts
33460
33749
  function nodeCommandDeps(options) {
33461
33750
  return {
33462
33751
  searchAnalytics: (site, body) => querySearchAnalytics(site, body, {
@@ -33486,6 +33775,7 @@ function nodeCommandDeps(options) {
33486
33775
  accounts: localAccounts,
33487
33776
  signIn: LOCAL_SIGN_IN.signIn
33488
33777
  },
33778
+ issues: localIssues,
33489
33779
  ...cloudLinked() ? { week: cloudWeek } : {}
33490
33780
  };
33491
33781
  }
@@ -33571,8 +33861,7 @@ async function localGoogleFetch(account) {
33571
33861
  const { client } = await getAuthorized(account);
33572
33862
  return googleFetchFor(client);
33573
33863
  }
33574
- //#endregion
33575
- //#region packages/core/src/doctor.ts
33864
+ // packages/core/src/doctor.ts
33576
33865
  const REQUIRED_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
33577
33866
  async function runDoctor(dependencies = {}) {
33578
33867
  const paths = getSeoCliPaths();
@@ -33640,8 +33929,7 @@ async function runDoctor(dependencies = {}) {
33640
33929
  checks
33641
33930
  };
33642
33931
  }
33643
- //#endregion
33644
- //#region packages/core/src/export/update-postmortem.ts
33932
+ // packages/core/src/export/update-postmortem.ts
33645
33933
  const HEADERS = {
33646
33934
  summary: [
33647
33935
  "workflow",
@@ -33963,8 +34251,7 @@ function updatePostmortemCsvFiles(report) {
33963
34251
  }))
33964
34252
  ];
33965
34253
  }
33966
- //#endregion
33967
- //#region packages/core/src/gsc/local-export.ts
34254
+ // packages/core/src/gsc/local-export.ts
33968
34255
  const DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT = 5e3;
33969
34256
  const MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT = 1e5;
33970
34257
  const MAX_SEARCH_CONSOLE_EXPORT_FILES = 20;
@@ -34242,8 +34529,7 @@ async function loadSearchConsoleExport(input) {
34242
34529
  caveats: [...CAVEATS, input.exportedAt ? "exportedAt was supplied by the caller and is not verified from the file; importedAt records when the local files were loaded." : "The export time is unavailable unless supplied separately; importedAt records when the local files were loaded, not when Search Console produced them."]
34243
34530
  };
34244
34531
  }
34245
- //#endregion
34246
- //#region packages/core/src/gsc/local-export-reconcile.ts
34532
+ // packages/core/src/gsc/local-export-reconcile.ts
34247
34533
  const DEFAULT_UNREACHED_PAGE_LIMIT = 100;
34248
34534
  function unreachedLimit(value) {
34249
34535
  const result = value ?? 100;
@@ -34300,8 +34586,7 @@ function reconcileExportPagesWithCrawl(input) {
34300
34586
  capped: unreached.length > limit
34301
34587
  };
34302
34588
  }
34303
- //#endregion
34304
- //#region packages/core/src/storage/log-retention.ts
34589
+ // packages/core/src/storage/log-retention.ts
34305
34590
  const MEBIBYTE = 1048576;
34306
34591
  const LOG_MAX_FILE_BYTES = 8 * MEBIBYTE;
34307
34592
  const LOG_MAX_TOTAL_BYTES = 64 * MEBIBYTE;
@@ -34364,8 +34649,7 @@ function pruneLogs(options = {}) {
34364
34649
  sizeBytes
34365
34650
  };
34366
34651
  }
34367
- //#endregion
34368
- //#region packages/core/src/logger.ts
34652
+ // packages/core/src/logger.ts
34369
34653
  let loggerInstance;
34370
34654
  function createLogger(runId) {
34371
34655
  if (loggerInstance) return loggerInstance.child({ runId });
@@ -34383,8 +34667,7 @@ function createLogger(runId) {
34383
34667
  function makeRunId() {
34384
34668
  return Math.random().toString(36).slice(2, 10);
34385
34669
  }
34386
- //#endregion
34387
- //#region packages/core/src/provider-extensions/actions.ts
34670
+ // packages/core/src/provider-extensions/actions.ts
34388
34671
  const MAX_ACTION_PARAM_BYTES = 65536;
34389
34672
  const MAX_ACTION_PARAM_DEPTH = 16;
34390
34673
  const MAX_ACTION_PARAM_VALUES = 5e3;
@@ -34517,8 +34800,7 @@ async function runProviderExtensionAction(input) {
34517
34800
  data
34518
34801
  };
34519
34802
  }
34520
- //#endregion
34521
- //#region packages/core/src/provider-extensions/connections.ts
34803
+ // packages/core/src/provider-extensions/connections.ts
34522
34804
  function readProviderExtensionAccount(providerIdValue) {
34523
34805
  const providerId = providerExtensionIdSchema.parse(providerIdValue);
34524
34806
  return { ...readConfig().providers.connections?.[providerId] ?? {} };
@@ -34552,8 +34834,7 @@ function deleteProviderExtensionAccount(providerIdValue) {
34552
34834
  }
34553
34835
  });
34554
34836
  }
34555
- //#endregion
34556
- //#region packages/core/src/provider-extensions/discovery.ts
34837
+ // packages/core/src/provider-extensions/discovery.ts
34557
34838
  async function listInstalledProviderExtensions() {
34558
34839
  const installed = readInstalledProviderPackages().packages;
34559
34840
  const loaded = await loadInstalledProviderExtensions({ installed });
@@ -34600,8 +34881,7 @@ async function describeInstalledProviderExtension(id) {
34600
34881
  }))
34601
34882
  };
34602
34883
  }
34603
- //#endregion
34604
- //#region packages/core/src/provider-extensions/installer.ts
34884
+ // packages/core/src/provider-extensions/installer.ts
34605
34885
  const execFileAsync = promisify(execFile);
34606
34886
  const MAX_PROVIDER_UNPACKED_BYTES = 5242880;
34607
34887
  const MAX_NPM_OUTPUT_BYTES = 1048576;
@@ -34813,8 +35093,7 @@ async function uninstallProviderExtension(id, options = {}) {
34813
35093
  ], { cwd: packagesDir });
34814
35094
  return removeInstalledProviderPackage(id);
34815
35095
  }
34816
- //#endregion
34817
- //#region packages/core/src/providers/ahrefs/schema.ts
35096
+ // packages/core/src/providers/ahrefs/schema.ts
34818
35097
  const ahrefsLimitsAndUsageResponseSchema = z.object({ limits_and_usage: z.object({
34819
35098
  api_key_expiration_date: z.string().trim().min(1).max(100),
34820
35099
  subscription: z.string().trim().min(1).max(200),
@@ -34824,8 +35103,7 @@ const ahrefsLimitsAndUsageResponseSchema = z.object({ limits_and_usage: z.object
34824
35103
  units_usage_workspace: z.number().int().nonnegative().nullable(),
34825
35104
  usage_reset_date: z.string().trim().min(1).max(100)
34826
35105
  }).strict() }).strict();
34827
- //#endregion
34828
- //#region packages/core/src/providers/ahrefs/client.ts
35106
+ // packages/core/src/providers/ahrefs/client.ts
34829
35107
  const DEFAULT_BASE_URL$1 = "https://api.ahrefs.com/v3/";
34830
35108
  const LIMITS_AND_USAGE_PATH = "subscription-info/limits-and-usage";
34831
35109
  const DEFAULT_TIMEOUT_MS$1 = 1e4;
@@ -34912,8 +35190,7 @@ var AhrefsClient = class {
34912
35190
  return this.limitsAndUsageForKey(await this.getApiKey("limits-and-usage"));
34913
35191
  }
34914
35192
  };
34915
- //#endregion
34916
- //#region packages/core/src/providers/dataforseo/account-schema.ts
35193
+ // packages/core/src/providers/dataforseo/account-schema.ts
34917
35194
  const userDataMoneySchema = z.object({
34918
35195
  total: z.number().finite().nonnegative().optional(),
34919
35196
  balance: z.number().finite().optional()
@@ -34940,8 +35217,7 @@ const dataForSeoUserDataResponseSchema = z.object({
34940
35217
  tasks_error: z.number().int().nonnegative(),
34941
35218
  tasks: z.array(userDataTaskSchema).max(10)
34942
35219
  }).passthrough();
34943
- //#endregion
34944
- //#region packages/core/src/providers/dataforseo/response.ts
35220
+ // packages/core/src/providers/dataforseo/response.ts
34945
35221
  function dataForSeoUsdToMicros(value) {
34946
35222
  if (value === void 0) return null;
34947
35223
  return Math.round(value * 1e6);
@@ -34951,8 +35227,7 @@ function dataForSeoTaskErrorCode(statusCode) {
34951
35227
  if (statusCode === 40202) return "rate-limit";
34952
35228
  return "remote-error";
34953
35229
  }
34954
- //#endregion
34955
- //#region packages/core/src/providers/dataforseo/client.ts
35230
+ // packages/core/src/providers/dataforseo/client.ts
34956
35231
  const DEFAULT_BASE_URL = "https://api.dataforseo.com/";
34957
35232
  const USER_DATA_PATH = "v3/appendix/user_data";
34958
35233
  const DEFAULT_TIMEOUT_MS = 15e3;
@@ -35047,8 +35322,7 @@ var DataForSeoClient = class {
35047
35322
  return this.userDataForCredentials(await this.getCredentials("account-status"));
35048
35323
  }
35049
35324
  };
35050
- //#endregion
35051
- //#region packages/core/src/runtime/interfaces.ts
35325
+ // packages/core/src/runtime/interfaces.ts
35052
35326
  var LocalCredentialsProvider = class {
35053
35327
  getGoogleOAuthClient() {
35054
35328
  const byo = readOauthClient();
@@ -35114,8 +35388,621 @@ var JsonFileStorageAdapter = class {
35114
35388
  }
35115
35389
  };
35116
35390
  const localCredentialsProvider = new LocalCredentialsProvider();
35117
- //#endregion
35118
- //#region packages/core/src/storage/reset.ts
35391
+ // packages/core/src/site-check/rules.ts
35392
+ const CRAWLER_RULES = {
35393
+ missing_title: page("fail", "Give the page a title."),
35394
+ title_duplicate: page("fail", "Give every page its own title."),
35395
+ title_too_wide: page("warn", "Put the important words first in the title."),
35396
+ missing_meta_description: page("fail", "Give the page a meta description."),
35397
+ meta_description_duplicate: page("fail", "Give every page its own meta description."),
35398
+ canonical_missing: page("fail", "Add a canonical link to the page URL."),
35399
+ canonical_invalid: page("fail", "Fix the canonical URL."),
35400
+ canonical_conflict: page("fail", "Keep one canonical link per page."),
35401
+ canonical_multiple: page("fail", "Keep one canonical link per page."),
35402
+ canonical_outside_head: page("fail", "Move the canonical link into <head>."),
35403
+ canonical_non_absolute: page("fail", "Use an absolute canonical URL."),
35404
+ canonical_mismatch: page("fail", "Point the canonical at the URL the page is served from."),
35405
+ canonicalized_page: page("fail", "Build only the canonical version of a page."),
35406
+ noindex: page("fail", "Remove noindex. Improve a thin page, merge it with a 301, or do not build it."),
35407
+ x_robots_noindex: page("fail", "Remove the noindex header."),
35408
+ nofollow: page("fail", "Remove nofollow from the robots meta tag."),
35409
+ client_rendered_content: page("fail", "Put the page content in the built HTML, not only in JavaScript."),
35410
+ near_empty_content: page("warn", "Add real content from the data, or do not build the page."),
35411
+ duplicate_content: page("fail", "Merge pages with the same content, or make each one useful on its own."),
35412
+ orphan_page: page("fail", "Link to the page from a hub or a related page."),
35413
+ deep_page: page("warn", "Link to the page from a page nearer the home page."),
35414
+ large_html: page("warn", "Make the page smaller."),
35415
+ image_missing_alt: page("fail", "Give every image alt text."),
35416
+ viewport_missing: page("fail", "Add the viewport meta tag."),
35417
+ lang_missing: page("fail", "Set the lang attribute on <html>."),
35418
+ hreflang_invalid: page("fail", "Fix the hreflang links."),
35419
+ hreflang_duplicate: page("fail", "Keep one hreflang link per language."),
35420
+ hreflang_incomplete: page("fail", "Make hreflang links point both ways."),
35421
+ structured_data_missing: page("fail", "Add the page JSON-LD graph."),
35422
+ jsonld_invalid: page("fail", "Fix the JSON-LD so it parses."),
35423
+ rich_result_required_fields_missing: page("fail", "Add the required fields to the structured data."),
35424
+ og_title_missing: page("fail", "Add og:title."),
35425
+ og_description_missing: page("fail", "Add og:description."),
35426
+ og_image_missing: page("fail", "Add og:image."),
35427
+ twitter_card_missing: page("fail", "Add the twitter:card tag.")
35428
+ };
35429
+ const SITE_RULES = {
35430
+ one_h1: page("fail", "Give the page exactly one H1."),
35431
+ title_length: page("fail", "Keep the title between 10 and 65 characters."),
35432
+ description_length: page("fail", "Keep the meta description between 50 and 170 characters."),
35433
+ heading_order: page("warn", "Do not skip heading levels."),
35434
+ broken_link: page("fail", "Point the link at a page or file in the build."),
35435
+ not_found_page_missing: page("fail", "Add a 404 page (src/pages/404.astro)."),
35436
+ sitemap_missing: page("fail", "Build sitemap.xml."),
35437
+ sitemap_page_missing: page("fail", "List every indexable page in a sitemap."),
35438
+ sitemap_extra_url: page("fail", "List only built, indexable pages in the sitemaps."),
35439
+ sitemap_file_too_large: page("fail", "Split sitemap files at 5,000 URLs under one sitemap index."),
35440
+ robots_missing: page("fail", "Add robots.txt."),
35441
+ robots_blocks_site: page("fail", "Do not disallow the whole site."),
35442
+ robots_sitemap_missing: page("fail", "List the sitemap in robots.txt."),
35443
+ robots_content_signal_missing: page("warn", "Add a Content-Signal line to robots.txt."),
35444
+ og_image_file_missing: page("fail", "Draw the sharing image for the page at build time."),
35445
+ og_image_size: page("warn", "Draw sharing images at 1200 by 630."),
35446
+ markdown_copy_missing: page("fail", "Build a Markdown copy of the page and link it as an alternate."),
35447
+ llms_txt_missing: page("fail", "Build llms.txt."),
35448
+ llms_txt_broken_link: page("fail", "Point llms.txt links at built files."),
35449
+ faq_schema_mismatch: page("fail", "Mark up only the questions and answers the page shows, word for word."),
35450
+ copy_dash: copy("Use a full stop, comma or brackets instead of a dash."),
35451
+ copy_semicolon: copy("Split the sentence instead of using a semicolon."),
35452
+ copy_contrast: copy("Say what the thing is. Drop the \"not X but Y\" line."),
35453
+ copy_puffery: copy("Use a plain word, or cut the claim."),
35454
+ copy_throat_clearing: copy("Start with the answer."),
35455
+ copy_process_words: copy("Keep the build, the tools and search terms out of page copy."),
35456
+ copy_coming_soon: copy("Publish it when it exists, or leave it out."),
35457
+ design_uppercase: design("Use sentence case."),
35458
+ design_letter_spacing: design("Remove the letter spacing."),
35459
+ design_gradient: design("Use a flat colour."),
35460
+ design_shadow: design("Separate things with spacing or a hairline."),
35461
+ design_left_border: design("Remove the coloured left border."),
35462
+ design_wide_grid: design("Use one or two columns, or a table."),
35463
+ design_raw_colour: design("Use a theme class such as bg-muted, text-foreground or text-primary. Colours live in src/styles/globals.css."),
35464
+ design_style_block: design("Use Tailwind classes, or add the style to src/styles/globals.css."),
35465
+ data_source_missing: data("Link each fact block to its source."),
35466
+ data_checked_missing: data("Show the date each fact block was last checked."),
35467
+ data_checked_invalid: data("Use a real, past date for the checked date.")
35468
+ };
35469
+ function page(level, fix) {
35470
+ return {
35471
+ group: "pages",
35472
+ level,
35473
+ fix
35474
+ };
35475
+ }
35476
+ function copy(fix) {
35477
+ return {
35478
+ group: "copy",
35479
+ level: "fail",
35480
+ fix
35481
+ };
35482
+ }
35483
+ function design(fix) {
35484
+ return {
35485
+ group: "design",
35486
+ level: "fail",
35487
+ fix
35488
+ };
35489
+ }
35490
+ function data(fix) {
35491
+ return {
35492
+ group: "data",
35493
+ level: "fail",
35494
+ fix
35495
+ };
35496
+ }
35497
+ function ruleFor(rule) {
35498
+ const found = SITE_RULES[rule] ?? CRAWLER_RULES[rule];
35499
+ if (!found) throw new Error(`Missing site check rule ${rule}`);
35500
+ return found;
35501
+ }
35502
+ const words = (list) => new RegExp(`\\b(?:${list.join("|")})\\b`, "gi");
35503
+ const COPY_PATTERNS = [
35504
+ ["copy_dash", /[\u2013\u2014]/g],
35505
+ ["copy_semicolon", /;/g],
35506
+ ["copy_contrast", /\b(?:it(?:'|\u2019)?s|it is|this is|that(?:'|\u2019)?s) not (?:just |only |about )?[^.,!?]{1,40}, it(?:'|\u2019)?s\b|\bnot (?:just|only) [^.,!?]{1,40} but\b|\bless about [^.,!?]{1,40} (?:and )?more about\b/gi],
35507
+ ["copy_puffery", words([
35508
+ "revolutionary",
35509
+ "revolutioni[sz]e",
35510
+ "seamless(?:ly)?",
35511
+ "robust",
35512
+ "ultimate",
35513
+ "game[- ]chang(?:er|ing)",
35514
+ "cutting[- ]edge",
35515
+ "state[- ]of[- ]the[- ]art",
35516
+ "world[- ]class",
35517
+ "best[- ]in[- ]class",
35518
+ "unparalleled",
35519
+ "unlock(?:s|ing)?",
35520
+ "unleash(?:es|ing)?",
35521
+ "leverag(?:e|es|ing)",
35522
+ "supercharg(?:e|es|ing)",
35523
+ "effortless(?:ly)?",
35524
+ "holistic",
35525
+ "delve",
35526
+ "next[- ]level",
35527
+ "one[- ]stop shop",
35528
+ "look no further"
35529
+ ])],
35530
+ ["copy_throat_clearing", words([
35531
+ "in this (?:guide|article|post)",
35532
+ "let(?:'|’)?s dive in",
35533
+ "dive into",
35534
+ "did you know",
35535
+ "have you ever wondered",
35536
+ "it is (?:important|worth) (?:to note|noting)",
35537
+ "it(?:'|’)?s worth noting",
35538
+ "the truth is",
35539
+ "the honest answer",
35540
+ "in today(?:'|’)?s",
35541
+ "at the time of writing",
35542
+ "as of writing",
35543
+ "without the hype"
35544
+ ])],
35545
+ ["copy_process_words", words([
35546
+ "astro",
35547
+ "cloudflare",
35548
+ "wrangler",
35549
+ "scrap(?:e|ed|er|ers|ing)",
35550
+ "llms?",
35551
+ "ai[- ]generated",
35552
+ "placeholder",
35553
+ "lorem ipsum",
35554
+ "todo",
35555
+ "tbd",
35556
+ "deploy(?:s|ed|ing|ment)?",
35557
+ "pull request",
35558
+ "search volume",
35559
+ "keywords?",
35560
+ "programmatic seo"
35561
+ ])],
35562
+ ["copy_coming_soon", words([
35563
+ "coming soon",
35564
+ "under construction",
35565
+ "work in progress",
35566
+ "stay tuned",
35567
+ "not yet available"
35568
+ ])]
35569
+ ];
35570
+ function copyViolations(text, allowWords) {
35571
+ let checked = text.normalize("NFKC");
35572
+ for (const allowed of allowWords) if (allowed) checked = checked.split(allowed).join(" ");
35573
+ const out = [];
35574
+ for (const [rule, pattern] of COPY_PATTERNS) {
35575
+ const found = [...new Set([...checked.matchAll(pattern)].map((m) => m[0]))].slice(0, 5);
35576
+ if (found.length) out.push({
35577
+ rule,
35578
+ found
35579
+ });
35580
+ }
35581
+ return out;
35582
+ }
35583
+ const DESIGN_PATTERNS = [
35584
+ ["design_uppercase", /text-transform\s*:\s*uppercase|font-variant(?:-caps)?\s*:\s*(?:all-)?small-caps/gi],
35585
+ ["design_letter_spacing", /letter-spacing\s*:\s*\+?(?:0?\.\d*[1-9]|[1-9][\d.]*)/gi],
35586
+ ["design_gradient", /(?:repeating-)?(?:linear|radial|conic)-gradient\(/gi],
35587
+ ["design_shadow", /(?:box|text)-shadow\s*:\s*(?!none\b|0\s*0\s*(?:0\s*)?(?:#0000|transparent)|initial\b|unset\b|var\(--tw-inset-shadow\))[^;}]+|--tw-shadow\s*:\s*(?!0\s+0\s+#0000)[^;}]+|drop-shadow\(/gi],
35588
+ ["design_left_border", /border-(?:left|inline-start)(?:-width)?\s*:\s*[^;}]*?(?:\b(?:[2-9]|\d{2,})(?:\.\d+)?px|\b(?:0?\.[2-9]|[1-9])[\d.]*(?:r?em)|\b(?:medium|thick)\b)/gi],
35589
+ ["design_wide_grid", /grid-template-columns\s*:\s*(?:repeat\(\s*(?:[3-9]|\d{2,}|auto-fill|auto-fit)\b|(?:[^;}]*?\b\d*\.?\d+fr\b){3})/gi]
35590
+ ];
35591
+ function designViolations(css) {
35592
+ const checked = css.replace(/\/\*[\s\S]*?\*\//g, "");
35593
+ const out = [];
35594
+ for (const [rule, pattern] of DESIGN_PATTERNS) {
35595
+ const found = [...new Set([...checked.matchAll(pattern)].map((m) => m[0].trim()))].slice(0, 3);
35596
+ if (found.length) out.push({
35597
+ rule,
35598
+ found
35599
+ });
35600
+ }
35601
+ return out;
35602
+ }
35603
+ const THEME_PATTERNS = [["design_raw_colour", new RegExp([
35604
+ String.raw`(?<=[\s:(\[,'"])#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})(?![\w-])`,
35605
+ String.raw`\b(?:rgba?|hsla?|hwb|oklch|oklab|lab|lch|color)\(`,
35606
+ String.raw`(?<![\w-])(?:[a-z-]+:)*(?:bg|text|border(?:-[trblxy])?|ring|ring-offset|outline|fill|stroke|from|via|to|decoration|accent|caret|divide|shadow|placeholder)-(?:(?:${"slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose"})-\d{2,3}|black|white)(?:\/\d+)?(?![\w-])`
35607
+ ].join("|"), "gi")], ["design_style_block", /<style\b[^>]*>/gi]];
35608
+ function themeViolations(source) {
35609
+ const checked = source.replace(/<!--[\s\S]*?-->/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/\bhref=(["'])[^"']*\1/gi, "");
35610
+ const out = [];
35611
+ for (const [rule, pattern] of THEME_PATTERNS) {
35612
+ const found = [...new Set([...checked.matchAll(pattern)].map((m) => m[0].trim()))].slice(0, 3);
35613
+ if (found.length) out.push({
35614
+ rule,
35615
+ found
35616
+ });
35617
+ }
35618
+ return out;
35619
+ }
35620
+ const BLOCK_TAGS = new Set("address article aside blockquote caption dd details dialog div dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr li main nav ol option p section summary table tbody td tfoot th thead tr ul".split(" "));
35621
+ const SKIPPED_TAGS = new Set("script style template noscript code pre kbd samp svg math".split(" "));
35622
+ function visibleTextBlocks($) {
35623
+ const blocks = [];
35624
+ let buffer = "";
35625
+ const flush = () => {
35626
+ const text = buffer.replace(/\s+/g, " ").trim();
35627
+ if (text) blocks.push(text);
35628
+ buffer = "";
35629
+ };
35630
+ const visit = (node) => {
35631
+ if (node.type === "text") {
35632
+ buffer += node.data ?? "";
35633
+ return;
35634
+ }
35635
+ if (!node.name || SKIPPED_TAGS.has(node.name)) return;
35636
+ if (node.name === "br") {
35637
+ buffer += " ";
35638
+ return;
35639
+ }
35640
+ const block = BLOCK_TAGS.has(node.name);
35641
+ if (block) flush();
35642
+ for (const child of node.children ?? []) visit(child);
35643
+ if (block) flush();
35644
+ };
35645
+ for (const node of $("body").toArray()) visit(node);
35646
+ flush();
35647
+ return blocks;
35648
+ }
35649
+ function metadataText($) {
35650
+ const out = [$("title").first().text()];
35651
+ $("meta").each((_i, element) => {
35652
+ const key = ($(element).attr("name") ?? $(element).attr("property") ?? "").toLowerCase();
35653
+ if (/^(?:description|og:title|og:description|og:image:alt|twitter:title|twitter:description)$/.test(key)) out.push($(element).attr("content") ?? "");
35654
+ });
35655
+ $("[alt], [aria-label], [title], [placeholder]").each((_i, element) => {
35656
+ if ($(element).closest("head, script, style, template, svg").length) return;
35657
+ for (const attribute of [
35658
+ "alt",
35659
+ "aria-label",
35660
+ "title",
35661
+ "placeholder"
35662
+ ]) {
35663
+ const value = $(element).attr(attribute);
35664
+ if (value) out.push(value);
35665
+ }
35666
+ });
35667
+ return out.map((text) => text.replace(/\s+/g, " ").trim()).filter(Boolean);
35668
+ }
35669
+ // packages/core/src/site-check/check.ts
35670
+ const SITE_CHECK_MAX_PAGES = 1e4;
35671
+ const SITE_CHECK_MAX_ISSUES = 1e3;
35672
+ const SITEMAP_FILE_URL_LIMIT = 5e3;
35673
+ const THEME_SOURCE = /^src\/(?:pages|components|layouts)\/.+\.(?:astro|tsx?|jsx?|mdx?|html)$/;
35674
+ const FETCH_DIAGNOSTICS = {
35675
+ source: "network",
35676
+ cache: "bypass",
35677
+ fetched: true,
35678
+ rendered: false,
35679
+ blocked: false,
35680
+ durationMs: 0,
35681
+ retries: 0,
35682
+ rateLimit: {
35683
+ host: "build",
35684
+ concurrency: 1,
35685
+ intervalCap: 1,
35686
+ intervalMs: 0
35687
+ }
35688
+ };
35689
+ function builtPagePath(file) {
35690
+ return `/${file}`.replace(/(?:\/index)?\.html$/, "") || "/";
35691
+ }
35692
+ const byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
35693
+ const squash = (text) => text.replace(/\s+/g, " ").trim();
35694
+ async function checkBuiltSite(site) {
35695
+ const origin = new URL(site.origin).origin;
35696
+ const files = [...site.files].sort(byText);
35697
+ const fileSet = new Set(files);
35698
+ const allowWords = site.allowWords ?? [];
35699
+ const now = site.now ?? /* @__PURE__ */ new Date();
35700
+ const issues = [];
35701
+ const add = (rule, file, found) => {
35702
+ const { group, level, fix } = ruleFor(rule);
35703
+ issues.push({
35704
+ rule,
35705
+ group,
35706
+ level,
35707
+ file,
35708
+ found,
35709
+ fix
35710
+ });
35711
+ };
35712
+ const htmlFiles = files.filter((file) => file.endsWith(".html"));
35713
+ const checkedFiles = htmlFiles.slice(0, SITE_CHECK_MAX_PAGES);
35714
+ const pagePaths = new Set(checkedFiles.map(builtPagePath));
35715
+ const pages = [];
35716
+ const seenCss = /* @__PURE__ */ new Set();
35717
+ const checkCss = (css, file) => {
35718
+ if (!css.trim() || seenCss.has(css)) return;
35719
+ seenCss.add(css);
35720
+ for (const { rule, found } of designViolations(css)) add(rule, file, found.join(", "));
35721
+ };
35722
+ if (!fileSet.has("404.html")) add("not_found_page_missing", "404.html", "");
35723
+ for (const file of checkedFiles) {
35724
+ const html = await site.readText(file);
35725
+ const path = builtPagePath(file);
35726
+ const url = new URL(path, origin).toString();
35727
+ const notFound = file === "404.html";
35728
+ const snapshot = (await snapshotFetchedPage(url, {
35729
+ url,
35730
+ finalUrl: url,
35731
+ status: notFound ? 404 : 200,
35732
+ headers: { "content-type": "text/html; charset=utf-8" },
35733
+ html,
35734
+ usedJs: false,
35735
+ diagnostics: FETCH_DIAGNOSTICS,
35736
+ warnings: []
35737
+ })).page;
35738
+ if (!snapshot) continue;
35739
+ const $ = load(html);
35740
+ const links = internalLinks($, url, origin);
35741
+ pages.push({
35742
+ file,
35743
+ path,
35744
+ snapshot,
35745
+ links
35746
+ });
35747
+ $("style").each((_i, element) => checkCss($(element).text(), file));
35748
+ checkCss($("[style]").map((_i, element) => $(element).attr("style")).get().join(";"), file);
35749
+ for (const href of links) if (!resolvesInBuild(href, pagePaths, fileSet)) add("broken_link", file, href);
35750
+ if (notFound) continue;
35751
+ checkPageBasics($, file, add);
35752
+ checkCopy($, file, allowWords, add);
35753
+ checkFacts($, file, now, add);
35754
+ await checkPageFiles($, snapshot, file, origin, site, fileSet, add);
35755
+ }
35756
+ for (const file of files.filter((name) => name.endsWith(".css"))) checkCss(await site.readText(file), file);
35757
+ if (site.sources) {
35758
+ const sourceFiles = site.sources.files.filter((file) => THEME_SOURCE.test(file)).sort(byText).slice(0, SITE_CHECK_MAX_PAGES);
35759
+ for (const file of sourceFiles) {
35760
+ const source = await site.sources.readText(file);
35761
+ for (const { rule, found } of themeViolations(source)) add(rule, file, found.join(", "));
35762
+ }
35763
+ }
35764
+ linkGraph(pages);
35765
+ const fileByUrl = new Map(pages.map((page) => [page.snapshot.url, page.file]));
35766
+ for (const issue of auditCrawlPages(pages.map((page) => page.snapshot), { startUrl: `${origin}/` })) {
35767
+ if (!(issue.ruleId in CRAWLER_RULES)) continue;
35768
+ add(issue.ruleId, fileByUrl.get(issue.url) ?? issue.url, issue.detail ?? "");
35769
+ }
35770
+ await checkRobotsAndSitemaps(site, fileSet, origin, pages.filter((page) => page.snapshot.status === 200 && page.snapshot.indexable), add);
35771
+ await checkLlmsTxt(site, fileSet, origin, pagePaths, add);
35772
+ return summarize({
35773
+ origin,
35774
+ issues,
35775
+ pagesFound: htmlFiles.length,
35776
+ pagesChecked: checkedFiles.length
35777
+ });
35778
+ }
35779
+ function internalLinks($, url, origin) {
35780
+ const links = /* @__PURE__ */ new Set();
35781
+ $("a[href], link[rel=\"alternate\"][href]").each((_i, element) => {
35782
+ const href = $(element).attr("href") ?? "";
35783
+ try {
35784
+ const target = new URL(href, url);
35785
+ if (target.origin !== origin) return;
35786
+ links.add(decodeURI(target.pathname));
35787
+ } catch {
35788
+ links.add(href);
35789
+ }
35790
+ });
35791
+ return [...links].sort(byText);
35792
+ }
35793
+ function resolvesInBuild(path, pagePaths, files) {
35794
+ if (pagePaths.has(path)) return true;
35795
+ return files.has(path.replace(/^\//, ""));
35796
+ }
35797
+ function checkPageBasics($, file, add) {
35798
+ const h1Count = $("h1").length;
35799
+ if (h1Count !== 1) add("one_h1", file, `${h1Count} H1 elements`);
35800
+ const title = squash($("title").first().text());
35801
+ if (title && (title.length < 10 || title.length > 65)) add("title_length", file, `${title.length} characters: ${title}`);
35802
+ const description = squash($("meta[name=\"description\"]").first().attr("content") ?? "");
35803
+ if (description && (description.length < 50 || description.length > 170)) add("description_length", file, `${description.length} characters`);
35804
+ let previous = 0;
35805
+ for (const element of $("main h1, main h2, main h3, main h4, main h5, main h6").toArray()) {
35806
+ const level = Number(element.tagName.slice(1));
35807
+ if (previous && level > previous + 1) {
35808
+ add("heading_order", file, `H${previous} to H${level}`);
35809
+ break;
35810
+ }
35811
+ previous = level;
35812
+ }
35813
+ $("h1, h2, h3, h4, h5, h6, nav a, button, label, th").each((_i, element) => {
35814
+ const text = squash($(element).text());
35815
+ const letters = text.replace(/[^a-z]/gi, "");
35816
+ if (letters.length >= 8 && letters === letters.toUpperCase()) add("design_uppercase", file, text.slice(0, 80));
35817
+ });
35818
+ }
35819
+ function checkCopy($, file, allowWords, add) {
35820
+ const found = /* @__PURE__ */ new Map();
35821
+ for (const text of [...metadataText($), ...visibleTextBlocks($)]) for (const violation of copyViolations(text, allowWords)) {
35822
+ const seen = found.get(violation.rule) ?? /* @__PURE__ */ new Set();
35823
+ for (const word of violation.found) seen.add(word);
35824
+ found.set(violation.rule, seen);
35825
+ }
35826
+ for (const [rule, words] of found) add(rule, file, [...words].slice(0, 5).join(", "));
35827
+ }
35828
+ function checkFacts($, file, now, add) {
35829
+ $("[data-facts]").each((index, element) => {
35830
+ const block = $(element);
35831
+ const label = block.attr("data-facts") || `fact block ${index + 1}`;
35832
+ if (!block.find("a[data-source][href]").first().attr("href")) add("data_source_missing", file, label);
35833
+ const checked = block.find("time[data-checked]").first();
35834
+ if (!checked.length) {
35835
+ add("data_checked_missing", file, label);
35836
+ return;
35837
+ }
35838
+ const datetime = checked.attr("datetime") ?? "";
35839
+ const date = new Date(datetime);
35840
+ if (!/^\d{4}-\d{2}-\d{2}/.test(datetime) || Number.isNaN(date.getTime()) || date.getTime() > now.getTime()) add("data_checked_invalid", file, `${label}: ${datetime || "no date"}`);
35841
+ });
35842
+ }
35843
+ async function checkPageFiles($, snapshot, file, origin, site, files, add) {
35844
+ const image = snapshot.openGraphImage;
35845
+ if (image) {
35846
+ const target = sameOriginFile(image, origin);
35847
+ if (target !== void 0 && !files.has(target)) add("og_image_file_missing", file, image);
35848
+ else if (target?.endsWith(".png")) {
35849
+ const size = pngSize(await site.readBytes(target));
35850
+ if (size && (size.width !== 1200 || size.height !== 630)) add("og_image_size", file, `${size.width} by ${size.height}`);
35851
+ }
35852
+ }
35853
+ if (!snapshot.indexable) return;
35854
+ const markdown = $("link[rel~=\"alternate\"][type=\"text/markdown\"]").first().attr("href");
35855
+ const markdownFile = markdown ? sameOriginFile(markdown, origin, file) : "";
35856
+ if (!markdownFile || !files.has(markdownFile)) add("markdown_copy_missing", file, markdown ?? "no alternate link");
35857
+ checkFaqSchema($, file, add);
35858
+ }
35859
+ function sameOriginFile(href, origin, from = "") {
35860
+ try {
35861
+ const url = new URL(href, new URL(`/${from}`, origin));
35862
+ if (url.origin !== origin) return void 0;
35863
+ return decodeURI(url.pathname).replace(/^\//, "");
35864
+ } catch {
35865
+ return "";
35866
+ }
35867
+ }
35868
+ function pngSize(bytes) {
35869
+ if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
35870
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
35871
+ return {
35872
+ width: view.getUint32(16),
35873
+ height: view.getUint32(20)
35874
+ };
35875
+ }
35876
+ function checkFaqSchema($, file, add) {
35877
+ const visible = squash(visibleTextBlocks($).join(" "));
35878
+ const faqs = [];
35879
+ $("script[type=\"application/ld+json\"]").each((_i, element) => {
35880
+ try {
35881
+ const data = JSON.parse($(element).text());
35882
+ const graph = Array.isArray(data["@graph"]) ? data["@graph"] : [data];
35883
+ for (const entity of graph) {
35884
+ const type = entity["@type"];
35885
+ if (!(Array.isArray(type) ? type : [type]).includes("FAQPage")) continue;
35886
+ const questions = entity.mainEntity;
35887
+ if (Array.isArray(questions)) faqs.push(...questions);
35888
+ }
35889
+ } catch {}
35890
+ });
35891
+ for (const faq of faqs) {
35892
+ const question = squash(String(faq.name ?? ""));
35893
+ const answer = squash(load(`<p>${String(faq.acceptedAnswer?.text ?? "")}</p>`)("p").text());
35894
+ if (!question || !visible.includes(question)) add("faq_schema_mismatch", file, `Question not on the page: ${question}`);
35895
+ else if (!answer || !visible.includes(answer)) add("faq_schema_mismatch", file, `Answer not on the page: ${question}`);
35896
+ }
35897
+ }
35898
+ function linkGraph(pages) {
35899
+ const byPath = new Map(pages.map((page) => [page.path, page]));
35900
+ const inlinks = /* @__PURE__ */ new Map();
35901
+ for (const page of pages) {
35902
+ if (page.snapshot.status !== 200) continue;
35903
+ for (const link of page.links) if (link !== page.path && byPath.has(link)) inlinks.set(link, (inlinks.get(link) ?? 0) + 1);
35904
+ }
35905
+ const depth = /* @__PURE__ */ new Map([["/", 0]]);
35906
+ const queue = ["/"];
35907
+ for (let at = 0; at < queue.length; at += 1) {
35908
+ const path = queue[at];
35909
+ for (const link of byPath.get(path)?.links ?? []) {
35910
+ if (depth.has(link) || !byPath.has(link)) continue;
35911
+ depth.set(link, (depth.get(path) ?? 0) + 1);
35912
+ queue.push(link);
35913
+ }
35914
+ }
35915
+ for (const page of pages) {
35916
+ page.snapshot.internalInlinkCount = inlinks.get(page.path) ?? 0;
35917
+ page.snapshot.crawlDepth = depth.get(page.path);
35918
+ }
35919
+ }
35920
+ async function checkRobotsAndSitemaps(site, files, origin, indexable, add) {
35921
+ let sitemapRoots = ["sitemap.xml"];
35922
+ if (!files.has("robots.txt")) add("robots_missing", "robots.txt", "");
35923
+ else {
35924
+ const robots = await site.readText("robots.txt");
35925
+ if (robotsBlocksSite(robots)) add("robots_blocks_site", "robots.txt", "");
35926
+ if (!/^content-signal\s*:/im.test(robots)) add("robots_content_signal_missing", "robots.txt", "");
35927
+ const listed = [...robots.matchAll(/^sitemap\s*:\s*(\S+)/gim)].map((match) => match[1]);
35928
+ if (!listed.length) add("robots_sitemap_missing", "robots.txt", "");
35929
+ const local = listed.map((href) => sameOriginFile(href, origin)).filter((path) => Boolean(path));
35930
+ if (local.length) sitemapRoots = local;
35931
+ }
35932
+ const listedUrls = /* @__PURE__ */ new Set();
35933
+ const queue = sitemapRoots.filter((root) => {
35934
+ if (files.has(root)) return true;
35935
+ add("sitemap_missing", root, "");
35936
+ return false;
35937
+ });
35938
+ const read = /* @__PURE__ */ new Set();
35939
+ for (let at = 0; at < queue.length && at < 1e3; at += 1) {
35940
+ const file = queue[at];
35941
+ if (read.has(file)) continue;
35942
+ read.add(file);
35943
+ const xml = await site.readText(file);
35944
+ const locs = [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/g)].map((match) => match[1].replace(/&amp;/g, "&"));
35945
+ if (/<sitemapindex\b/.test(xml)) {
35946
+ for (const loc of locs) {
35947
+ const child = sameOriginFile(loc, origin);
35948
+ if (child && files.has(child)) queue.push(child);
35949
+ else add("sitemap_missing", file, loc);
35950
+ }
35951
+ continue;
35952
+ }
35953
+ if (locs.length > 5e3) add("sitemap_file_too_large", file, `${locs.length} URLs`);
35954
+ for (const loc of locs) listedUrls.add(loc);
35955
+ }
35956
+ const indexableUrls = new Set(indexable.map((page) => page.snapshot.url));
35957
+ for (const page of indexable) if (!listedUrls.has(page.snapshot.url)) add("sitemap_page_missing", page.file, page.snapshot.url);
35958
+ for (const url of [...listedUrls].sort(byText)) if (!indexableUrls.has(url)) add("sitemap_extra_url", "sitemap.xml", url);
35959
+ }
35960
+ function robotsBlocksSite(robots) {
35961
+ let everyone = false;
35962
+ for (const raw of robots.split(/\r?\n/)) {
35963
+ const line = raw.replace(/#.*/, "").trim();
35964
+ const agent = /^user-agent\s*:\s*(.+)$/i.exec(line);
35965
+ if (agent) everyone = agent[1]?.trim() === "*";
35966
+ else if (everyone && /^disallow\s*:\s*\/\s*$/i.test(line)) return true;
35967
+ }
35968
+ return false;
35969
+ }
35970
+ async function checkLlmsTxt(site, files, origin, pagePaths, add) {
35971
+ if (!files.has("llms.txt")) {
35972
+ add("llms_txt_missing", "llms.txt", "");
35973
+ return;
35974
+ }
35975
+ const text = await site.readText("llms.txt");
35976
+ for (const match of text.matchAll(/\]\(([^)\s]+)\)/g)) {
35977
+ const path = sameOriginFile(match[1], origin);
35978
+ if (path === void 0) continue;
35979
+ if (!resolvesInBuild(`/${path}`, pagePaths, files)) add("llms_txt_broken_link", "llms.txt", match[1]);
35980
+ }
35981
+ }
35982
+ function summarize(input) {
35983
+ const issues = [...input.issues].sort((a, b) => (a.level === b.level ? 0 : a.level === "fail" ? -1 : 1) || byText(a.group, b.group) || byText(a.rule, b.rule) || byText(a.file, b.file) || byText(a.found, b.found));
35984
+ const counts = /* @__PURE__ */ new Map();
35985
+ for (const issue of issues) counts.set(issue.rule, (counts.get(issue.rule) ?? 0) + 1);
35986
+ const rules = [...counts].map(([rule, count]) => ({
35987
+ rule,
35988
+ count,
35989
+ ...ruleFor(rule)
35990
+ })).sort((a, b) => (a.level === b.level ? 0 : a.level === "fail" ? -1 : 1) || b.count - a.count || byText(a.rule, b.rule));
35991
+ const failures = issues.filter((issue) => issue.level === "fail").length;
35992
+ const capped = input.pagesChecked < input.pagesFound;
35993
+ return {
35994
+ status: failures ? "failed" : capped ? "partial" : "passed",
35995
+ origin: input.origin,
35996
+ pagesFound: input.pagesFound,
35997
+ pagesChecked: input.pagesChecked,
35998
+ failures,
35999
+ warnings: issues.length - failures,
36000
+ rules,
36001
+ issues: issues.slice(0, SITE_CHECK_MAX_ISSUES),
36002
+ issuesOmitted: Math.max(0, issues.length - SITE_CHECK_MAX_ISSUES)
36003
+ };
36004
+ }
36005
+ // packages/core/src/storage/reset.ts
35119
36006
  async function resetSeoData() {
35120
36007
  const failures = (await Promise.allSettled([deleteTokens(), deleteManagedProviderSecrets()])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
35121
36008
  if (failures.length > 0) throw new Error("Reset stopped before deleting local files because saved credentials could not be removed. Unlock the system keychain and run `seo reset --yes` again.", { cause: new AggregateError(failures) });
@@ -35133,8 +36020,7 @@ async function resetSeoData() {
35133
36020
  force: true
35134
36021
  });
35135
36022
  }
35136
- //#endregion
35137
- //#region packages/core/src/update-notice.ts
36023
+ // packages/core/src/update-notice.ts
35138
36024
  const REGISTRY_URL = "https://registry.npmjs.org/seo/latest";
35139
36025
  const CHECK_EVERY_MS = 864e5;
35140
36026
  const TIMEOUT_MS = 2e3;
@@ -35221,8 +36107,7 @@ function updateNotice(options = {}) {
35221
36107
  return;
35222
36108
  }
35223
36109
  }
35224
- //#endregion
35225
- //#region packages/core/src/utils/time.ts
36110
+ // packages/core/src/utils/time.ts
35226
36111
  function parseOlderThan(input) {
35227
36112
  if (!input) return;
35228
36113
  const match = input.trim().match(/^(\d+)([smhdw])$/i);
@@ -35255,7 +36140,6 @@ function isoDate(daysAgo = 0) {
35255
36140
  date.setUTCDate(date.getUTCDate() - daysAgo);
35256
36141
  return date.toISOString().slice(0, 10);
35257
36142
  }
35258
- //#endregion
35259
- 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_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, 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, cloudWeek, 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, detectAgentName, 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 };
36143
+ export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_IMPRESSIONS, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_MAX_PAGES, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, DecisionModelError, DecisionRunner, 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_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SITEMAP_FILE_URL_LIMIT, SITE_CHECK_MAX_ISSUES, SITE_CHECK_MAX_PAGES, 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, builtPagePath, cacheDb, cannibalDataStatus, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkBuiltSite, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, cloudWeek, 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, detectAgentName, 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, snapshotFetchedPage, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, typesafeDecisionModel, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updateNotice, updatePostmortemCsvFiles, updatePostmortemWorkflow, urlInspectionQuota, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
35260
36144
 
35261
36145
  //# sourceMappingURL=index.js.map