seo 0.13.2 → 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 +37 -8
  2. package/dist/cli.js +84 -80
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{dist-BBszRLUG.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-Dmtr8qrP.js → esm-BmuxgZyN.js} +95 -188
  8. package/dist/{esm-Dmtr8qrP.js.map → esm-BmuxgZyN.js.map} +1 -1
  9. package/dist/index.d.ts +92 -406
  10. package/dist/index.js +1558 -646
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.d.ts +0 -14
  13. package/dist/mcp.js +1130 -889
  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-7TxFytBp.js +0 -420
  88. package/dist/dist-7TxFytBp.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-Dmtr8qrP.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) {
@@ -1638,6 +1632,13 @@ async function failure(response, label, subject, options) {
1638
1632
  const detail = (await response.text().catch(() => "")).slice(0, 300);
1639
1633
  const status = response.status;
1640
1634
  const what = subject ? `${label} for ${subject}` : label;
1635
+ if (status === 400) {
1636
+ let reason = detail;
1637
+ try {
1638
+ reason = JSON.parse(detail).error?.message ?? detail;
1639
+ } catch {}
1640
+ return new GoogleApiError("INVALID_INPUT", `${label} rejected the request: ${reason}`.trim(), status);
1641
+ }
1641
1642
  if (status === 401) return new GoogleApiError("AUTH_EXPIRED", `Google rejected the sign-in. ${options.signIn}`, status);
1642
1643
  if (status === 403) return new GoogleApiError("ACCESS_DENIED", `Google denied access to ${what}. Check the Google account, its access to this property, and the granted scopes. ${detail}`.trim(), status);
1643
1644
  if (status === 404) return new GoogleApiError("PROPERTY_NOT_FOUND", `Google could not find ${what}. Check the property and account access.`, status);
@@ -1738,8 +1739,7 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1738
1739
  body
1739
1740
  });
1740
1741
  }
1741
- //#endregion
1742
- //#region packages/core/src/bing/api.ts
1742
+ // packages/core/src/bing/api.ts
1743
1743
  const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1744
1744
  const MAX_BYTES$4 = 8e6;
1745
1745
  function bingCall(auth, options) {
@@ -1769,7 +1769,7 @@ function bingCall(auth, options) {
1769
1769
  return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$4 })).d;
1770
1770
  };
1771
1771
  }
1772
- function day$2(value) {
1772
+ function day$3(value) {
1773
1773
  if (typeof value !== "string") return null;
1774
1774
  const legacy = /^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/.exec(value);
1775
1775
  const date = legacy ? new Date(Number(legacy[1])) : new Date(value);
@@ -1790,7 +1790,7 @@ async function bingTraffic(call, site) {
1790
1790
  const rows = [];
1791
1791
  let invalidRows = 0;
1792
1792
  for (const row of list$2(await call("GetRankAndTrafficStats", { siteUrl: site }), "traffic list")) {
1793
- const date = day$2(row.Date);
1793
+ const date = day$3(row.Date);
1794
1794
  const clicks = count$3(row.Clicks);
1795
1795
  const impressions = count$3(row.Impressions);
1796
1796
  if (!date || clicks === null || impressions === null) {
@@ -1813,7 +1813,7 @@ async function keyed(call, method, params) {
1813
1813
  const rows = [];
1814
1814
  let invalidRows = 0;
1815
1815
  for (const row of list$2(await call(method, params), "row list")) {
1816
- const date = day$2(row.Date);
1816
+ const date = day$3(row.Date);
1817
1817
  const key = typeof row.Query === "string" ? row.Query.trim() : "";
1818
1818
  const clicks = count$3(row.Clicks);
1819
1819
  const impressions = count$3(row.Impressions);
@@ -1850,8 +1850,7 @@ function bingSiteFor(sites, target) {
1850
1850
  if (!wanted) return null;
1851
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;
1852
1852
  }
1853
- //#endregion
1854
- //#region packages/core/src/gsc/dates.ts
1853
+ // packages/core/src/gsc/dates.ts
1855
1854
  const GSC_TIME_ZONE = "America/Los_Angeles";
1856
1855
  const GSC_FINAL_DATA_LAG_DAYS = 4;
1857
1856
  function dateInTimeZone(now, timeZone) {
@@ -1880,8 +1879,7 @@ function finalGscDateRange(days = 28, now = /* @__PURE__ */ new Date()) {
1880
1879
  endDate
1881
1880
  };
1882
1881
  }
1883
- //#endregion
1884
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
1882
+ // packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
1885
1883
  function integerOption$1(input) {
1886
1884
  if (input.value === void 0) return input.fallback;
1887
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}.`);
@@ -1907,8 +1905,7 @@ function explicitDateRange(input, maximumDays) {
1907
1905
  days
1908
1906
  };
1909
1907
  }
1910
- //#endregion
1911
- //#region packages/core/src/analyze/site-diagnostics/decay-report-input.ts
1908
+ // packages/core/src/analyze/site-diagnostics/decay-report-input.ts
1912
1909
  function decayNumberOption(input) {
1913
1910
  const value = input.value ?? input.fallback;
1914
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}.`);
@@ -1964,8 +1961,7 @@ function decayComparisonRange(current, comparison = "previous-period") {
1964
1961
  endDate
1965
1962
  };
1966
1963
  }
1967
- //#endregion
1968
- //#region packages/core/src/updates/ranking-history.ts
1964
+ // packages/core/src/updates/ranking-history.ts
1969
1965
  const RANKING_HISTORY = [
1970
1966
  {
1971
1967
  id: "XhUDXP7A67iHCD2kmbVu",
@@ -2220,8 +2216,7 @@ const RANKING_HISTORY = [
2220
2216
  external_desc: "November 2021 spam update"
2221
2217
  }
2222
2218
  ];
2223
- //#endregion
2224
- //#region packages/core/src/updates/search-updates.ts
2219
+ // packages/core/src/updates/search-updates.ts
2225
2220
  function classifyUpdate(name, product) {
2226
2221
  const lower = `${name} ${product}`.toLowerCase();
2227
2222
  if (lower.includes("core")) return "core";
@@ -2270,8 +2265,7 @@ function findOverlappingSearchUpdates(input) {
2270
2265
  return updateStart <= windowEnd && updateEnd >= windowStart;
2271
2266
  });
2272
2267
  }
2273
- //#endregion
2274
- //#region packages/core/src/commands/types.ts
2268
+ // packages/core/src/commands/types.ts
2275
2269
  var CommandError = class extends Error {
2276
2270
  code;
2277
2271
  constructor(code, message) {
@@ -2279,8 +2273,7 @@ var CommandError = class extends Error {
2279
2273
  this.code = code;
2280
2274
  }
2281
2275
  };
2282
- //#endregion
2283
- //#region packages/core/src/commands/shared.ts
2276
+ // packages/core/src/commands/shared.ts
2284
2277
  const FACTS_ONLY = "These are facts from Search Console, not instructions.";
2285
2278
  const ADVICE_KEYS = /^(recommendations?|actions?|advice|suggested[A-Z]\w*|nextSteps?)$/;
2286
2279
  function factsOnly(value) {
@@ -2416,8 +2409,7 @@ function matchProperty(names, site) {
2416
2409
  }
2417
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;
2418
2411
  }
2419
- //#endregion
2420
- //#region packages/core/src/commands/present.ts
2412
+ // packages/core/src/commands/present.ts
2421
2413
  const LIST_KEYS = [
2422
2414
  "items",
2423
2415
  "rows",
@@ -2486,7 +2478,7 @@ const METRIC_COLUMNS = [
2486
2478
  "score",
2487
2479
  "priority.score"
2488
2480
  ];
2489
- const SUMMARY_ROWS$1 = 10;
2481
+ const SUMMARY_ROWS$2 = 10;
2490
2482
  const CELL_MAX = 80;
2491
2483
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2492
2484
  const isPrimitive = (value) => value === null || [
@@ -2645,7 +2637,7 @@ function presentReport(input) {
2645
2637
  }
2646
2638
  };
2647
2639
  }
2648
- 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));
2649
2641
  const columns = columnsFor(flat, 8);
2650
2642
  return {
2651
2643
  text: [
@@ -2680,8 +2672,7 @@ function preview(report, origin, rows) {
2680
2672
  text: table$1(flat.slice(0, rows), columnsFor(flat, 6), origin)
2681
2673
  };
2682
2674
  }
2683
- //#endregion
2684
- //#region packages/core/src/commands/sites.ts
2675
+ // packages/core/src/commands/sites.ts
2685
2676
  async function readableSites(google, signIn) {
2686
2677
  return (await listSearchConsoleSites(google, { signIn })).filter((entry) => entry.permissionLevel !== "siteUnverifiedUser").map((entry) => ({
2687
2678
  siteUrl: entry.siteUrl,
@@ -2755,8 +2746,7 @@ async function runSites(input) {
2755
2746
  }
2756
2747
  };
2757
2748
  }
2758
- //#endregion
2759
- //#region packages/core/src/commands/linked.ts
2749
+ // packages/core/src/commands/linked.ts
2760
2750
  const MAX_ROWS$2 = 1e5;
2761
2751
  const PERMISSION_RANK = {
2762
2752
  siteOwner: 0,
@@ -2859,8 +2849,7 @@ var LinkedAccounts = class {
2859
2849
  });
2860
2850
  }
2861
2851
  };
2862
- //#endregion
2863
- //#region packages/core/src/paths.ts
2852
+ // packages/core/src/paths.ts
2864
2853
  let memoizedPaths;
2865
2854
  function defaultConfigDir(input) {
2866
2855
  if (input.platform === "win32") return join(input.env.APPDATA ?? join(input.home, "AppData", "Roaming"), "seo", "Config");
@@ -2955,8 +2944,7 @@ function fileSize(path) {
2955
2944
  return 0;
2956
2945
  }
2957
2946
  }
2958
- //#endregion
2959
- //#region packages/core/src/storage/files.ts
2947
+ // packages/core/src/storage/files.ts
2960
2948
  function ensureParentDir(path, mode = 448) {
2961
2949
  mkdirSync(dirname(path), {
2962
2950
  recursive: true,
@@ -2994,8 +2982,7 @@ function safeRemove(path) {
2994
2982
  force: true
2995
2983
  });
2996
2984
  }
2997
- //#endregion
2998
- //#region packages/core/src/telemetry-failures.ts
2985
+ // packages/core/src/telemetry-failures.ts
2999
2986
  const TELEMETRY_ERROR_CATEGORIES = [
3000
2987
  "auth",
3001
2988
  "crawl_timeout",
@@ -3270,8 +3257,7 @@ function classifyTelemetryFailure(error) {
3270
3257
  function isTelemetryOperation(value) {
3271
3258
  return typeof value === "string" && TELEMETRY_OPERATIONS.includes(value);
3272
3259
  }
3273
- //#endregion
3274
- //#region packages/core/src/telemetry-shared.ts
3260
+ // packages/core/src/telemetry-shared.ts
3275
3261
  const TELEMETRY_ENDPOINT = "https://seoskill.dev/api/t";
3276
3262
  const TELEMETRY_SCHEMA_VERSION = 2;
3277
3263
  function isoWeek(date) {
@@ -3290,11 +3276,9 @@ function agentFromClientName(name) {
3290
3276
  if (value.includes("codex") || value.includes("openai")) return "codex";
3291
3277
  return "unknown";
3292
3278
  }
3293
- //#endregion
3294
- //#region packages/core/src/version.ts
3279
+ // packages/core/src/version.ts
3295
3280
  const SEO_VERSION = version;
3296
- //#endregion
3297
- //#region packages/core/src/telemetry.ts
3281
+ // packages/core/src/telemetry.ts
3298
3282
  const TELEMETRY_TIMEOUT_MS = 2e3;
3299
3283
  const TELEMETRY_EVENTS = [
3300
3284
  "first_run",
@@ -3658,8 +3642,7 @@ function telemetryFailureInput(error) {
3658
3642
  };
3659
3643
  return classifyTelemetryFailure(error);
3660
3644
  }
3661
- //#endregion
3662
- //#region packages/core/src/cloud.ts
3645
+ // packages/core/src/cloud.ts
3663
3646
  const CLOUD_URL = "https://seoskill.dev";
3664
3647
  const PRIVATE = 384;
3665
3648
  const EARLY = 6e4;
@@ -3815,8 +3798,7 @@ async function cloudLogout() {
3815
3798
  async function openCloudLink(url) {
3816
3799
  await open(url).catch(() => void 0);
3817
3800
  }
3818
- //#endregion
3819
- //#region packages/core/src/types/auth.ts
3801
+ // packages/core/src/types/auth.ts
3820
3802
  const tokenSchema = z.object({
3821
3803
  provider: z.literal("google"),
3822
3804
  account_email: z.string().email().max(320),
@@ -3845,8 +3827,7 @@ const tokenStoreSchema = z.object({
3845
3827
  message: "The active Google account must exist in accounts."
3846
3828
  });
3847
3829
  });
3848
- //#endregion
3849
- //#region packages/core/src/providers/contracts.ts
3830
+ // packages/core/src/providers/contracts.ts
3850
3831
  const providerIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
3851
3832
  const providerCapabilitySchema = z.enum([
3852
3833
  "keyword-metrics",
@@ -3864,8 +3845,7 @@ const providerCapabilitySchema = z.enum([
3864
3845
  "ai-mentions",
3865
3846
  "ai-prompt-observation"
3866
3847
  ]);
3867
- //#endregion
3868
- //#region packages/core/src/types/config.ts
3848
+ // packages/core/src/types/config.ts
3869
3849
  const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
3870
3850
  const analyticsProviderSchema = z.enum(["google", "clicky"]);
3871
3851
  const analyticsExtensionIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
@@ -3918,8 +3898,7 @@ async function setKeyringPassword(service, account, password) {
3918
3898
  async function deleteKeyringPassword(service, account) {
3919
3899
  return (await resolveKeyring()).deletePassword(service, account);
3920
3900
  }
3921
- //#endregion
3922
- //#region packages/core/src/storage/config.ts
3901
+ // packages/core/src/storage/config.ts
3923
3902
  const KEYRING_SERVICE$1 = "seo";
3924
3903
  const PRIVATE_FILE_MODE$2 = 384;
3925
3904
  function tightenConfigPermissions(path) {
@@ -4201,8 +4180,7 @@ function getPrivacySnapshot() {
4201
4180
  }
4202
4181
  ];
4203
4182
  }
4204
- //#endregion
4205
- //#region packages/core/src/storage/lock.ts
4183
+ // packages/core/src/storage/lock.ts
4206
4184
  async function withFileLock(path, task) {
4207
4185
  ensureParentDir(path);
4208
4186
  const fastLocks = process.env.SEO_LOCK_FAST === "1";
@@ -4231,14 +4209,12 @@ async function withFileLock(path, task) {
4231
4209
  await release();
4232
4210
  }
4233
4211
  }
4234
- //#endregion
4235
- //#region packages/core/src/gsc/shared-client.generated.ts
4212
+ // packages/core/src/gsc/shared-client.generated.ts
4236
4213
  const SHARED_OAUTH_CLIENT = {
4237
4214
  clientId: "548418788053-q7p964rqvjiv8ooamciprku6bpu34u9d.apps.googleusercontent.com",
4238
4215
  clientSecret: "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"
4239
4216
  };
4240
- //#endregion
4241
- //#region packages/core/src/gsc/auth/client-config.ts
4217
+ // packages/core/src/gsc/auth/client-config.ts
4242
4218
  function getAuthModeStatus() {
4243
4219
  const byo = readOauthClient();
4244
4220
  return {
@@ -4270,8 +4246,7 @@ function getClientConfig(source) {
4270
4246
  function missingOAuthClientMessage(source) {
4271
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.";
4272
4248
  }
4273
- //#endregion
4274
- //#region packages/core/src/gsc/auth/types.ts
4249
+ // packages/core/src/gsc/auth/types.ts
4275
4250
  const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
4276
4251
  const GOOGLE_READONLY_SCOPE_LABELS = {
4277
4252
  "https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
@@ -4286,8 +4261,7 @@ const GOOGLE_SCOPE = [
4286
4261
  "openid",
4287
4262
  "email"
4288
4263
  ].join(" ");
4289
- //#endregion
4290
- //#region packages/core/src/gsc/auth/service-account.ts
4264
+ // packages/core/src/gsc/auth/service-account.ts
4291
4265
  const GOOGLE_TOKEN_ENDPOINT$1 = "https://oauth2.googleapis.com/token";
4292
4266
  const TOKEN_BUFFER_MS = 6e4;
4293
4267
  function authConfigError(message) {
@@ -4433,8 +4407,7 @@ var ServiceAccountAccessTokenClient = class {
4433
4407
  return this.token.value;
4434
4408
  }
4435
4409
  };
4436
- //#endregion
4437
- //#region packages/core/src/gsc/auth/token-endpoint.ts
4410
+ // packages/core/src/gsc/auth/token-endpoint.ts
4438
4411
  const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
4439
4412
  var GoogleTokenEndpointError = class extends Error {
4440
4413
  status;
@@ -4488,8 +4461,7 @@ async function exchangeGoogleCode(input, fetchImpl = fetch) {
4488
4461
  }
4489
4462
  return readJson(response, "Google sign-in");
4490
4463
  }
4491
- //#endregion
4492
- //#region packages/core/src/gsc/auth/authorized-client.ts
4464
+ // packages/core/src/gsc/auth/authorized-client.ts
4493
4465
  const REFRESH_BUFFER_MS = 6e4;
4494
4466
  function authRequired(accountEmail) {
4495
4467
  return new SeoError("AUTH_REQUIRED", accountEmail ? `Google account ${accountEmail} is not connected. Run \`seo auth login\` first.` : notSignedIn());
@@ -4641,8 +4613,7 @@ async function authStatus() {
4641
4613
  serviceAccount
4642
4614
  };
4643
4615
  }
4644
- //#endregion
4645
- //#region packages/core/src/gsc/auth/callback-page.ts
4616
+ // packages/core/src/gsc/auth/callback-page.ts
4646
4617
  function callbackCopy(options) {
4647
4618
  if (options.status === "connected") return {
4648
4619
  title: "Google account connected",
@@ -4681,8 +4652,7 @@ function oauthCallbackPage(options = { status: "connected" }) {
4681
4652
  </body>
4682
4653
  </html>`;
4683
4654
  }
4684
- //#endregion
4685
- //#region packages/core/src/gsc/auth/loopback-callback.ts
4655
+ // packages/core/src/gsc/auth/loopback-callback.ts
4686
4656
  const OAUTH_CALLBACK_TIMEOUT_MS = 3e5;
4687
4657
  function timeoutError() {
4688
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.");
@@ -4738,8 +4708,7 @@ function waitForCode(input) {
4738
4708
  });
4739
4709
  });
4740
4710
  }
4741
- //#endregion
4742
- //#region packages/core/src/gsc/auth/user-email.ts
4711
+ // packages/core/src/gsc/auth/user-email.ts
4743
4712
  function decodeJwtEmail(idToken) {
4744
4713
  if (!idToken) return;
4745
4714
  const [, payload] = idToken.split(".");
@@ -4755,8 +4724,7 @@ async function fetchUserEmail(accessToken) {
4755
4724
  if (!response.ok) return;
4756
4725
  return (await readJson(response, "Google user info")).email;
4757
4726
  }
4758
- //#endregion
4759
- //#region packages/core/src/gsc/auth/loopback.ts
4727
+ // packages/core/src/gsc/auth/loopback.ts
4760
4728
  function missingSharedClientLoginMessage() {
4761
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.";
4762
4730
  }
@@ -4831,8 +4799,7 @@ async function loginWithLoopback(clientConfig = getClientConfig()) {
4831
4799
  server.close();
4832
4800
  }
4833
4801
  }
4834
- //#endregion
4835
- //#region packages/core/src/gsc/client/fetch.ts
4802
+ // packages/core/src/gsc/client/fetch.ts
4836
4803
  async function authedFetch(client, url, init) {
4837
4804
  const accessToken = await client.getAccessToken();
4838
4805
  if (!accessToken) throw new SeoError("AUTH_EXPIRED", "Could not obtain a Google access token. Run `seo auth login` again.");
@@ -4853,8 +4820,7 @@ function googleFetchFor(client) {
4853
4820
  return (url, init) => authedFetch(client, url, init);
4854
4821
  }
4855
4822
  const LOCAL_SIGN_IN = { signIn: "Run `seo auth login` again." };
4856
- //#endregion
4857
- //#region packages/core/src/storage/cache-maintenance.ts
4823
+ // packages/core/src/storage/cache-maintenance.ts
4858
4824
  const MEBIBYTE$2 = 1048576;
4859
4825
  const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
4860
4826
  const CACHE_MAX_AGE_MS = 2592e6;
@@ -5031,8 +4997,7 @@ function runCacheMaintenance(database, options = {}) {
5031
4997
  compaction
5032
4998
  };
5033
4999
  }
5034
- //#endregion
5035
- //#region packages/core/src/storage/sqlite.ts
5000
+ // packages/core/src/storage/sqlite.ts
5036
5001
  function withoutDriverMetadata(row) {
5037
5002
  if (!row || typeof row !== "object" || Array.isArray(row)) return row;
5038
5003
  const { _metadata: _driverMetadata, ...data } = row;
@@ -5077,8 +5042,7 @@ var SqliteDatabase = class {
5077
5042
  return this;
5078
5043
  }
5079
5044
  };
5080
- //#endregion
5081
- //#region packages/core/src/storage/database.ts
5045
+ // packages/core/src/storage/database.ts
5082
5046
  const PRIVATE_DATABASE_FILE_MODE = 384;
5083
5047
  const DATABASE_FILE_SUFFIXES = [
5084
5048
  "",
@@ -5361,8 +5325,7 @@ function clearCache(provider, olderThanMs) {
5361
5325
  compactCacheDatabase(database, { allowFullVacuum: true });
5362
5326
  return removed;
5363
5327
  }
5364
- //#endregion
5365
- //#region packages/core/src/ga4/client.ts
5328
+ // packages/core/src/ga4/client.ts
5366
5329
  function ga4RequestCanUseCache(body) {
5367
5330
  return body.dateRanges.every((range) => /^\d{4}-\d{2}-\d{2}$/.test(range.startDate) && /^\d{4}-\d{2}-\d{2}$/.test(range.endDate));
5368
5331
  }
@@ -5443,8 +5406,7 @@ function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
5443
5406
  })) warnings.push(`${label} was sampled by Google Analytics.`);
5444
5407
  return warnings;
5445
5408
  }
5446
- //#endregion
5447
- //#region packages/core/src/analyze/ai-referrals-sources.ts
5409
+ // packages/core/src/analyze/ai-referrals-sources.ts
5448
5410
  const AI_REFERRAL_SOURCES_VERSION = "ai-referral-sources@1";
5449
5411
  const AI_REFERRAL_SOURCES = [
5450
5412
  {
@@ -5530,8 +5492,7 @@ function aiReferralSourceForValue(value) {
5530
5492
  if (!hostname) return void 0;
5531
5493
  return AI_REFERRAL_SOURCES.find((source) => source.domains.some((domain) => matchesDomain(hostname, domain)));
5532
5494
  }
5533
- //#endregion
5534
- //#region packages/core/src/analyze/ai-referrals-analysis.ts
5495
+ // packages/core/src/analyze/ai-referrals-analysis.ts
5535
5496
  function compareText$19(left, right) {
5536
5497
  return left < right ? -1 : left > right ? 1 : 0;
5537
5498
  }
@@ -5703,13 +5664,12 @@ function analyzeAiReferralRows(input) {
5703
5664
  ]
5704
5665
  };
5705
5666
  }
5706
- //#endregion
5707
- //#region packages/core/src/analyze/ai-referrals-query.ts
5708
- const PAGE_SIZE$3 = 25e3;
5667
+ // packages/core/src/analyze/ai-referrals-query.ts
5668
+ const PAGE_SIZE$4 = 25e3;
5709
5669
  async function fetchAiReferralRows(input) {
5710
- const pageSize = input.pageSize ?? PAGE_SIZE$3;
5670
+ const pageSize = input.pageSize ?? PAGE_SIZE$4;
5711
5671
  if (!Number.isInteger(input.maxRows) || input.maxRows < 1) throw new Error("maxRows must be a positive whole number.");
5712
- 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}.`);
5713
5673
  const rows = [];
5714
5674
  const warnings = /* @__PURE__ */ new Set();
5715
5675
  const retainedKeys = /* @__PURE__ */ new Set();
@@ -5779,8 +5739,7 @@ function sessionSourceInListFilter(values) {
5779
5739
  }
5780
5740
  } };
5781
5741
  }
5782
- //#endregion
5783
- //#region packages/core/src/analyze/ai-referrals.ts
5742
+ // packages/core/src/analyze/ai-referrals.ts
5784
5743
  const DEFAULT_MAX_ROWS$1 = 1e5;
5785
5744
  const MAX_ROWS$1 = 1e5;
5786
5745
  const DEFAULT_RESULT_LIMIT = 25;
@@ -5957,8 +5916,7 @@ async function aiReferralsReport(input, dependencies = {}) {
5957
5916
  totalUsersEvidence
5958
5917
  });
5959
5918
  }
5960
- //#endregion
5961
- //#region packages/core/src/robots-directives.ts
5919
+ // packages/core/src/robots-directives.ts
5962
5920
  function normalizedDirective(value) {
5963
5921
  const directive = value.trim().toLowerCase().split(/\s+/)[0];
5964
5922
  if (directive === "none") return ["noindex", "nofollow"];
@@ -6217,8 +6175,7 @@ function resolveStreamedSegments($) {
6217
6175
  function hasNextAppRouterPayload($) {
6218
6176
  return $("script:not([src])").toArray().some((script) => $(script).text().includes("self.__next_f"));
6219
6177
  }
6220
- //#endregion
6221
- //#region packages/core/src/extract/canonical.ts
6178
+ // packages/core/src/extract/canonical.ts
6222
6179
  function headerValue$4(headers, name) {
6223
6180
  const lower = name.toLowerCase();
6224
6181
  return Object.entries(headers).find(([key]) => key.toLowerCase() === lower)?.[1];
@@ -6367,11 +6324,10 @@ function extractHttpCanonicalEvidence(headers, baseUrl) {
6367
6324
  function extractCanonicalEvidence($, headers, baseUrl) {
6368
6325
  return evidenceFromCandidates([...htmlCandidates($, baseUrl), ...headerCandidates(headers, baseUrl)]);
6369
6326
  }
6370
- //#endregion
6371
- //#region packages/core/src/extract/content-sketch.ts
6327
+ // packages/core/src/extract/content-sketch.ts
6372
6328
  const SHINGLE_SIZE = 5;
6373
6329
  const MAX_HASHES = 32;
6374
- function words$2(value) {
6330
+ function words$3(value) {
6375
6331
  return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
6376
6332
  }
6377
6333
  function sampleIndexes(total, limit) {
@@ -6382,7 +6338,7 @@ function digest(value) {
6382
6338
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
6383
6339
  }
6384
6340
  function contentSketch(value) {
6385
- const tokens = words$2(value);
6341
+ const tokens = words$3(value);
6386
6342
  const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
6387
6343
  return {
6388
6344
  version: 1,
@@ -6392,8 +6348,7 @@ function contentSketch(value) {
6392
6348
  hashes
6393
6349
  };
6394
6350
  }
6395
- //#endregion
6396
- //#region packages/core/src/extract/main-content.ts
6351
+ // packages/core/src/extract/main-content.ts
6397
6352
  function parseDocument$1(html) {
6398
6353
  const { document } = parseHTML(html);
6399
6354
  if (document.documentElement?.localName === "html") return document;
@@ -6519,8 +6474,7 @@ async function extractMainMarkdown(fetchResult) {
6519
6474
  truncated: true
6520
6475
  };
6521
6476
  }
6522
- //#endregion
6523
- //#region packages/core/src/extract/google-rich-results.ts
6477
+ // packages/core/src/extract/google-rich-results.ts
6524
6478
  const SUPPORTED_TYPES = /* @__PURE__ */ new Set([
6525
6479
  "Article",
6526
6480
  "BlogPosting",
@@ -6797,8 +6751,7 @@ function selectGoogleRichResultAssessments(assessments, limit = 50) {
6797
6751
  }
6798
6752
  };
6799
6753
  }
6800
- //#endregion
6801
- //#region packages/core/src/extract/structured-data.ts
6754
+ // packages/core/src/extract/structured-data.ts
6802
6755
  function schemaTypeFromIri(value) {
6803
6756
  try {
6804
6757
  const iri = new URL(value);
@@ -7035,8 +6988,7 @@ function extractStructuredData($, baseUrl, options = {}) {
7035
6988
  hasDate: parsed.hasDate || $("[itemprop~=\"datePublished\"], [itemprop~=\"dateModified\"], [property~=\"datePublished\"], [property~=\"dateModified\"]").toArray().some((element) => isValidStructuredDate($(element).attr("content") ?? $(element).attr("datetime") ?? $(element).text()))
7036
6989
  };
7037
6990
  }
7038
- //#endregion
7039
- //#region packages/core/src/extract/page-extractor.ts
6991
+ // packages/core/src/extract/page-extractor.ts
7040
6992
  function metaRefresh($, finalUrl) {
7041
6993
  for (const element of $("meta[http-equiv]").toArray()) {
7042
6994
  if ($(element).attr("http-equiv")?.trim().toLowerCase() !== "refresh") continue;
@@ -7454,8 +7406,7 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
7454
7406
  ]
7455
7407
  };
7456
7408
  }
7457
- //#endregion
7458
- //#region packages/core/src/fetch/crawler-identity.ts
7409
+ // packages/core/src/fetch/crawler-identity.ts
7459
7410
  const SEO_CRAWLER_TOKEN = "SEO-Skill";
7460
7411
  const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
7461
7412
  const SEO_CRAWLER_IDENTITY = {
@@ -7465,8 +7416,7 @@ const SEO_CRAWLER_IDENTITY = {
7465
7416
  userAgent: SEO_CRAWLER_USER_AGENT,
7466
7417
  documentationUrl: "https://seoskill.dev/docs/crawler/"
7467
7418
  };
7468
- //#endregion
7469
- //#region packages/core/src/fetch/access-block.ts
7419
+ // packages/core/src/fetch/access-block.ts
7470
7420
  const CLOUDFLARE_SKIP_DOCS = "https://developers.cloudflare.com/waf/custom-rules/skip/";
7471
7421
  function isHeadersLike(headers) {
7472
7422
  return "get" in headers && typeof headers.get === "function";
@@ -7536,8 +7486,7 @@ function detectAccessBlock(input) {
7536
7486
  }
7537
7487
  };
7538
7488
  }
7539
- //#endregion
7540
- //#region packages/core/src/fetch/http-client.ts
7489
+ // packages/core/src/fetch/http-client.ts
7541
7490
  var ResponseSizeLimitError = class extends Error {
7542
7491
  maxBytes;
7543
7492
  constructor(maxBytes, label = "Response") {
@@ -7604,8 +7553,7 @@ async function readBoundedResponseText(response, maxBytes, label = "Response") {
7604
7553
  }
7605
7554
  return Buffer.concat(chunks, total).toString("utf8");
7606
7555
  }
7607
- //#endregion
7608
- //#region packages/core/src/fetch/page-fetcher/rate-controls.ts
7556
+ // packages/core/src/fetch/page-fetcher/rate-controls.ts
7609
7557
  const HOST_QUEUES = /* @__PURE__ */ new Map();
7610
7558
  const HOST_HEALTH = /* @__PURE__ */ new Map();
7611
7559
  var OriginBackpressureError = class extends Error {
@@ -7750,8 +7698,7 @@ function recordHostFetch(input) {
7750
7698
  }
7751
7699
  return snapshotForHost(input.host);
7752
7700
  }
7753
- //#endregion
7754
- //#region packages/core/src/fetch/page-fetcher/redirects.ts
7701
+ // packages/core/src/fetch/page-fetcher/redirects.ts
7755
7702
  async function fetchWithRedirectChain(url, signal) {
7756
7703
  const redirectChain = [];
7757
7704
  let currentUrl = url;
@@ -7776,8 +7723,7 @@ async function fetchWithRedirectChain(url, signal) {
7776
7723
  }
7777
7724
  throw new Error(`Too many redirects for ${url}`);
7778
7725
  }
7779
- //#endregion
7780
- //#region packages/core/src/fetch/page-fetcher/robots.ts
7726
+ // packages/core/src/fetch/page-fetcher/robots.ts
7781
7727
  const MAX_ROBOTS_RESPONSE_BYTES = 1048576;
7782
7728
  var RobotsAccessError = class extends Error {
7783
7729
  reason;
@@ -7928,8 +7874,7 @@ function createRobotsSession(input = {}) {
7928
7874
  }
7929
7875
  };
7930
7876
  }
7931
- //#endregion
7932
- //#region packages/core/src/fetch/page-fetcher/robots-evidence.ts
7877
+ // packages/core/src/fetch/page-fetcher/robots-evidence.ts
7933
7878
  function pageRobotsEvidence(robots) {
7934
7879
  return {
7935
7880
  url: robots.url,
@@ -7950,8 +7895,7 @@ function diagnosticRobotsEvidence(robots) {
7950
7895
  error: robots.error
7951
7896
  };
7952
7897
  }
7953
- //#endregion
7954
- //#region packages/core/src/fetch/page-fetcher/plain.ts
7898
+ // packages/core/src/fetch/page-fetcher/plain.ts
7955
7899
  const MAX_PAGE_RESPONSE_BYTES = 5242880;
7956
7900
  function encodePageFetchCacheEvidence(result) {
7957
7901
  return JSON.stringify({
@@ -8093,8 +8037,7 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
8093
8037
  });
8094
8038
  return result;
8095
8039
  }
8096
- //#endregion
8097
- //#region packages/core/src/fetch/page-fetcher/browser-path.ts
8040
+ // packages/core/src/fetch/page-fetcher/browser-path.ts
8098
8041
  const PLAYWRIGHT_CORE_VERSION = "1.61.1";
8099
8042
  function systemBrowserCandidates(input) {
8100
8043
  const macApplications = ["/Applications", join(input.homeDirectory, "Applications")];
@@ -8192,8 +8135,7 @@ function browserUnavailableMessage(resolution) {
8192
8135
  "Set SEO_BROWSER_EXECUTABLE_PATH to use a managed browser executable."
8193
8136
  ].join(" ");
8194
8137
  }
8195
- //#endregion
8196
- //#region packages/core/src/fetch/page-fetcher/rendered.ts
8138
+ // packages/core/src/fetch/page-fetcher/rendered.ts
8197
8139
  const DEFAULT_RENDER_TIMEOUT_MS = 2e4;
8198
8140
  const NETWORK_IDLE_TIMEOUT_MS = 3e3;
8199
8141
  const MAX_RENDER_CONTEXTS = 4;
@@ -8423,8 +8365,7 @@ function looksLikeSpa(html) {
8423
8365
  const lower = html.toLowerCase();
8424
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);
8425
8367
  }
8426
- //#endregion
8427
- //#region packages/core/src/fetch/page-fetcher/rendering-difference.ts
8368
+ // packages/core/src/fetch/page-fetcher/rendering-difference.ts
8428
8369
  const MAX_HEADINGS = 50;
8429
8370
  const MAX_TEXT_VALUE_LENGTH = 500;
8430
8371
  function safeText(value) {
@@ -8529,8 +8470,7 @@ function renderingDocumentDifference(raw, rendered) {
8529
8470
  ].filter((field) => JSON.stringify(rawDocument[field]) !== JSON.stringify(renderedDocument[field]))
8530
8471
  };
8531
8472
  }
8532
- //#endregion
8533
- //#region packages/core/src/fetch/page-fetcher/status-probe.ts
8473
+ // packages/core/src/fetch/page-fetcher/status-probe.ts
8534
8474
  async function statusProbe(url, opts) {
8535
8475
  const startedAt = Date.now();
8536
8476
  const host = new URL(url).host;
@@ -8608,8 +8548,7 @@ async function fetchPageStatus(url, opts = {}) {
8608
8548
  const rate = normalizeRateControls(opts.rate);
8609
8549
  return await queueForHost(new URL(url).host, rate).add(() => statusProbe(url, opts));
8610
8550
  }
8611
- //#endregion
8612
- //#region packages/core/src/fetch/page-fetcher.ts
8551
+ // packages/core/src/fetch/page-fetcher.ts
8613
8552
  function normalizeJavaScriptRenderingMode(input) {
8614
8553
  if (input === true) return "on";
8615
8554
  if (input === false) return "off";
@@ -8686,8 +8625,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
8686
8625
  }
8687
8626
  });
8688
8627
  }
8689
- //#endregion
8690
- //#region packages/core/src/gsc/property-url.ts
8628
+ // packages/core/src/gsc/property-url.ts
8691
8629
  function invalid(message) {
8692
8630
  throw new SeoError("INVALID_INPUT", message);
8693
8631
  }
@@ -8880,8 +8818,7 @@ function finalizeUrlInspectionQuota(input) {
8880
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);
8881
8819
  }).immediate();
8882
8820
  }
8883
- //#endregion
8884
- //#region packages/core/src/gsc/client/inspection.ts
8821
+ // packages/core/src/gsc/client/inspection.ts
8885
8822
  function credentialKey(input) {
8886
8823
  return createHash("sha256").update(`${input.clientId}\n${input.accountEmail.toLowerCase()}`).digest("base64url");
8887
8824
  }
@@ -8966,8 +8903,7 @@ async function inspectUrl(input) {
8966
8903
  if (!response.ok) throw inspectionResponseError(response, input);
8967
8904
  return await readJson(response, "URL Inspection");
8968
8905
  }
8969
- //#endregion
8970
- //#region packages/core/src/gsc/client/search-analytics.ts
8906
+ // packages/core/src/gsc/client/search-analytics.ts
8971
8907
  function searchDateRange(days) {
8972
8908
  return finalGscDateRange(days);
8973
8909
  }
@@ -9133,8 +9069,7 @@ async function queryPagesTopQueriesBatch(site, pageUrls, range = 28) {
9133
9069
  retainedRowLimitReached: result.rowsFetched >= retainedRowLimit
9134
9070
  };
9135
9071
  }
9136
- //#endregion
9137
- //#region packages/core/src/gsc/client/sites.ts
9072
+ // packages/core/src/gsc/client/sites.ts
9138
9073
  async function listSites(_refresh = false, accountEmail) {
9139
9074
  const entries = !accountEmail && cloudLinked() ? await allCloudSites() : await listSearchConsoleSites(googleFetchFor((await getAuthorized(accountEmail)).client), LOCAL_SIGN_IN);
9140
9075
  writeToCache((database) => {
@@ -9153,8 +9088,7 @@ async function allCloudSites() {
9153
9088
  }
9154
9089
  return [...best.values()];
9155
9090
  }
9156
- //#endregion
9157
- //#region packages/core/src/analyze/page-technical-signals.ts
9091
+ // packages/core/src/analyze/page-technical-signals.ts
9158
9092
  function normalizePageUrl(value) {
9159
9093
  try {
9160
9094
  const parsed = new URL(value);
@@ -9187,8 +9121,7 @@ function pageTechnicalSignals(input) {
9187
9121
  if (input.page?.canonical && !samePageUrl(input.page.finalUrl, absoluteUrl(input.page.canonical, input.page.finalUrl))) signals.push("canonical-mismatch");
9188
9122
  return signals;
9189
9123
  }
9190
- //#endregion
9191
- //#region packages/core/src/analyze/title-width.ts
9124
+ // packages/core/src/analyze/title-width.ts
9192
9125
  const SERP_TITLE_REFERENCE_WIDTH_PX = 580;
9193
9126
  const SERP_TITLE_WIDTH_PROFILE = {
9194
9127
  id: "arial-20-v1",
@@ -9319,8 +9252,7 @@ function estimateSerpTitleWidth(title) {
9319
9252
  fallbackGraphemes: fallbackGraphemes.slice(0, 10)
9320
9253
  };
9321
9254
  }
9322
- //#endregion
9323
- //#region packages/core/src/analyze/audit-page.ts
9255
+ // packages/core/src/analyze/audit-page.ts
9324
9256
  const AUDIT_PAGE_RULE_IDS = [
9325
9257
  "missing_title",
9326
9258
  "title_too_wide",
@@ -9480,8 +9412,7 @@ async function auditPage(input, dependencies = defaultDependencies$12) {
9480
9412
  report.recommendations = buildRecommendations(report);
9481
9413
  return report;
9482
9414
  }
9483
- //#endregion
9484
- //#region packages/core/src/analyze/content-coverage-analysis.ts
9415
+ // packages/core/src/analyze/content-coverage-analysis.ts
9485
9416
  const STOPWORDS = /* @__PURE__ */ new Set([
9486
9417
  "a",
9487
9418
  "an",
@@ -9707,8 +9638,7 @@ async function verifyQueryContentWith(input) {
9707
9638
  };
9708
9639
  }
9709
9640
  }
9710
- //#endregion
9711
- //#region packages/core/src/analyze/content-coverage.ts
9641
+ // packages/core/src/analyze/content-coverage.ts
9712
9642
  function verifyQueryContent(input) {
9713
9643
  return verifyQueryContentWith({
9714
9644
  ...input,
@@ -9716,8 +9646,7 @@ function verifyQueryContent(input) {
9716
9646
  extract: input.extract ?? extractPage
9717
9647
  });
9718
9648
  }
9719
- //#endregion
9720
- //#region packages/core/src/brand.ts
9649
+ // packages/core/src/brand.ts
9721
9650
  const COUNTRY_CODE_SECOND_LEVELS = /* @__PURE__ */ new Set([
9722
9651
  "ac",
9723
9652
  "co",
@@ -9825,8 +9754,7 @@ function shouldExcludeBrandQuery(input) {
9825
9754
  const terms = input.brandTerms?.length ? input.brandTerms : deriveBrandTerms({ siteUrl: input.siteUrl });
9826
9755
  return isBrandQuery(input.query, terms);
9827
9756
  }
9828
- //#endregion
9829
- //#region packages/core/src/analyze/shared.ts
9757
+ // packages/core/src/analyze/shared.ts
9830
9758
  const CTR_BASELINE = {
9831
9759
  1: .27,
9832
9760
  2: .15,
@@ -9861,8 +9789,7 @@ function jaccard(a, b) {
9861
9789
  function defaultDateRange$1(days = 28, now = /* @__PURE__ */ new Date()) {
9862
9790
  return finalGscDateRange(days, now);
9863
9791
  }
9864
- //#endregion
9865
- //#region packages/core/src/analyze/opportunity-primitives.ts
9792
+ // packages/core/src/analyze/opportunity-primitives.ts
9866
9793
  const MIN_BENCHMARK_URL_IMPRESSIONS = 30;
9867
9794
  const MIN_BENCHMARK_IMPRESSIONS = 1e3;
9868
9795
  const MIN_BENCHMARK_URL_SAMPLES = 5;
@@ -10275,8 +10202,7 @@ function queryOpportunityRecommendation(input) {
10275
10202
  expectedImpact: `About ${Math.round(input.estimatedClickLift)} extra clicks in the selected GSC window if CTR reaches the benchmark.`
10276
10203
  };
10277
10204
  }
10278
- //#endregion
10279
- //#region packages/core/src/analyze/page-opportunities-evidence.ts
10205
+ // packages/core/src/analyze/page-opportunities-evidence.ts
10280
10206
  function verificationFor(input) {
10281
10207
  const signals = pageTechnicalSignals(input);
10282
10208
  if (signals.length > 0) return {
@@ -10341,8 +10267,7 @@ function benchmarkDetails$2(benchmark, excludedTargetRows) {
10341
10267
  excludedTargetRows
10342
10268
  };
10343
10269
  }
10344
- //#endregion
10345
- //#region packages/core/src/analyze/query-quality.ts
10270
+ // packages/core/src/analyze/query-quality.ts
10346
10271
  function hasLetter(value) {
10347
10272
  return /\p{L}/u.test(value);
10348
10273
  }
@@ -10613,8 +10538,7 @@ function countQueryFlags(items) {
10613
10538
  flaggedRows
10614
10539
  };
10615
10540
  }
10616
- //#endregion
10617
- //#region packages/core/src/analyze/page-opportunities-selection.ts
10541
+ // packages/core/src/analyze/page-opportunities-selection.ts
10618
10542
  const DEFAULT_MIN_IMPRESSIONS$1 = 10;
10619
10543
  const DEFAULT_LIMIT$3 = 25;
10620
10544
  const MAX_LIMIT$3 = 100;
@@ -10707,8 +10631,7 @@ function selectBenchmarkRows$1(input) {
10707
10631
  });
10708
10632
  });
10709
10633
  }
10710
- //#endregion
10711
- //#region packages/core/src/analyze/page-opportunities-analysis.ts
10634
+ // packages/core/src/analyze/page-opportunities-analysis.ts
10712
10635
  function compareText$18(left, right) {
10713
10636
  return left < right ? -1 : left > right ? 1 : 0;
10714
10637
  }
@@ -10807,8 +10730,7 @@ function analyzePageOpportunitiesFromRows(input) {
10807
10730
  }
10808
10731
  };
10809
10732
  }
10810
- //#endregion
10811
- //#region packages/core/src/analyze/page-opportunities-presentation.ts
10733
+ // packages/core/src/analyze/page-opportunities-presentation.ts
10812
10734
  const actionPriority = new Map([
10813
10735
  "technical-check",
10814
10736
  "content-gap",
@@ -10872,8 +10794,7 @@ function pageOpportunityRecommendations(analysis) {
10872
10794
  if (gaps >= 3) recommendations.push(`Group the ${gaps} verified content gaps into useful sections rather than adding one thin paragraph per query.`);
10873
10795
  return recommendations;
10874
10796
  }
10875
- //#endregion
10876
- //#region packages/core/src/analyze/page-opportunities.ts
10797
+ // packages/core/src/analyze/page-opportunities.ts
10877
10798
  const MAX_BENCHMARK_ROWS = 1e5;
10878
10799
  const defaultDependencies$11 = {
10879
10800
  searchAnalytics: querySearchAnalytics,
@@ -11074,8 +10995,7 @@ async function pageOpportunitiesReport(input, dependencies = defaultDependencies
11074
10995
  recommendations: pageOpportunityRecommendations(analysis)
11075
10996
  };
11076
10997
  }
11077
- //#endregion
11078
- //#region packages/core/src/analyze/content-optimization.ts
10998
+ // packages/core/src/analyze/content-optimization.ts
11079
10999
  function intentFor(query) {
11080
11000
  const value = query.toLowerCase();
11081
11001
  if (/\b(vs|versus|alternative|alternatives|compare|comparison|best)\b/.test(value)) return "comparison";
@@ -11223,8 +11143,7 @@ function contentOptimizationFromPageOpportunities(sourceReport) {
11223
11143
  async function contentOptimizationReport(input) {
11224
11144
  return contentOptimizationFromPageOpportunities(await pageOpportunitiesReport(input));
11225
11145
  }
11226
- //#endregion
11227
- //#region packages/core/src/analyze/crawler/page-eligibility.ts
11146
+ // packages/core/src/analyze/crawler/page-eligibility.ts
11228
11147
  function comparableUrl(value) {
11229
11148
  if (!value) return void 0;
11230
11149
  try {
@@ -11251,8 +11170,7 @@ function isAuditableHtmlPage(page) {
11251
11170
  const contentAuditAllowed = page.contentAuditAllowed ?? (!page.blocked && page.robotsTxt?.allowed !== false);
11252
11171
  return isSuccessfulResponse(page) && isHtmlPage(page) && !isRedirectedPage(page) && contentAuditAllowed;
11253
11172
  }
11254
- //#endregion
11255
- //#region packages/core/src/analyze/crawler/audit.ts
11173
+ // packages/core/src/analyze/crawler/audit.ts
11256
11174
  function sameUrl(a, b) {
11257
11175
  if (!a || !b) return false;
11258
11176
  try {
@@ -11726,8 +11644,7 @@ function auditCrawlPages(pages, opts = {}) {
11726
11644
  }
11727
11645
  return issues;
11728
11646
  }
11729
- //#endregion
11730
- //#region packages/core/src/analyze/crawler/crawl-skip-reasons.ts
11647
+ // packages/core/src/analyze/crawler/crawl-skip-reasons.ts
11731
11648
  const CRAWL_SKIP_REASONS = [
11732
11649
  "asset-url",
11733
11650
  "configured-exclusion",
@@ -11802,8 +11719,7 @@ function crawlSkipReasonCountsFromRecord(input = {}) {
11802
11719
  count
11803
11720
  })) }).skipReasons;
11804
11721
  }
11805
- //#endregion
11806
- //#region packages/core/src/export/diagnosis-segment-csv.ts
11722
+ // packages/core/src/export/diagnosis-segment-csv.ts
11807
11723
  const dimensions = [
11808
11724
  "page",
11809
11725
  "query",
@@ -11904,8 +11820,7 @@ function diagnosisSegmentCsvFiles(report) {
11904
11820
  }))
11905
11821
  ];
11906
11822
  }
11907
- //#endregion
11908
- //#region packages/core/src/export/csv.ts
11823
+ // packages/core/src/export/csv.ts
11909
11824
  const CSV_SCHEMAS = {
11910
11825
  "priorities.csv": [
11911
11826
  "rank",
@@ -12370,8 +12285,7 @@ function refreshPrioritiesCsvFiles(report) {
12370
12285
  ...prefixCsvFiles("diagnosis", diagnoseCsvFiles(report.output.diagnosis))
12371
12286
  ]);
12372
12287
  }
12373
- //#endregion
12374
- //#region packages/core/src/analyze/crawler/top-fixes.ts
12288
+ // packages/core/src/analyze/crawler/top-fixes.ts
12375
12289
  const SEVERITY_SCORE = {
12376
12290
  high: 5e3,
12377
12291
  medium: 1200,
@@ -12522,8 +12436,7 @@ function reviewObservations(report, filters = {}) {
12522
12436
  recommendation: "review"
12523
12437
  });
12524
12438
  }
12525
- //#endregion
12526
- //#region packages/core/src/analyze/crawler/export.ts
12439
+ // packages/core/src/analyze/crawler/export.ts
12527
12440
  const issueHeaders = [
12528
12441
  "rule_id",
12529
12442
  "title",
@@ -12835,8 +12748,7 @@ function escapeHtml(value) {
12835
12748
  function escapeXml(value) {
12836
12749
  return escapeHtml(value);
12837
12750
  }
12838
- //#endregion
12839
- //#region packages/core/src/analyze/crawler/report.ts
12751
+ // packages/core/src/analyze/crawler/report.ts
12840
12752
  const MAX_CRAWL_PAGES = 1e4;
12841
12753
  const MAX_CRAWL_DEPTH = 64;
12842
12754
  const MAX_CRAWL_CONCURRENCY = 16;
@@ -13275,8 +13187,7 @@ function normalizeLoadedCrawlReport(report) {
13275
13187
  caveats: sanitizeMessages(report.caveats).sort(compareText$17)
13276
13188
  };
13277
13189
  }
13278
- //#endregion
13279
- //#region packages/core/src/analyze/crawler/site-checks-schema.ts
13190
+ // packages/core/src/analyze/crawler/site-checks-schema.ts
13280
13191
  const crawlSiteChecksSchema = z.object({ soft404: z.object({
13281
13192
  status: z.enum([
13282
13193
  "pass",
@@ -13294,8 +13205,7 @@ const crawlSiteChecksSchema = z.object({ soft404: z.object({
13294
13205
  probeLimit: z.number().int().positive(),
13295
13206
  complete: z.boolean()
13296
13207
  }) });
13297
- //#endregion
13298
- //#region packages/core/src/analyze/crawler/site-crawl-ai-schema.ts
13208
+ // packages/core/src/analyze/crawler/site-crawl-ai-schema.ts
13299
13209
  const crawlAiAvailabilitySchema = z.enum([
13300
13210
  "available",
13301
13211
  "absent",
@@ -13380,8 +13290,7 @@ const crawlAiSignalsSchema = z.object({
13380
13290
  contentSignal: z.string().optional()
13381
13291
  }).optional()
13382
13292
  });
13383
- //#endregion
13384
- //#region packages/core/src/analyze/crawler/schemas.ts
13293
+ // packages/core/src/analyze/crawler/schemas.ts
13385
13294
  const crawlerRuleSeveritySchema = z.enum([
13386
13295
  "low",
13387
13296
  "medium",
@@ -14339,8 +14248,7 @@ const crawlerJsonSchemas = {
14339
14248
  requestObservation: z.toJSONSchema(crawlRequestObservationSchema),
14340
14249
  pageSnapshot: z.toJSONSchema(crawlPageSnapshotSchema)
14341
14250
  };
14342
- //#endregion
14343
- //#region packages/core/src/analyze/crawler/agent-ready.ts
14251
+ // packages/core/src/analyze/crawler/agent-ready.ts
14344
14252
  function needsSignIn(page) {
14345
14253
  return page.status === 401 || page.status === 403 || Boolean(page.softAuthenticationGate);
14346
14254
  }
@@ -14391,8 +14299,7 @@ function agentReadyIssues(input) {
14391
14299
  }
14392
14300
  return issues;
14393
14301
  }
14394
- //#endregion
14395
- //#region packages/core/src/analyze/crawler/crawl-caveats.ts
14302
+ // packages/core/src/analyze/crawler/crawl-caveats.ts
14396
14303
  function crawlCaveats(input) {
14397
14304
  const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
14398
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.`);
@@ -14400,8 +14307,7 @@ function crawlCaveats(input) {
14400
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.`);
14401
14308
  return caveats;
14402
14309
  }
14403
- //#endregion
14404
- //#region packages/core/src/analyze/crawler/crawl-control.ts
14310
+ // packages/core/src/analyze/crawler/crawl-control.ts
14405
14311
  const CRAWL_CANCELLED = Symbol("crawl-cancelled");
14406
14312
  function abortController(input) {
14407
14313
  const controller = new AbortController();
@@ -14423,8 +14329,7 @@ function cancellationRace(signal) {
14423
14329
  signal.addEventListener("abort", () => resolve(CRAWL_CANCELLED), { once: true });
14424
14330
  });
14425
14331
  }
14426
- //#endregion
14427
- //#region packages/core/src/analyze/crawler/crawl-memory.ts
14332
+ // packages/core/src/analyze/crawler/crawl-memory.ts
14428
14333
  const MEBIBYTE$1 = 1048576;
14429
14334
  const MIN_CRAWL_MEMORY_LIMIT = 384 * MEBIBYTE$1;
14430
14335
  const MAX_CRAWL_MEMORY_LIMIT = 640 * MEBIBYTE$1;
@@ -14443,8 +14348,7 @@ function crawlRssLimitBytes(totalMemoryBytes) {
14443
14348
  function crawlMemoryPressure(input) {
14444
14349
  return input.memoryUsage.heapUsed + input.memoryUsage.external >= crawlMemoryLimitBytes(input.totalMemoryBytes) || input.memoryUsage.rss >= crawlRssLimitBytes(input.totalMemoryBytes);
14445
14350
  }
14446
- //#endregion
14447
- //#region packages/core/src/analyze/crawler/crawl-url-queue.ts
14351
+ // packages/core/src/analyze/crawler/crawl-url-queue.ts
14448
14352
  function before(left, right) {
14449
14353
  return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
14450
14354
  }
@@ -14523,8 +14427,7 @@ var CrawlUrlQueue = class {
14523
14427
  }
14524
14428
  }
14525
14429
  };
14526
- //#endregion
14527
- //#region packages/core/src/analyze/crawler/external-link-checks.ts
14430
+ // packages/core/src/analyze/crawler/external-link-checks.ts
14528
14431
  const EXTERNAL_LINK_SELECTION_LIMIT = 200;
14529
14432
  const EXTERNAL_LINK_STATES = [
14530
14433
  "available",
@@ -14712,8 +14615,7 @@ async function verifyExternalLinks(input) {
14712
14615
  warnings
14713
14616
  };
14714
14617
  }
14715
- //#endregion
14716
- //#region packages/core/src/analyze/crawler/link-graph.ts
14618
+ // packages/core/src/analyze/crawler/link-graph.ts
14717
14619
  function resolveFetchedAlias(url, aliases) {
14718
14620
  const visited = /* @__PURE__ */ new Set();
14719
14621
  let current = url;
@@ -14733,8 +14635,7 @@ function resolveLinkCountAliases(counts, aliases) {
14733
14635
  }
14734
14636
  return resolved;
14735
14637
  }
14736
- //#endregion
14737
- //#region packages/core/src/analyze/crawler/request-evidence.ts
14638
+ // packages/core/src/analyze/crawler/request-evidence.ts
14738
14639
  function observationFromPage(requestedUrl, page) {
14739
14640
  const observation = {
14740
14641
  requestedUrl,
@@ -14757,8 +14658,7 @@ function observationFromPage(requestedUrl, page) {
14757
14658
  extraction: page.extractionStatus ?? "complete"
14758
14659
  };
14759
14660
  }
14760
- //#endregion
14761
- //#region packages/core/src/analyze/crawler/site-checks.ts
14661
+ // packages/core/src/analyze/crawler/site-checks.ts
14762
14662
  const PROBE_COUNT = 2;
14763
14663
  function probeUrls(startUrl) {
14764
14664
  const origin = new URL(startUrl).origin;
@@ -14826,8 +14726,7 @@ async function collectCrawlSiteChecks(input) {
14826
14726
  }]
14827
14727
  };
14828
14728
  }
14829
- //#endregion
14830
- //#region packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
14729
+ // packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
14831
14730
  const MAX_AUXILIARY_RESPONSE_BYTES = 2097152;
14832
14731
  const LLMS_LINK_LIMIT = 20;
14833
14732
  const AI_BOTS = [
@@ -14929,7 +14828,7 @@ function llmsLinks(text, base) {
14929
14828
  } catch {}
14930
14829
  return links;
14931
14830
  }
14932
- async function checkLlmsTxt(input) {
14831
+ async function checkLlmsTxt$1(input) {
14933
14832
  const result = await get$1(input, new URL("/llms.txt", input.url).toString());
14934
14833
  const availability = result.availability === "available" && isHtml(result.contentType) ? "absent" : result.availability;
14935
14834
  const exists = availability === "available";
@@ -15057,7 +14956,7 @@ async function collectCrawlAiSignals(input) {
15057
14956
  const allows = input.respectRobots ? robots.allows : () => true;
15058
14957
  const startAllowed = allows(input.url);
15059
14958
  const [llmsTxt, agentResources, markdownNegotiation] = await Promise.all([
15060
- checkLlmsTxt(input),
14959
+ checkLlmsTxt$1(input),
15061
14960
  checkAgentResources(input),
15062
14961
  startAllowed ? checkMarkdownNegotiation(input) : Promise.resolve({
15063
14962
  url: input.url,
@@ -15121,8 +15020,7 @@ function linkRelations(header) {
15121
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);
15122
15021
  return [...relations].sort();
15123
15022
  }
15124
- //#endregion
15125
- //#region packages/core/src/analyze/crawler/site-crawl-providers.ts
15023
+ // packages/core/src/analyze/crawler/site-crawl-providers.ts
15126
15024
  function crawlMetricsWindow(now) {
15127
15025
  return {
15128
15026
  ...defaultDateRange$2(28, now),
@@ -15318,8 +15216,7 @@ async function joinSearchMetrics(input) {
15318
15216
  ...warning ? { warning } : {}
15319
15217
  };
15320
15218
  }
15321
- //#endregion
15322
- //#region packages/core/src/analyze/crawler/site-crawl-data-sources.ts
15219
+ // packages/core/src/analyze/crawler/site-crawl-data-sources.ts
15323
15220
  function crawlProviderLimits(input) {
15324
15221
  const normalize = (value, fallback, label) => {
15325
15222
  const limit = value ?? fallback;
@@ -15389,8 +15286,7 @@ async function crawlDataSources(input) {
15389
15286
  });
15390
15287
  return dataSources;
15391
15288
  }
15392
- //#endregion
15393
- //#region packages/core/src/analyze/monitoring/crawl-page.ts
15289
+ // packages/core/src/analyze/monitoring/crawl-page.ts
15394
15290
  function resolvedUrl(href, base) {
15395
15291
  if (!href) return void 0;
15396
15292
  try {
@@ -15584,6 +15480,9 @@ async function crawlOne(url, opts = {}) {
15584
15480
  warning: `${url}: ${message}`
15585
15481
  };
15586
15482
  }
15483
+ return snapshotFetchedPage(url, fetched);
15484
+ }
15485
+ async function snapshotFetchedPage(url, fetched) {
15587
15486
  const contentType = headerValue(fetched.headers, "content-type");
15588
15487
  if (fetched.diagnostics.accessBlock) {
15589
15488
  const page = unextractedPage(fetched, contentType);
@@ -15786,8 +15685,7 @@ async function crawlStatusOnly(url, opts = {}) {
15786
15685
  };
15787
15686
  }
15788
15687
  }
15789
- //#endregion
15790
- //#region packages/core/src/analyze/monitoring/sitemaps.ts
15688
+ // packages/core/src/analyze/monitoring/sitemaps.ts
15791
15689
  const INVALID_LOC_SAMPLE_LIMIT = 10;
15792
15690
  const LASTMOD_SAMPLE_LIMIT = 10;
15793
15691
  const MAX_SITEMAP_BYTES = 52428800;
@@ -16198,8 +16096,7 @@ async function fetchSitemapUrls(input) {
16198
16096
  warnings
16199
16097
  };
16200
16098
  }
16201
- //#endregion
16202
- //#region packages/core/src/providers/cache.ts
16099
+ // packages/core/src/providers/cache.ts
16203
16100
  function stableValue(value) {
16204
16101
  if (Array.isArray(value)) return value.map(stableValue);
16205
16102
  if (!value || typeof value !== "object") return value;
@@ -16285,8 +16182,7 @@ function writeProviderCache(key, input, options = {}) {
16285
16182
  taskIds: normalizedTaskIds
16286
16183
  };
16287
16184
  }
16288
- //#endregion
16289
- //#region packages/core/src/storage/provider-secrets.ts
16185
+ // packages/core/src/storage/provider-secrets.ts
16290
16186
  const KEYRING_SERVICE = "seo";
16291
16187
  const PRIVATE_FILE_MODE$1 = 384;
16292
16188
  const PROVIDER_SECRET_NAMES = {
@@ -16316,7 +16212,7 @@ function tightenFilePermissions(path) {
16316
16212
  if (error.code !== "ENOENT") throw error;
16317
16213
  }
16318
16214
  }
16319
- function readFile$1() {
16215
+ function readFile$2() {
16320
16216
  const path = getSeoCliPaths().providerSecretsFile;
16321
16217
  tightenFilePermissions(path);
16322
16218
  const raw = readJsonFile(path);
@@ -16337,7 +16233,7 @@ function writeFile$1(file) {
16337
16233
  writeJsonAtomic(path, file, PRIVATE_FILE_MODE$1);
16338
16234
  }
16339
16235
  function deleteFileSecret(name) {
16340
- const file = readFile$1();
16236
+ const file = readFile$2();
16341
16237
  if (!(name in file.secrets)) return;
16342
16238
  delete file.secrets[name];
16343
16239
  writeFile$1(file);
@@ -16356,7 +16252,7 @@ async function readProviderSecret(input) {
16356
16252
  source: "keychain"
16357
16253
  };
16358
16254
  } catch {}
16359
- const value = readFile$1().secrets[input.name];
16255
+ const value = readFile$2().secrets[input.name];
16360
16256
  return value ? {
16361
16257
  value,
16362
16258
  source: "file"
@@ -16370,7 +16266,7 @@ async function writeProviderSecret(name, value) {
16370
16266
  deleteFileSecret(name);
16371
16267
  return "keychain";
16372
16268
  } catch {}
16373
- const file = readFile$1();
16269
+ const file = readFile$2();
16374
16270
  file.secrets[name] = normalized;
16375
16271
  writeFile$1(file);
16376
16272
  return "file";
@@ -16391,8 +16287,7 @@ async function deleteManagedProviderSecrets() {
16391
16287
  const failed = results.flatMap((result, index) => result.status === "rejected" ? [MANAGED_PROVIDER_SECRET_NAMES[index]] : []);
16392
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] : [])) });
16393
16289
  }
16394
- //#endregion
16395
- //#region packages/core/src/clicky/credentials.ts
16290
+ // packages/core/src/clicky/credentials.ts
16396
16291
  const CLICKY_SITEKEY_ENV = "SEO_CLICKY_SITEKEY";
16397
16292
  const CLICKY_SITE_KEYS_SECRET = PROVIDER_SECRET_NAMES.clickySiteKeys;
16398
16293
  const storedSiteKeysSchema = z.object({
@@ -16478,8 +16373,7 @@ async function deleteClickySiteKey(siteIdValue) {
16478
16373
  siteKeys: stored.siteKeys
16479
16374
  }));
16480
16375
  }
16481
- //#endregion
16482
- //#region packages/core/src/provider-extensions/contracts.ts
16376
+ // packages/core/src/provider-extensions/contracts.ts
16483
16377
  const identifierPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
16484
16378
  const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/u;
16485
16379
  const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
@@ -16630,8 +16524,7 @@ const providerRegistrationSchema = z.object({
16630
16524
  actionIds.add(action.id);
16631
16525
  }
16632
16526
  });
16633
- //#endregion
16634
- //#region packages/core/src/provider-extensions/credentials.ts
16527
+ // packages/core/src/provider-extensions/credentials.ts
16635
16528
  const storedCredentialsSchema = z.object({
16636
16529
  version: z.literal(1),
16637
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))))
@@ -16701,8 +16594,7 @@ async function deleteProviderExtensionCredentials(input) {
16701
16594
  }
16702
16595
  await writeProviderSecret(PROVIDER_SECRET_NAMES.providerExtensionCredentials, JSON.stringify(stored));
16703
16596
  }
16704
- //#endregion
16705
- //#region packages/core/src/providers/transport.ts
16597
+ // packages/core/src/providers/transport.ts
16706
16598
  function schemaIssueSummary(error) {
16707
16599
  const issues = error.issues.slice(0, 3).map((issue) => {
16708
16600
  return `${issue.path.flatMap((segment) => {
@@ -16819,8 +16711,7 @@ async function providerRequestJson(input) {
16819
16711
  }
16820
16712
  return parsed.data;
16821
16713
  }
16822
- //#endregion
16823
- //#region packages/core/src/provider-extensions/registry.ts
16714
+ // packages/core/src/provider-extensions/registry.ts
16824
16715
  var ProviderExtensionRegistry = class {
16825
16716
  #providers = /* @__PURE__ */ new Map();
16826
16717
  hostFor(input) {
@@ -16853,8 +16744,7 @@ var ProviderExtensionRegistry = class {
16853
16744
  return [...this.#providers.values()].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
16854
16745
  }
16855
16746
  };
16856
- //#endregion
16857
- //#region packages/core/src/provider-extensions/store.ts
16747
+ // packages/core/src/provider-extensions/store.ts
16858
16748
  const PRIVATE_FILE_MODE = 384;
16859
16749
  function emptyStore() {
16860
16750
  return {
@@ -16905,8 +16795,7 @@ function removeInstalledProviderPackage(id) {
16905
16795
  });
16906
16796
  return removed;
16907
16797
  }
16908
- //#endregion
16909
- //#region packages/core/src/provider-extensions/loader.ts
16798
+ // packages/core/src/provider-extensions/loader.ts
16910
16799
  const installedPackageJsonSchema = z.object({
16911
16800
  name: z.string(),
16912
16801
  version: z.string(),
@@ -16985,8 +16874,7 @@ async function loadInstalledProviderExtensions(options = {}) {
16985
16874
  failures
16986
16875
  };
16987
16876
  }
16988
- //#endregion
16989
- //#region packages/core/src/provider-extensions/runtime.ts
16877
+ // packages/core/src/provider-extensions/runtime.ts
16990
16878
  const DEFAULT_MAX_REQUESTS = 8;
16991
16879
  const MAX_RESPONSE_BYTES$1 = 10485760;
16992
16880
  const MAX_REQUEST_BODY_BYTES = 65536;
@@ -17057,8 +16945,7 @@ async function verifyProviderExtension(input) {
17057
16945
  throw safeProviderAdapterError(input.providerId, error, input.credentials);
17058
16946
  }
17059
16947
  }
17060
- //#endregion
17061
- //#region packages/core/src/provider-extensions/analytics.ts
16948
+ // packages/core/src/provider-extensions/analytics.ts
17062
16949
  const CACHE_TTL_MS$1 = 864e5;
17063
16950
  const landingPageResultSchema = z.object({
17064
16951
  metric: z.literal("landing-page-visits"),
@@ -17139,8 +17026,7 @@ async function runAnalyticsProviderLandingPages(input) {
17139
17026
  });
17140
17027
  return parsed;
17141
17028
  }
17142
- //#endregion
17143
- //#region packages/core/src/analyze/workflows/analytics-value.ts
17029
+ // packages/core/src/analyze/workflows/analytics-value.ts
17144
17030
  const defaultDependencies$10 = {
17145
17031
  runGa4Report,
17146
17032
  analyticsLandingPages: runAnalyticsProviderLandingPages
@@ -17367,8 +17253,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
17367
17253
  }
17368
17254
  }[connection.provider](connection);
17369
17255
  }
17370
- //#endregion
17371
- //#region packages/core/src/analyze/crawler/site-crawl-dependencies.ts
17256
+ // packages/core/src/analyze/crawler/site-crawl-dependencies.ts
17372
17257
  function resolveCrawlSiteDependencies(dependencies = {}) {
17373
17258
  const hasInjectedPerPageSearchProvider = Boolean(dependencies.queryPageMetrics || dependencies.queryPageTopQuery);
17374
17259
  const hasInjectedLegacyBulkSearchProvider = Boolean(dependencies.queryPagesMetrics || dependencies.queryPagesTopQueries);
@@ -17393,8 +17278,7 @@ function resolveCrawlSiteDependencies(dependencies = {}) {
17393
17278
  totalMemory: dependencies.totalMemory ?? totalmem
17394
17279
  };
17395
17280
  }
17396
- //#endregion
17397
- //#region packages/core/src/analyze/crawler/sitemap-discovery.ts
17281
+ // packages/core/src/analyze/crawler/sitemap-discovery.ts
17398
17282
  const MAX_SITEMAP_ROOTS = 20;
17399
17283
  function isSameOrigin(url, origin) {
17400
17284
  try {
@@ -17482,8 +17366,7 @@ async function sitemapSeeds(input) {
17482
17366
  }
17483
17367
  };
17484
17368
  }
17485
- //#endregion
17486
- //#region packages/core/src/analyze/crawler/site-crawl.ts
17369
+ // packages/core/src/analyze/crawler/site-crawl.ts
17487
17370
  function normalizeUrl$1(value, base) {
17488
17371
  try {
17489
17372
  const url = base ? new URL(value, base) : new URL(value);
@@ -18087,8 +17970,7 @@ async function crawlSite(input, dependencies) {
18087
17970
  await renderer?.close();
18088
17971
  }
18089
17972
  }
18090
- //#endregion
18091
- //#region packages/core/src/brand-evidence.ts
17973
+ // packages/core/src/brand-evidence.ts
18092
17974
  const NAVIGATIONAL_STEM_THRESHOLDS = {
18093
17975
  maximumPosition: 2,
18094
17976
  minimumCtr: .2,
@@ -18220,8 +18102,7 @@ function resolveReportBrandTerms(input) {
18220
18102
  heuristic: true
18221
18103
  };
18222
18104
  }
18223
- //#endregion
18224
- //#region packages/core/src/analyze/min-impressions-policy.ts
18105
+ // packages/core/src/analyze/min-impressions-policy.ts
18225
18106
  const POSITION_MIN_IMPRESSIONS_SCALE = {
18226
18107
  share: 2e-4,
18227
18108
  floor: 10,
@@ -18273,8 +18154,7 @@ function minImpressionsCaveat(input) {
18273
18154
  const oneIn = Math.round(1 / policy.scale.share).toLocaleString("en-US");
18274
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.`;
18275
18156
  }
18276
- //#endregion
18277
- //#region packages/core/src/analyze/page-fragments.ts
18157
+ // packages/core/src/analyze/page-fragments.ts
18278
18158
  function compareCodepoints(left, right) {
18279
18159
  return left < right ? -1 : left > right ? 1 : 0;
18280
18160
  }
@@ -18350,8 +18230,7 @@ function collapseFragmentPageRows(rows, options = {}) {
18350
18230
  }
18351
18231
  };
18352
18232
  }
18353
- //#endregion
18354
- //#region packages/core/src/analyze/site-ctr-curve.ts
18233
+ // packages/core/src/analyze/site-ctr-curve.ts
18355
18234
  const SITE_CTR_CURVE_FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
18356
18235
  const SITE_CTR_CURVE_METHOD = {
18357
18236
  method: "position_bucket_url_p75_v2",
@@ -18480,8 +18359,7 @@ function createSiteCtrCurve(rows) {
18480
18359
  fallbackSource: SITE_CTR_CURVE_FALLBACK_SOURCE
18481
18360
  });
18482
18361
  }
18483
- //#endregion
18484
- //#region packages/core/src/analyze/ctr-underperformers-analysis.ts
18362
+ // packages/core/src/analyze/ctr-underperformers-analysis.ts
18485
18363
  const CTR_MAX_MIN_IMPRESSIONS = 1e9;
18486
18364
  const CTR_DEFAULT_LIMIT = 25;
18487
18365
  const CTR_MAX_LIMIT = 100;
@@ -18619,8 +18497,7 @@ function analyzeCtrUnderperformersFromRows(input) {
18619
18497
  benchmarkByPosition: benchmarkContext.byPosition
18620
18498
  };
18621
18499
  }
18622
- //#endregion
18623
- //#region packages/core/src/analyze/ctr-underperformers-report.ts
18500
+ // packages/core/src/analyze/ctr-underperformers-report.ts
18624
18501
  const MAX_GSC_ROWS$3 = 1e5;
18625
18502
  const DEFAULT_DAYS$3 = 28;
18626
18503
  const MAX_DAYS$2 = 548;
@@ -18754,8 +18631,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
18754
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."]
18755
18632
  };
18756
18633
  }
18757
- //#endregion
18758
- //#region packages/core/src/analyze/ctr-underperformers.ts
18634
+ // packages/core/src/analyze/ctr-underperformers.ts
18759
18635
  const defaultDependencies$9 = {
18760
18636
  searchAnalytics: querySearchAnalytics,
18761
18637
  now: () => /* @__PURE__ */ new Date()
@@ -18763,16 +18639,14 @@ const defaultDependencies$9 = {
18763
18639
  function ctrUnderperformersReport(input, dependencies = defaultDependencies$9) {
18764
18640
  return ctrUnderperformersReportWith(input, dependencies);
18765
18641
  }
18766
- //#endregion
18767
- //#region packages/core/src/phrasing.ts
18642
+ // packages/core/src/phrasing.ts
18768
18643
  function plural(count, singular, pluralLabel = `${singular}s`) {
18769
18644
  return count === 1 ? singular : pluralLabel;
18770
18645
  }
18771
18646
  function countLabel(count, singular, pluralLabel = `${singular}s`) {
18772
18647
  return `${count.toLocaleString("en-GB")} ${plural(count, singular, pluralLabel)}`;
18773
18648
  }
18774
- //#endregion
18775
- //#region packages/core/src/analyze/diagnosis-priorities.ts
18649
+ // packages/core/src/analyze/diagnosis-priorities.ts
18776
18650
  function topDelta(report) {
18777
18651
  const top = report.items[0];
18778
18652
  if (!top) return "No segment movement found.";
@@ -18830,8 +18704,7 @@ function buildDiagnosisPriorities(input) {
18830
18704
  }
18831
18705
  return priorities.slice(0, 6);
18832
18706
  }
18833
- //#endregion
18834
- //#region packages/core/src/analyze/diagnosis-status.ts
18707
+ // packages/core/src/analyze/diagnosis-status.ts
18835
18708
  function diagnosisPartialReasons(report) {
18836
18709
  const skippedSections = new Set(report.skippedSections?.map((section) => section.section));
18837
18710
  const segmentReasons = [
@@ -18883,8 +18756,7 @@ function diagnosisDataStatus(input) {
18883
18756
  if (input.criticalStatuses.length > 0 && input.criticalStatuses.every((status) => status === "skipped")) return "unavailable";
18884
18757
  return input.skippedSections || input.partialReasons ? "partial" : "complete";
18885
18758
  }
18886
- //#endregion
18887
- //#region packages/core/src/analyze/segment-impact-rows.ts
18759
+ // packages/core/src/analyze/segment-impact-rows.ts
18888
18760
  function fixed(value) {
18889
18761
  return Number(value.toFixed(3));
18890
18762
  }
@@ -18977,8 +18849,7 @@ function compareRetainedSegmentRows(input) {
18977
18849
  unmatched
18978
18850
  };
18979
18851
  }
18980
- //#endregion
18981
- //#region packages/core/src/analyze/segment-impact-analysis.ts
18852
+ // packages/core/src/analyze/segment-impact-analysis.ts
18982
18853
  function verdict$4(input) {
18983
18854
  if (input.status === "empty") return "Search Console returned no retained segment rows for either window.";
18984
18855
  if (!input.matchedRows) return "No segment was retained in both windows, so no movement delta was inferred.";
@@ -19089,8 +18960,7 @@ function analyzeSegmentRows(input) {
19089
18960
  ]
19090
18961
  };
19091
18962
  }
19092
- //#endregion
19093
- //#region packages/core/src/analyze/segment-impact-input.ts
18963
+ // packages/core/src/analyze/segment-impact-input.ts
19094
18964
  const DAY_MS$1 = 864e5;
19095
18965
  const MAX_WINDOW_DAYS = 240;
19096
18966
  function subtractDays(value, days) {
@@ -19145,8 +19015,7 @@ function resolveSegmentRanges(input) {
19145
19015
  function validateSegmentSite(site) {
19146
19016
  if (!site.trim()) throw new SeoError("INVALID_INPUT", "site must not be empty.");
19147
19017
  }
19148
- //#endregion
19149
- //#region packages/core/src/analyze/segment-impact-fallback.ts
19018
+ // packages/core/src/analyze/segment-impact-fallback.ts
19150
19019
  function unavailableSegmentImpactReport(input) {
19151
19020
  const days = segmentRangeDays(input.after);
19152
19021
  const before = segmentComparisonRange(input.after, days);
@@ -19225,8 +19094,7 @@ function unavailableSegmentImpactReport(input) {
19225
19094
  caveats: ["No segment movement conclusion was produced because the provider evidence was unavailable."]
19226
19095
  };
19227
19096
  }
19228
- //#endregion
19229
- //#region packages/core/src/analyze/segment-impact.ts
19097
+ // packages/core/src/analyze/segment-impact.ts
19230
19098
  const DEFAULT_MAX_ROWS = 1e5;
19231
19099
  const defaultDependencies$8 = {
19232
19100
  searchAnalytics: querySearchAnalytics,
@@ -19335,8 +19203,7 @@ async function segmentImpact(input, dependencies = defaultDependencies$8) {
19335
19203
  generatedAt: now.toISOString()
19336
19204
  });
19337
19205
  }
19338
- //#endregion
19339
- //#region packages/core/src/imports/records.ts
19206
+ // packages/core/src/imports/records.ts
19340
19207
  const DEFAULT_IMPORT_STREAM_BYTES = 5e7;
19341
19208
  async function importFile(input) {
19342
19209
  const path = resolve(input.file);
@@ -19543,8 +19410,7 @@ async function readJsonArray(input) {
19543
19410
  if (!Array.isArray(parsed)) throw new SeoError("INVALID_INPUT", `${input.label} JSON must be an array of objects.`);
19544
19411
  return parsed;
19545
19412
  }
19546
- //#endregion
19547
- //#region packages/core/src/providers/imports/research-columns.ts
19413
+ // packages/core/src/providers/imports/research-columns.ts
19548
19414
  const RESEARCH_IMPORT_COLUMN_KEYS = [
19549
19415
  "keyword",
19550
19416
  "url",
@@ -20159,8 +20025,7 @@ async function importResearchRows(source, now = /* @__PURE__ */ new Date()) {
20159
20025
  warnings
20160
20026
  };
20161
20027
  }
20162
- //#endregion
20163
- //#region packages/core/src/providers/imports/research-provider.ts
20028
+ // packages/core/src/providers/imports/research-provider.ts
20164
20029
  function compareText$11(left, right) {
20165
20030
  return left < right ? -1 : left > right ? 1 : 0;
20166
20031
  }
@@ -20201,8 +20066,7 @@ async function combineResearchRows(sources, now = /* @__PURE__ */ new Date()) {
20201
20066
  warnings
20202
20067
  };
20203
20068
  }
20204
- //#endregion
20205
- //#region packages/core/src/analyze/page-patterns.ts
20069
+ // packages/core/src/analyze/page-patterns.ts
20206
20070
  const LOCAL_QUERY_TERMS = [
20207
20071
  "near me",
20208
20072
  "today",
@@ -20373,8 +20237,7 @@ function dominantTemplateFamily(rows) {
20373
20237
  share: top.count / rows.length
20374
20238
  };
20375
20239
  }
20376
- //#endregion
20377
- //#region packages/core/src/analyze/site-diagnostics/cannibal-analysis-primitives.ts
20240
+ // packages/core/src/analyze/site-diagnostics/cannibal-analysis-primitives.ts
20378
20241
  function compareCannibalText(left, right) {
20379
20242
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
20380
20243
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
@@ -20419,8 +20282,7 @@ function addCannibalRow(group, url, row) {
20419
20282
  position: weightedPosition / impressions
20420
20283
  });
20421
20284
  }
20422
- //#endregion
20423
- //#region packages/core/src/analyze/site-diagnostics/cannibal-item.ts
20285
+ // packages/core/src/analyze/site-diagnostics/cannibal-item.ts
20424
20286
  function cannibalReviewContext(query, rows) {
20425
20287
  const context = [];
20426
20288
  if (isQuotedQuery(query)) context.push("quoted-query");
@@ -20498,8 +20360,7 @@ function createCannibalItem(input) {
20498
20360
  function compareCannibalItems(left, right) {
20499
20361
  return right.priority.score - left.priority.score || right.priority.demandImpressions - left.priority.demandImpressions || compareCannibalText(left.query, right.query);
20500
20362
  }
20501
- //#endregion
20502
- //#region packages/core/src/analyze/site-diagnostics/cannibal-import.ts
20363
+ // packages/core/src/analyze/site-diagnostics/cannibal-import.ts
20503
20364
  const MAX_SUPPRESSIONS$1 = 100;
20504
20365
  function siteScope(site) {
20505
20366
  const withoutScDomain = site.trim().replace(/^sc-domain:/iu, "");
@@ -20777,8 +20638,7 @@ async function cannibalImportReport(input, dependencies = {}) {
20777
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."]
20778
20639
  };
20779
20640
  }
20780
- //#endregion
20781
- //#region packages/core/src/storage/ledger.ts
20641
+ // packages/core/src/storage/ledger.ts
20782
20642
  var SessionLedger = class {
20783
20643
  entries = /* @__PURE__ */ new Map();
20784
20644
  gscCalls = 0;
@@ -20808,8 +20668,7 @@ var SessionLedger = class {
20808
20668
  return providerBits.join(" · ");
20809
20669
  }
20810
20670
  };
20811
- //#endregion
20812
- //#region packages/core/src/analyze/site-diagnostics/cannibal-analysis.ts
20671
+ // packages/core/src/analyze/site-diagnostics/cannibal-analysis.ts
20813
20672
  const MINIMUM_PAGE_SHARE = .1;
20814
20673
  const MINIMUM_PAGE_IMPRESSIONS = 10;
20815
20674
  const MAXIMUM_DOMINANT_SHARE = .8;
@@ -20971,10 +20830,20 @@ function analyzeCannibalRows(input) {
20971
20830
  templates: summarizeTemplates(returnedItems.flatMap((item) => item.pages))
20972
20831
  };
20973
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;
20974
20844
  const CANNIBAL_MINIMUM_PAGE_SHARE = MINIMUM_PAGE_SHARE;
20975
20845
  const CANNIBAL_MAXIMUM_DOMINANT_SHARE = MAXIMUM_DOMINANT_SHARE;
20976
- //#endregion
20977
- //#region packages/core/src/analyze/site-diagnostics/cannibal-report.ts
20846
+ // packages/core/src/analyze/site-diagnostics/cannibal-report.ts
20978
20847
  const MAX_GSC_ROWS$2 = 1e5;
20979
20848
  function verdict$3(input) {
20980
20849
  if (input.status === "empty") return "Search Console returned no retained query/page rows for this window.";
@@ -21043,7 +20912,7 @@ async function cannibalReportWith(input, dependencies) {
21043
20912
  const possiblyTruncated = pageTruncated || propertyTruncated;
21044
20913
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
21045
20914
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
21046
- 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);
21047
20916
  return {
21048
20917
  schemaVersion: 1,
21049
20918
  dataSource: "search-console-api",
@@ -21127,8 +20996,7 @@ async function cannibalReportWith(input, dependencies) {
21127
20996
  ledgerSummary: ledger.summary()
21128
20997
  };
21129
20998
  }
21130
- //#endregion
21131
- //#region packages/core/src/analyze/site-diagnostics/cannibal.ts
20999
+ // packages/core/src/analyze/site-diagnostics/cannibal.ts
21132
21000
  const defaultDependencies$7 = {
21133
21001
  searchAnalytics: querySearchAnalytics,
21134
21002
  now: () => /* @__PURE__ */ new Date()
@@ -21153,8 +21021,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$7) {
21153
21021
  }
21154
21022
  return cannibalReportWith(input, dependencies);
21155
21023
  }
21156
- //#endregion
21157
- //#region packages/core/src/analyze/site-diagnostics/decay-analysis-primitives.ts
21024
+ // packages/core/src/analyze/site-diagnostics/decay-analysis-primitives.ts
21158
21025
  function compareDecayText(left, right) {
21159
21026
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
21160
21027
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
@@ -21293,8 +21160,7 @@ function boundedDecayNumber(value, fallback, min, max) {
21293
21160
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21294
21161
  return Math.min(max, Math.max(min, value));
21295
21162
  }
21296
- //#endregion
21297
- //#region packages/core/src/analyze/site-diagnostics/decay-analysis.ts
21163
+ // packages/core/src/analyze/site-diagnostics/decay-analysis.ts
21298
21164
  function analyzeDecay(input) {
21299
21165
  const minDropPct = boundedDecayNumber(input.minDropPct, 20, 0, 100);
21300
21166
  const minPreviousClicks = boundedDecayNumber(input.minPreviousClicks, 2, 0, 1e9);
@@ -21417,8 +21283,7 @@ function analyzeDecay(input) {
21417
21283
  templates: summarizeTemplates(returned)
21418
21284
  };
21419
21285
  }
21420
- //#endregion
21421
- //#region packages/core/src/analyze/site-diagnostics/decaying-report.ts
21286
+ // packages/core/src/analyze/site-diagnostics/decaying-report.ts
21422
21287
  const MAX_GSC_ROWS$1 = 1e5;
21423
21288
  function verdict$2(report) {
21424
21289
  if (report.dataStatus === "empty") return "Search Console returned no retained query/page rows for either comparison window.";
@@ -21589,8 +21454,7 @@ async function decayingReportWith(input, dependencies) {
21589
21454
  ]
21590
21455
  };
21591
21456
  }
21592
- //#endregion
21593
- //#region packages/core/src/analyze/site-diagnostics/decaying.ts
21457
+ // packages/core/src/analyze/site-diagnostics/decaying.ts
21594
21458
  const defaultDependencies$6 = {
21595
21459
  searchAnalytics: querySearchAnalytics,
21596
21460
  now: () => /* @__PURE__ */ new Date()
@@ -21598,8 +21462,7 @@ const defaultDependencies$6 = {
21598
21462
  function decayingReport(input, dependencies = defaultDependencies$6) {
21599
21463
  return decayingReportWith(input, dependencies);
21600
21464
  }
21601
- //#endregion
21602
- //#region packages/core/src/analyze/site-diagnostics/quick-win-groups.ts
21465
+ // packages/core/src/analyze/site-diagnostics/quick-win-groups.ts
21603
21466
  function normalizedQuery(value) {
21604
21467
  return value.normalize("NFKC").toLocaleLowerCase("und").replace(/\s+/gu, " ").trim();
21605
21468
  }
@@ -21644,8 +21507,7 @@ function groupQuickWins(items) {
21644
21507
  return group;
21645
21508
  }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$10(left.id, right.id));
21646
21509
  }
21647
- //#endregion
21648
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
21510
+ // packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
21649
21511
  function boundedInteger$3(value, fallback, minimum, maximum) {
21650
21512
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21651
21513
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
@@ -21745,8 +21607,7 @@ function compareQuickWinItems(left, right) {
21745
21607
  function roundedSum(values) {
21746
21608
  return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
21747
21609
  }
21748
- //#endregion
21749
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis.ts
21610
+ // packages/core/src/analyze/site-diagnostics/quick-wins-analysis.ts
21750
21611
  const MAX_MIN_IMPRESSIONS$2 = 1e9;
21751
21612
  const DEFAULT_LIMIT$2 = 25;
21752
21613
  const MAX_LIMIT$2 = 100;
@@ -21853,8 +21714,7 @@ function analyzeQuickWinsFromRows(input) {
21853
21714
  benchmarkByPosition: benchmarkContext.byPosition
21854
21715
  };
21855
21716
  }
21856
- //#endregion
21857
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
21717
+ // packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
21858
21718
  function compareText$8(left, right) {
21859
21719
  return left < right ? -1 : left > right ? 1 : 0;
21860
21720
  }
@@ -21903,8 +21763,7 @@ function quickWinTemplateSummaries(items) {
21903
21763
  sampleUrls: [...item.urls].sort(compareText$8).slice(0, 3)
21904
21764
  })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$8(left.id, right.id)).slice(0, 5);
21905
21765
  }
21906
- //#endregion
21907
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
21766
+ // packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
21908
21767
  const DEFAULT_DAYS$2 = 28;
21909
21768
  const MAX_DAYS$1 = 548;
21910
21769
  const DEFAULT_VERIFY_LIMIT$1 = 5;
@@ -22090,8 +21949,7 @@ async function quickWinsReportWith(input, dependencies) {
22090
21949
  warnings
22091
21950
  };
22092
21951
  }
22093
- //#endregion
22094
- //#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
21952
+ // packages/core/src/analyze/site-diagnostics/quick-wins.ts
22095
21953
  const defaultDependencies$5 = {
22096
21954
  searchAnalytics: querySearchAnalytics,
22097
21955
  fetch: fetchPage,
@@ -22101,8 +21959,7 @@ const defaultDependencies$5 = {
22101
21959
  function quickWinsReport(input, dependencies = defaultDependencies$5) {
22102
21960
  return quickWinsReportWith(input, dependencies);
22103
21961
  }
22104
- //#endregion
22105
- //#region packages/core/src/analyze/striking-distance-analysis-primitives.ts
21962
+ // packages/core/src/analyze/striking-distance-analysis-primitives.ts
22106
21963
  function compareText$7(left, right) {
22107
21964
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
22108
21965
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
@@ -22209,8 +22066,7 @@ function groupStrikingDistanceItems(items) {
22209
22066
  };
22210
22067
  }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$7(left.id, right.id));
22211
22068
  }
22212
- //#endregion
22213
- //#region packages/core/src/analyze/striking-distance-analysis.ts
22069
+ // packages/core/src/analyze/striking-distance-analysis.ts
22214
22070
  const MAX_MIN_IMPRESSIONS$1 = 1e9;
22215
22071
  const DEFAULT_LIMIT$1 = 25;
22216
22072
  const MAX_LIMIT$1 = 100;
@@ -22349,8 +22205,7 @@ function analyzeStrikingDistanceRows(input) {
22349
22205
  items
22350
22206
  };
22351
22207
  }
22352
- //#endregion
22353
- //#region packages/core/src/analyze/striking-distance-report.ts
22208
+ // packages/core/src/analyze/striking-distance-report.ts
22354
22209
  const DEFAULT_DAYS$1 = 28;
22355
22210
  const MAX_DAYS = 548;
22356
22211
  const DEFAULT_VERIFY_LIMIT = 5;
@@ -22505,8 +22360,7 @@ async function strikingDistanceWith(input, dependencies) {
22505
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."]
22506
22361
  };
22507
22362
  }
22508
- //#endregion
22509
- //#region packages/core/src/analyze/striking-distance.ts
22363
+ // packages/core/src/analyze/striking-distance.ts
22510
22364
  const defaultDependencies$4 = {
22511
22365
  searchAnalytics: querySearchAnalytics,
22512
22366
  verifyContent: verifyQueryContent,
@@ -22515,8 +22369,7 @@ const defaultDependencies$4 = {
22515
22369
  function strikingDistance(input, dependencies = defaultDependencies$4) {
22516
22370
  return strikingDistanceWith(input, dependencies);
22517
22371
  }
22518
- //#endregion
22519
- //#region packages/core/src/updates/search-status.ts
22372
+ // packages/core/src/updates/search-status.ts
22520
22373
  const INCIDENTS_URL = "https://status.search.google.com/incidents.json";
22521
22374
  async function fetchSearchStatusIncidents() {
22522
22375
  const response = await fetch$1(INCIDENTS_URL, { signal: AbortSignal.timeout(8e3) });
@@ -22526,8 +22379,7 @@ async function fetchSearchStatusIncidents() {
22526
22379
  async function listSearchUpdates(input = {}) {
22527
22380
  return searchUpdatesFromIncidents(withRankingHistory(await fetchSearchStatusIncidents()), input);
22528
22381
  }
22529
- //#endregion
22530
- //#region packages/core/src/analyze/traffic-anomaly-analysis.ts
22382
+ // packages/core/src/analyze/traffic-anomaly-analysis.ts
22531
22383
  /** The step-change heuristic and its thresholds, returned with the report. */
22532
22384
  const STEP_CHANGE_RULES = {
22533
22385
  method: "best-split",
@@ -22883,8 +22735,7 @@ function analyzeTrafficAnomalyRows(input) {
22883
22735
  }
22884
22736
  };
22885
22737
  }
22886
- //#endregion
22887
- //#region packages/core/src/analyze/traffic-anomaly-report.ts
22738
+ // packages/core/src/analyze/traffic-anomaly-report.ts
22888
22739
  async function trafficAnomalyWith(input, dependencies) {
22889
22740
  const days = input.days ?? 90;
22890
22741
  const recentDays = input.recentDays ?? 7;
@@ -22927,8 +22778,7 @@ async function trafficAnomalyWith(input, dependencies) {
22927
22778
  detection: analyzed.detection
22928
22779
  };
22929
22780
  }
22930
- //#endregion
22931
- //#region packages/core/src/analyze/traffic-anomaly.ts
22781
+ // packages/core/src/analyze/traffic-anomaly.ts
22932
22782
  const defaultDependencies$3 = {
22933
22783
  searchAnalytics: querySearchAnalytics,
22934
22784
  now: () => /* @__PURE__ */ new Date()
@@ -22975,8 +22825,7 @@ async function updateCorrelation(input) {
22975
22825
  })
22976
22826
  };
22977
22827
  }
22978
- //#endregion
22979
- //#region packages/core/src/analyze/diagnose-property.ts
22828
+ // packages/core/src/analyze/diagnose-property.ts
22980
22829
  function errorReason(error) {
22981
22830
  return error instanceof Error ? error.message : String(error);
22982
22831
  }
@@ -23512,8 +23361,7 @@ async function diagnoseProperty(input, dependencies = {}) {
23512
23361
  quickWins
23513
23362
  };
23514
23363
  }
23515
- //#endregion
23516
- //#region packages/core/src/decision-model.ts
23364
+ // packages/core/src/decision-model.ts
23517
23365
  var DecisionModelError = class extends Error {
23518
23366
  kind;
23519
23367
  retryAfterMs;
@@ -23666,8 +23514,7 @@ var DecisionRunner = class {
23666
23514
  return null;
23667
23515
  }
23668
23516
  };
23669
- //#endregion
23670
- //#region packages/core/src/analyze/internal-links-index.ts
23517
+ // packages/core/src/analyze/internal-links-index.ts
23671
23518
  const PAGE_TERMS = 300;
23672
23519
  const VECTOR_TERMS = 100;
23673
23520
  var SiteIndex = class {
@@ -23794,7 +23641,7 @@ const GENERIC_ANCHORS = /^(click here|here|read more|learn more|more|this|this p
23794
23641
  function normalise(value) {
23795
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();
23796
23643
  }
23797
- const words$1 = (value) => normalise(value).split(" ").filter(Boolean);
23644
+ const words$2 = (value) => normalise(value).split(" ").filter(Boolean);
23798
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(" "));
23799
23646
  const NOT_PLURAL = /* @__PURE__ */ new Set([
23800
23647
  "always",
@@ -23855,7 +23702,7 @@ function stem(word) {
23855
23702
  if (one.length > 6 && one.endsWith("ing")) return one.slice(0, -3);
23856
23703
  return one;
23857
23704
  }
23858
- 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);
23859
23706
  const slugWords = (url) => {
23860
23707
  try {
23861
23708
  return new URL(url).pathname.split(/[/._-]+/).filter(Boolean).join(" ");
@@ -24141,8 +23988,7 @@ function placementPage(input) {
24141
23988
  sentences: pageSentences(input.html)
24142
23989
  };
24143
23990
  }
24144
- //#endregion
24145
- //#region packages/core/src/analyze/internal-links-crawl.ts
23991
+ // packages/core/src/analyze/internal-links-crawl.ts
24146
23992
  const DEFAULT_MAX_PAGES = 500;
24147
23993
  const MAX_MAX_PAGES = 5e3;
24148
23994
  const FETCH_CONCURRENCY = 6;
@@ -24514,8 +24360,7 @@ async function crawl(input, robots, sentences, renderer, warnings) {
24514
24360
  warnings
24515
24361
  };
24516
24362
  }
24517
- //#endregion
24518
- //#region packages/core/src/analyze/internal-links-judge.ts
24363
+ // packages/core/src/analyze/internal-links-judge.ts
24519
24364
  const PAGE_TYPES = {
24520
24365
  pillar_guide: "A long, broad guide that covers a whole topic and would naturally link out to many narrower pages",
24521
24366
  support_article: "A focused article, post, tutorial, or explainer about one narrower subject",
@@ -24739,8 +24584,7 @@ function decideWithoutModel(placement, similarity) {
24739
24584
  anchor: placement.anchors.find((option) => option.sentence === sentence?.id)?.text ?? null
24740
24585
  };
24741
24586
  }
24742
- //#endregion
24743
- //#region packages/core/src/analyze/internal-links-report.ts
24587
+ // packages/core/src/analyze/internal-links-report.ts
24744
24588
  const DAY$1 = 864e5;
24745
24589
  const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
24746
24590
  function internalLinksReportRange(input, fallbackDays, now) {
@@ -24768,8 +24612,7 @@ function internalLinksVerdict(report) {
24768
24612
  const capped = report.crawl.capReached ? ` The crawl stopped at its limit of ${report.crawl.pageLimit} pages, so pages beyond it were not checked.` : "";
24769
24613
  return `${judged} ${scope}${counts.heldBack ? `; ${counts.heldBack} held back by the evidence floors` : ""}.${capped}`;
24770
24614
  }
24771
- //#endregion
24772
- //#region packages/core/src/analyze/internal-links.ts
24615
+ // packages/core/src/analyze/internal-links.ts
24773
24616
  const GSC_DAYS = 90;
24774
24617
  const MAX_GSC_ROWS = 1e5;
24775
24618
  const MAX_PAGES_PER_QUERY = 25;
@@ -25278,8 +25121,7 @@ function caveats$1(report) {
25278
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."
25279
25122
  ];
25280
25123
  }
25281
- //#endregion
25282
- //#region packages/core/src/analyze/url-templates.ts
25124
+ // packages/core/src/analyze/url-templates.ts
25283
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;
25284
25126
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25285
25127
  function compareText$6(left, right) {
@@ -25408,8 +25250,7 @@ function clusterPseoTemplates(urls, opts = {}) {
25408
25250
  sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
25409
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);
25410
25252
  }
25411
- //#endregion
25412
- //#region packages/core/src/analyze/monitoring/index-coverage.ts
25253
+ // packages/core/src/analyze/monitoring/index-coverage.ts
25413
25254
  const DEFAULT_ITEMS_PER_SECTION = 100;
25414
25255
  const MAX_ITEMS_PER_SECTION = 1e3;
25415
25256
  const DEFAULT_TEMPLATE_LIMIT = 50;
@@ -25752,8 +25593,7 @@ function analyzeIndexCoverageSignals(input) {
25752
25593
  })
25753
25594
  };
25754
25595
  }
25755
- //#endregion
25756
- //#region packages/core/src/analyze/monitoring/index-coverage-report.ts
25596
+ // packages/core/src/analyze/monitoring/index-coverage-report.ts
25757
25597
  const MAX_SITEMAP_DOCUMENTS = 20;
25758
25598
  const MAX_SEARCH_ROWS = 25e4;
25759
25599
  const MAX_SITEMAP_URLS = 25e4;
@@ -25883,8 +25723,7 @@ async function indexCoverageSignals(input, dependencies = defaultDependencies$1)
25883
25723
  }
25884
25724
  };
25885
25725
  }
25886
- //#endregion
25887
- //#region packages/core/src/analyze/monitoring/redirect-trace.ts
25726
+ // packages/core/src/analyze/monitoring/redirect-trace.ts
25888
25727
  function resolveNextUrl(location, baseUrl) {
25889
25728
  try {
25890
25729
  const next = new URL(location, baseUrl);
@@ -25986,8 +25825,7 @@ async function redirectTrace(input) {
25986
25825
  warnings
25987
25826
  };
25988
25827
  }
25989
- //#endregion
25990
- //#region packages/core/src/analyze/monitoring/link-recover.ts
25828
+ // packages/core/src/analyze/monitoring/link-recover.ts
25991
25829
  function primaryIssue(issues) {
25992
25830
  return [
25993
25831
  "final-5xx",
@@ -26124,8 +25962,7 @@ async function linkRecover(input) {
26124
25962
  warnings: [...new Set(warnings)].slice(0, 50)
26125
25963
  };
26126
25964
  }
26127
- //#endregion
26128
- //#region packages/core/src/analyze/performance-analysis.ts
25965
+ // packages/core/src/analyze/performance-analysis.ts
26129
25966
  const FIELD_THRESHOLDS = {
26130
25967
  CLS: {
26131
25968
  good: .1,
@@ -26333,8 +26170,7 @@ function performanceActions(report) {
26333
26170
  });
26334
26171
  return actions;
26335
26172
  }
26336
- //#endregion
26337
- //#region packages/core/src/analyze/performance-crux.ts
26173
+ // packages/core/src/analyze/performance-crux.ts
26338
26174
  const CRUX_METRICS = [
26339
26175
  "cumulative_layout_shift",
26340
26176
  "interaction_to_next_paint",
@@ -26463,8 +26299,7 @@ async function fetchCruxFieldData(input) {
26463
26299
  };
26464
26300
  }
26465
26301
  }
26466
- //#endregion
26467
- //#region packages/core/src/analyze/performance-lighthouse.ts
26302
+ // packages/core/src/analyze/performance-lighthouse.ts
26468
26303
  const execFileAsync$1 = promisify(execFile);
26469
26304
  const INSIGHT_BYTE_BUDGET = 24e3;
26470
26305
  function compactRecord(value) {
@@ -26655,8 +26490,7 @@ async function runLighthouse(input) {
26655
26490
  }
26656
26491
  return report;
26657
26492
  }
26658
- //#endregion
26659
- //#region packages/core/src/analyze/performance.ts
26493
+ // packages/core/src/analyze/performance.ts
26660
26494
  const CACHE_TTL_MS = 864e5;
26661
26495
  function reportId(input) {
26662
26496
  return `perf_${createHash("sha256").update(JSON.stringify({
@@ -26846,8 +26680,7 @@ async function performanceAudit(input) {
26846
26680
  })) cacheSet(report, now.getTime());
26847
26681
  return report;
26848
26682
  }
26849
- //#endregion
26850
- //#region packages/core/src/analyze/query-cluster-primitives.ts
26683
+ // packages/core/src/analyze/query-cluster-primitives.ts
26851
26684
  const CLUSTER_THRESHOLD = .5;
26852
26685
  const MAX_TOKEN_BUCKET = 250;
26853
26686
  const MAX_TOKEN_SHARE = .08;
@@ -26963,8 +26796,7 @@ function* iterateQueryClusters(rows) {
26963
26796
  yield group;
26964
26797
  }
26965
26798
  }
26966
- //#endregion
26967
- //#region packages/core/src/analyze/query-cluster-analysis.ts
26799
+ // packages/core/src/analyze/query-cluster-analysis.ts
26968
26800
  function quotedLabel(label) {
26969
26801
  return label.includes("\"") ? `'${label}'` : `"${label}"`;
26970
26802
  }
@@ -27184,8 +27016,7 @@ function analyzeQueryClustersFromRows(input) {
27184
27016
  limit
27185
27017
  };
27186
27018
  }
27187
- //#endregion
27188
- //#region packages/core/src/analyze/query-cluster.ts
27019
+ // packages/core/src/analyze/query-cluster.ts
27189
27020
  function* retainedQueryClusterRows(input) {
27190
27021
  for (const row of input.rows) {
27191
27022
  const query = row.keys[0] ?? "";
@@ -27292,15 +27123,13 @@ function buildQueryClusterReportFromRows(input) {
27292
27123
  recommendations: clusters.filter((cluster) => cluster.recommendation).slice(0, 5).map((cluster) => cluster.recommendation ?? "")
27293
27124
  };
27294
27125
  }
27295
- //#endregion
27296
- //#region packages/core/src/analyze/reports/dates.ts
27126
+ // packages/core/src/analyze/reports/dates.ts
27297
27127
  function rangeDays(range) {
27298
27128
  const start = Date.parse(`${range.startDate}T00:00:00Z`);
27299
27129
  const end = Date.parse(`${range.endDate}T00:00:00Z`);
27300
27130
  return Math.floor((end - start) / 864e5) + 1;
27301
27131
  }
27302
- //#endregion
27303
- //#region packages/core/src/analyze/reports/markdown.ts
27132
+ // packages/core/src/analyze/reports/markdown.ts
27304
27133
  function renderMarkdown(report) {
27305
27134
  const lines = [
27306
27135
  `# SEO report: ${report.site}`,
@@ -27324,8 +27153,7 @@ function renderMarkdown(report) {
27324
27153
  for (const priority of report.priorities) lines.push(`- ${priority.title} (${priority.confidence}): ${priority.action}`);
27325
27154
  return lines.join("\n");
27326
27155
  }
27327
- //#endregion
27328
- //#region packages/core/src/analyze/reports/sections.ts
27156
+ // packages/core/src/analyze/reports/sections.ts
27329
27157
  function skippedReason(report, section) {
27330
27158
  return report.skippedSections?.find((item) => item.section === section)?.reason;
27331
27159
  }
@@ -27460,8 +27288,7 @@ function contentOpportunityBullets(report) {
27460
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.");
27461
27289
  return bullets;
27462
27290
  }
27463
- //#endregion
27464
- //#region packages/core/src/analyze/reports/narrative.ts
27291
+ // packages/core/src/analyze/reports/narrative.ts
27465
27292
  function reportCaveats(input) {
27466
27293
  return [
27467
27294
  `Date window: ${input.period.startDate} to ${input.period.endDate}.`,
@@ -27543,8 +27370,7 @@ async function reportNarrative(input) {
27543
27370
  markdown: renderMarkdown(report)
27544
27371
  };
27545
27372
  }
27546
- //#endregion
27547
- //#region packages/core/src/analyze/second-page-analysis.ts
27373
+ // packages/core/src/analyze/second-page-analysis.ts
27548
27374
  const DEFAULT_MIN_IMPRESSIONS = 50;
27549
27375
  const MAX_MIN_IMPRESSIONS = 1e9;
27550
27376
  const DEFAULT_LIMIT = 10;
@@ -27748,8 +27574,7 @@ function analyzeSecondPageRows(input) {
27748
27574
  items
27749
27575
  };
27750
27576
  }
27751
- //#endregion
27752
- //#region packages/core/src/analyze/second-page-report.ts
27577
+ // packages/core/src/analyze/second-page-report.ts
27753
27578
  const MAX_SOURCE_ROWS = 1e5;
27754
27579
  function integerOption(input) {
27755
27580
  if (input.value === void 0) return input.fallback;
@@ -27966,8 +27791,7 @@ async function secondPageWith(input, dependencies) {
27966
27791
  warnings: uniqueWarnings(warnings)
27967
27792
  };
27968
27793
  }
27969
- //#endregion
27970
- //#region packages/core/src/analyze/second-page.ts
27794
+ // packages/core/src/analyze/second-page.ts
27971
27795
  const defaultDependencies = {
27972
27796
  searchAnalytics: querySearchAnalytics,
27973
27797
  fetch: fetchPage,
@@ -27977,8 +27801,7 @@ const defaultDependencies = {
27977
27801
  function secondPage(input, dependencies = defaultDependencies) {
27978
27802
  return secondPageWith(input, dependencies);
27979
27803
  }
27980
- //#endregion
27981
- //#region packages/core/src/analyze/workflows/priority-scoring.ts
27804
+ // packages/core/src/analyze/workflows/priority-scoring.ts
27982
27805
  const SOURCE_WEIGHT = {
27983
27806
  decay: 1.25,
27984
27807
  "quick-win": 1.15,
@@ -28047,8 +27870,7 @@ function scorePriority(input) {
28047
27870
  final: Number(final.toFixed(2))
28048
27871
  };
28049
27872
  }
28050
- //#endregion
28051
- //#region packages/core/src/analyze/workflows/report.ts
27873
+ // packages/core/src/analyze/workflows/report.ts
28052
27874
  function workflowReport(input) {
28053
27875
  return {
28054
27876
  workflow: input.workflow,
@@ -28060,8 +27882,7 @@ function workflowReport(input) {
28060
27882
  output: input.output
28061
27883
  };
28062
27884
  }
28063
- //#endregion
28064
- //#region packages/core/src/analyze/workflows/crawl-implementation-queue.ts
27885
+ // packages/core/src/analyze/workflows/crawl-implementation-queue.ts
28065
27886
  const CATEGORY_TO_PRIORITY = {
28066
27887
  canonical: "technical",
28067
27888
  content: "content",
@@ -28197,8 +28018,7 @@ async function crawlImplementationQueueWorkflow(input, dependencies) {
28197
28018
  }
28198
28019
  });
28199
28020
  }
28200
- //#endregion
28201
- //#region packages/core/src/analyze/workflows/diagnose-property.ts
28021
+ // packages/core/src/analyze/workflows/diagnose-property.ts
28202
28022
  async function diagnosePropertyWorkflow(input) {
28203
28023
  const narrative = await reportNarrative(input);
28204
28024
  const skippedCount = narrative.diagnosis.skippedSections?.length ?? 0;
@@ -28220,8 +28040,7 @@ async function diagnosePropertyWorkflow(input) {
28220
28040
  output: { narrative }
28221
28041
  });
28222
28042
  }
28223
- //#endregion
28224
- //#region packages/core/src/markdown.ts
28043
+ // packages/core/src/markdown.ts
28225
28044
  function tableCell(value) {
28226
28045
  return String(value ?? "-").replaceAll("\n", " ").replaceAll("|", "\\|");
28227
28046
  }
@@ -28248,8 +28067,7 @@ function renderPresentationTablesMarkdown(tables) {
28248
28067
  ];
28249
28068
  });
28250
28069
  }
28251
- //#endregion
28252
- //#region packages/core/src/analyze/workflows/presentation.ts
28070
+ // packages/core/src/analyze/workflows/presentation.ts
28253
28071
  function isRecord(value) {
28254
28072
  return typeof value === "object" && value !== null;
28255
28073
  }
@@ -28384,8 +28202,7 @@ function workflowPresentation(report, options = {}) {
28384
28202
  }] : []
28385
28203
  };
28386
28204
  }
28387
- //#endregion
28388
- //#region packages/core/src/analyze/workflows/markdown.ts
28205
+ // packages/core/src/analyze/workflows/markdown.ts
28389
28206
  function renderWorkflowMarkdown(report, options = {}) {
28390
28207
  const presentation = workflowPresentation(report, options);
28391
28208
  return [
@@ -28399,8 +28216,7 @@ function renderWorkflowMarkdown(report, options = {}) {
28399
28216
  ...renderPresentationTablesMarkdown(presentation.tables)
28400
28217
  ].join("\n").trim();
28401
28218
  }
28402
- //#endregion
28403
- //#region packages/core/src/analyze/workflows/priority-grouping.ts
28219
+ // packages/core/src/analyze/workflows/priority-grouping.ts
28404
28220
  const GROUPABLE_SOURCES = /* @__PURE__ */ new Set(["quick-win", "striking-distance"]);
28405
28221
  function queueTemplateFamilyId(template) {
28406
28222
  if (!template) return "no-template";
@@ -28496,8 +28312,7 @@ function groupBy(items, keyFor) {
28496
28312
  function groupPriorityQueue(items) {
28497
28313
  return groupBy(groupBy(items, targetGroupKey), groupKey).sort((a, b) => b.score - a.score);
28498
28314
  }
28499
- //#endregion
28500
- //#region packages/core/src/analyze/workflows/refresh-priorities.ts
28315
+ // packages/core/src/analyze/workflows/refresh-priorities.ts
28501
28316
  function templateCount(input) {
28502
28317
  const template = input.templates?.find((item) => item.id === input.id);
28503
28318
  return template?.urlCount ?? template?.count;
@@ -28704,8 +28519,7 @@ async function refreshPrioritiesWorkflow(input) {
28704
28519
  }
28705
28520
  });
28706
28521
  }
28707
- //#endregion
28708
- //#region packages/core/src/analyze/workflows/segments.ts
28522
+ // packages/core/src/analyze/workflows/segments.ts
28709
28523
  function compareText$3(left, right) {
28710
28524
  return left < right ? -1 : left > right ? 1 : 0;
28711
28525
  }
@@ -28733,8 +28547,7 @@ function splitSegments(report) {
28733
28547
  losers
28734
28548
  };
28735
28549
  }
28736
- //#endregion
28737
- //#region packages/core/src/analyze/workflows/update-postmortem.ts
28550
+ // packages/core/src/analyze/workflows/update-postmortem.ts
28738
28551
  function formatMagnitude(value) {
28739
28552
  return Math.abs(value).toLocaleString("en-GB");
28740
28553
  }
@@ -29006,8 +28819,7 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
29006
28819
  }
29007
28820
  });
29008
28821
  }
29009
- //#endregion
29010
- //#region packages/core/src/brand-detect.ts
28822
+ // packages/core/src/brand-detect.ts
29011
28823
  function defaultDateRange(days = 28) {
29012
28824
  return finalGscDateRange(days);
29013
28825
  }
@@ -29074,8 +28886,7 @@ async function detectBrandTerms(input) {
29074
28886
  candidates: ranked
29075
28887
  };
29076
28888
  }
29077
- //#endregion
29078
- //#region packages/core/src/clicky/api.ts
28889
+ // packages/core/src/clicky/api.ts
29079
28890
  const ENDPOINT$1 = "https://api.clicky.com/api/stats/4";
29080
28891
  const MAX_BYTES$3 = 5e6;
29081
28892
  var ClickyError = class extends Error {
@@ -29129,8 +28940,7 @@ async function clickyDomain(site) {
29129
28940
  return null;
29130
28941
  }
29131
28942
  }
29132
- //#endregion
29133
- //#region packages/core/src/analyze/ai-queries.ts
28943
+ // packages/core/src/analyze/ai-queries.ts
29134
28944
  const RULES = [
29135
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"],
29136
28946
  ["tracker", "\\. ?my location is |\\?\\. ?my( |$)|^evaluate the .+ on "],
@@ -29172,7 +28982,7 @@ function aiQueryKind(query) {
29172
28982
  return MATCHERS.find(([, pattern]) => pattern.test(text))?.[0] ?? null;
29173
28983
  }
29174
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(" "));
29175
- function words(query) {
28985
+ function words$1(query) {
29176
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));
29177
28987
  }
29178
28988
  const SAME_QUESTION = .6;
@@ -29215,7 +29025,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29215
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])));
29216
29026
  const usage = /* @__PURE__ */ new Map();
29217
29027
  const unique = new Set(rows.map((row) => String(row.keys[0] ?? "")));
29218
- 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);
29219
29029
  const weight = (word) => Math.log((unique.size + 1) / (usage.get(word) ?? 1));
29220
29030
  const groups = [];
29221
29031
  const byWord = /* @__PURE__ */ new Map();
@@ -29235,7 +29045,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29235
29045
  continue;
29236
29046
  }
29237
29047
  const text = query.trim().toLowerCase();
29238
- const own = words(query);
29048
+ const own = words$1(query);
29239
29049
  let group = byText.get(text);
29240
29050
  if (!group) {
29241
29051
  let checked = 0;
@@ -29350,8 +29160,7 @@ async function aiQueriesReport(input, searchAnalytics) {
29350
29160
  ]
29351
29161
  };
29352
29162
  }
29353
- //#endregion
29354
- //#region packages/core/src/commands/research-providers.ts
29163
+ // packages/core/src/commands/research-providers.ts
29355
29164
  function need(headers, provider) {
29356
29165
  const values = provider === "dataforseo" ? [headers.get("x-dataforseo-login"), headers.get("x-dataforseo-password")] : [headers.get("x-ahrefs-key")];
29357
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\`.`);
@@ -29943,8 +29752,7 @@ async function runAiAnswers(input, headers, deps, more) {
29943
29752
  }
29944
29753
  };
29945
29754
  }
29946
- //#endregion
29947
- //#region packages/core/src/commands/analytics.ts
29755
+ // packages/core/src/commands/analytics.ts
29948
29756
  async function analyticsSignIn(accounts, property, signIn) {
29949
29757
  const only = accounts.length === 1 ? accounts[0] : void 0;
29950
29758
  if (only) return only.google;
@@ -30006,8 +29814,7 @@ async function runAnalyticsRows(input, google, options) {
30006
29814
  }
30007
29815
  };
30008
29816
  }
30009
- //#endregion
30010
- //#region packages/core/src/ga4/matching.ts
29817
+ // packages/core/src/ga4/matching.ts
30011
29818
  function hostnameFromUrl(value) {
30012
29819
  try {
30013
29820
  const url = new URL(value);
@@ -30049,8 +29856,7 @@ function ga4MatchReason(match, site) {
30049
29856
  const location = match.match === "domain" ? "is inside" : "matches";
30050
29857
  return `Matched Google Analytics web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
30051
29858
  }
30052
- //#endregion
30053
- //#region packages/core/src/commands/analytics-site.ts
29859
+ // packages/core/src/commands/analytics-site.ts
30054
29860
  const STREAM_CHECKS = 30;
30055
29861
  const AT_ONCE = 5;
30056
29862
  const TIED = 3;
@@ -30356,10 +30162,9 @@ async function analyticsBeside(access, site, range, before, pages) {
30356
30162
  };
30357
30163
  }
30358
30164
  }
30359
- //#endregion
30360
- //#region packages/core/src/commands/audit.ts
30165
+ // packages/core/src/commands/audit.ts
30361
30166
  const AUDIT_SECTION = "weekly-audit";
30362
- 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), with what is new, back or resolved since the crawl before. Only for sites added to the weekly site audit beta.";
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.";
30363
30168
  const AUDIT_RULES = {
30364
30169
  link_to_error: "Internal links to a page that returns an error",
30365
30170
  link_to_redirect: "Internal links to a redirect",
@@ -30372,25 +30177,37 @@ const AUDIT_RULES = {
30372
30177
  canonical_elsewhere: "Pages whose canonical is another URL",
30373
30178
  sitemap_url_not_ok: "Sitemap URLs that do not return 200",
30374
30179
  sitemap_page_unlinked: "Sitemap pages no crawled page links to (heuristic: the crawl may not reach every page)",
30375
- thin_page: "Pages under 100 words (heuristic)"
30180
+ thin_page: "Pages under 100 words (heuristic)",
30181
+ js_changed_tags: "Pages whose raw HTML has another title, canonical or robots tag than the page after JavaScript runs"
30376
30182
  };
30377
- const PAGE_SIZE$1 = 50;
30378
- const SUMMARY_ROWS = 20;
30379
- const day$1 = (ms) => new Date(ms).toISOString().slice(0, 10);
30380
- 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) {
30381
30187
  return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
30382
30188
  }
30383
30189
  function evidence(finding, origin) {
30384
30190
  const details = JSON.parse(finding.details);
30385
30191
  const parts = [];
30386
30192
  if (typeof details.status === "number") parts.push(`${details.status}`);
30387
- if (typeof details.location === "string") parts.push(`to ${relative$2(details.location, origin)}`);
30388
- 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)}`);
30389
30195
  if (typeof details.words === "number") parts.push(`${details.words} words`);
30196
+ if (details.raw && typeof details.raw === "object") {
30197
+ const raw = details.raw;
30198
+ for (const key of [
30199
+ "title",
30200
+ "canonical",
30201
+ "robots"
30202
+ ]) if (key in raw) {
30203
+ const value = raw[key];
30204
+ parts.push(`raw ${key} ${typeof value === "string" ? key === "canonical" ? relative$3(value, origin) : value : "none"}`);
30205
+ }
30206
+ }
30390
30207
  const linking = typeof details.linking_pages === "number";
30391
30208
  const count = linking ? details.linking_pages : details.pages;
30392
30209
  if (typeof count === "number") {
30393
- 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);
30394
30211
  const noun = `${linking ? "linking page" : "page"}${count === 1 ? "" : "s"}`;
30395
30212
  parts.push(`${count} ${noun}${shown.length ? ` (${shown.join(", ")}${count > shown.length ? ", and more" : ""})` : ""}`);
30396
30213
  }
@@ -30400,13 +30217,13 @@ function state(finding, since) {
30400
30217
  if (finding.state === "resolved") return "resolved";
30401
30218
  if (finding.reopened_at && finding.reopened_at >= since) return "back";
30402
30219
  if (finding.first_seen_at >= since) return "new";
30403
- return `open since ${day$1(finding.first_seen_at)}`;
30220
+ return `open since ${day$2(finding.first_seen_at)}`;
30404
30221
  }
30405
30222
  function table(rows, origin, since) {
30406
30223
  return ["state | finding | subject | evidence", ...rows.map((finding) => [
30407
30224
  state(finding, since),
30408
30225
  finding.rule,
30409
- relative$2(finding.subject, origin),
30226
+ relative$3(finding.subject, origin),
30410
30227
  evidence(finding, origin)
30411
30228
  ].join(" | "))].join("\n");
30412
30229
  }
@@ -30417,6 +30234,14 @@ function ordered(findings, since) {
30417
30234
  };
30418
30235
  return [...findings].sort((a, b) => rank(a) - rank(b) || a.rule.localeCompare(b.rule) || (a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : 0));
30419
30236
  }
30237
+ function renderingText(rendering) {
30238
+ if (!rendering) return null;
30239
+ const pages = (n) => n === 1 ? "1 page" : `${n} pages`;
30240
+ if (rendering.mode === "on") return `JavaScript changed what some pages show, so the crawl rendered the parts of the site that need it (${pages(rendering.rendered)} rendered${rendering.failed ? `; ${pages(rendering.failed)} could not be rendered, so ${rendering.failed === 1 ? "it is" : "they are"} left out of the tag and text checks` : ""}).`;
30241
+ if (rendering.mode === "off") return "JavaScript did not change the pages checked, so the crawl read raw HTML.";
30242
+ if (rendering.mode === "unavailable") return "JavaScript rendering failed on the pages checked, so the crawl read raw HTML. If the site builds its pages with JavaScript, titles, canonicals and links may be missing here.";
30243
+ return rendering.failed && !rendering.rendered ? "JavaScript rendering failed, so the crawl read raw HTML." : null;
30244
+ }
30420
30245
  function runAuditReport(input, snapshot, call) {
30421
30246
  const site = input.site;
30422
30247
  const detail = input.detail ?? "summary";
@@ -30463,9 +30288,10 @@ function runAuditReport(input, snapshot, call) {
30463
30288
  const pages = summary.pages;
30464
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`;
30465
30290
  const head = [
30466
- `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.` : ""}`,
30467
- 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.`,
30468
- summary.findings.resolution === "crawled-pages-only" ? "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,
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.`,
30293
+ renderingText(summary.rendering),
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,
30469
30295
  summary.findings.capped_rules.length ? `Capped at 1,000 findings each, so these counts are lower bounds: ${summary.findings.capped_rules.join(", ")}.` : null,
30470
30296
  running
30471
30297
  ].filter(Boolean).join("\n");
@@ -30475,8 +30301,8 @@ function runAuditReport(input, snapshot, call) {
30475
30301
  "change | url | date | reason now | top page",
30476
30302
  ...index.rows.map((row) => [
30477
30303
  row.change === "dropped" ? "dropped out" : "back in",
30478
- relative$2(row.url, origin),
30479
- day$1(row.at),
30304
+ relative$3(row.url, origin),
30305
+ day$2(row.at),
30480
30306
  row.coverage ?? "",
30481
30307
  row.top ? "yes" : "no"
30482
30308
  ].join(" | ")),
@@ -30484,7 +30310,7 @@ function runAuditReport(input, snapshot, call) {
30484
30310
  ].filter(Boolean).join("\n") : null;
30485
30311
  if (detail === "rows") {
30486
30312
  const offset = input.offset ?? 0;
30487
- const slice = findings.slice(offset, offset + PAGE_SIZE$1);
30313
+ const slice = findings.slice(offset, offset + PAGE_SIZE$2);
30488
30314
  const end = offset + slice.length;
30489
30315
  return {
30490
30316
  text: [
@@ -30518,7 +30344,7 @@ function runAuditReport(input, snapshot, call) {
30518
30344
  head,
30519
30345
  indexText,
30520
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.",
30521
- 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,
30522
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,
30523
30349
  `More: ${call("report", {
30524
30350
  site,
@@ -30532,7 +30358,7 @@ function runAuditReport(input, snapshot, call) {
30532
30358
  site,
30533
30359
  finished_at: snapshot.finished_at,
30534
30360
  summary,
30535
- fresh: fresh.slice(0, SUMMARY_ROWS)
30361
+ fresh: fresh.slice(0, SUMMARY_ROWS$1)
30536
30362
  }
30537
30363
  };
30538
30364
  }
@@ -30540,12 +30366,12 @@ function auditPreview(snapshot) {
30540
30366
  if (!snapshot?.summary || snapshot.finished_at === null) return snapshot ? "The first weekly crawl has not finished." : null;
30541
30367
  const { findings } = snapshot.summary;
30542
30368
  const index = snapshot.index_changes;
30543
- 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.`;
30544
30370
  }
30545
30371
  const WEEK_MS = 6048e5;
30546
- 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);
30547
30373
  const count$2 = (value) => value.toLocaleString("en-US");
30548
- function relative$1(value, origin) {
30374
+ function relative$2(value, origin) {
30549
30375
  if (!value) return "";
30550
30376
  return origin && value.startsWith(origin) ? value.slice(origin.length) || "/" : value;
30551
30377
  }
@@ -30559,15 +30385,15 @@ function rowsTable(rows, checks) {
30559
30385
  const since = checks.as_of - WEEK_MS;
30560
30386
  const changed = (row) => row.changed_at !== null && row.changed_at > since;
30561
30387
  return ["url | index change | reason now | before | changed | checked | clicks (28 days before) | in_sitemap | google_canonical | error", ...rows.map((row) => [
30562
- relative$1(row.url, checks.origin),
30563
- 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)}` : "",
30564
30390
  row.coverage ?? (row.checked_at === null ? "not checked yet" : ""),
30565
30391
  changed(row) ? row.prev_coverage ?? "" : "",
30566
- changed(row) ? day(row.changed_at) : "",
30567
- day(row.checked_at),
30392
+ changed(row) ? day$1(row.changed_at) : "",
30393
+ day$1(row.checked_at),
30568
30394
  `${count$2(row.clicks)} (${count$2(row.previous_clicks)})${row.lost ? " lost" : ""}`,
30569
30395
  row.in_sitemap ? "yes" : "no",
30570
- 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) : "",
30571
30397
  row.error ?? ""
30572
30398
  ].join(" | "))].join("\n");
30573
30399
  }
@@ -30649,8 +30475,7 @@ function runAuditChecks(input, checks, call) {
30649
30475
  }
30650
30476
  };
30651
30477
  }
30652
- //#endregion
30653
- //#region packages/core/src/commands/bing.ts
30478
+ // packages/core/src/commands/bing.ts
30654
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."];
30655
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.";
30656
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\`.`;
@@ -30966,8 +30791,7 @@ function bingOverviewText(report, heading, origin, rowsPerList) {
30966
30791
  `Limits:\n${report.caveats.map((line) => `- ${line}`).join("\n")}`
30967
30792
  ].filter(Boolean).join("\n\n");
30968
30793
  }
30969
- //#endregion
30970
- //#region packages/core/src/commands/clicky.ts
30794
+ // packages/core/src/commands/clicky.ts
30971
30795
  const CLICKY_KEY_PREFIX = "x-clicky-";
30972
30796
  function clickySites(keys) {
30973
30797
  const sites = [];
@@ -31121,8 +30945,7 @@ async function runClickyRows(input, keys, options) {
31121
30945
  }
31122
30946
  };
31123
30947
  }
31124
- //#endregion
31125
- //#region packages/core/src/commands/domain-rating.ts
30948
+ // packages/core/src/commands/domain-rating.ts
31126
30949
  const DR_ATTRIBUTION = "domain_rating: Domain Rating by Ahrefs (https://ahrefs.com/).";
31127
30950
  const ENDPOINT = "https://api.ahrefs.com/v3/public/domain-rating-free";
31128
30951
  const MAX_DOMAINS = 25;
@@ -31383,9 +31206,9 @@ function indexingRow(e, sources, inspectedAt) {
31383
31206
  ...live.first.error ? { live_error: live.first.error } : {}
31384
31207
  };
31385
31208
  }
31386
- const byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31209
+ const byText$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31387
31210
  function sortRows(rows) {
31388
- 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));
31389
31212
  }
31390
31213
  function byReason(rows) {
31391
31214
  const groups = /* @__PURE__ */ new Map();
@@ -31399,7 +31222,7 @@ function byReason(rows) {
31399
31222
  counts[row.label] += 1;
31400
31223
  groups.set(row.reason, counts);
31401
31224
  }
31402
- 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]) => ({
31403
31226
  reason,
31404
31227
  urls: counts.fix + counts.check + counts.expected + counts.indexed,
31405
31228
  ...counts
@@ -31413,7 +31236,7 @@ function knownUrls(input) {
31413
31236
  if (!sources.includes(source)) sources.push(source);
31414
31237
  found.set(url, sources);
31415
31238
  };
31416
- 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));
31417
31240
  for (const url of input.lost) add(url, "lost_clicks");
31418
31241
  for (const page of pages) add(page.url, "search_console");
31419
31242
  for (const url of input.sitemap.urls) add(url, "sitemap");
@@ -31504,7 +31327,7 @@ async function inspectKnown(site, known, deps, asked = 100) {
31504
31327
  inspected: rows.length,
31505
31328
  fromSaved: reused.length,
31506
31329
  oldestSaved: oldest,
31507
- failed: failed.sort((a, b) => byText(a.url, b.url)),
31330
+ failed: failed.sort((a, b) => byText$1(a.url, b.url)),
31508
31331
  stoppedBy: quotaStop || quotaShort ? "quota" : known.urls.length > wanted.length ? "limit" : null,
31509
31332
  quota
31510
31333
  };
@@ -31571,8 +31394,441 @@ function indexingText(site, run, page, origin, askMore, now) {
31571
31394
  if (!page.summary && more) lines.push(`More rows: ${page.more(end)}.`);
31572
31395
  return lines;
31573
31396
  }
31574
- //#endregion
31575
- //#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
31576
31832
  const MAX_PAGES = 20;
31577
31833
  const TIMEOUT_MS$2 = 6e3;
31578
31834
  const MAX_BYTES$2 = 1e6;
@@ -31723,8 +31979,7 @@ function temporaryRedirectNote(pages) {
31723
31979
  if (!temporary.length) return null;
31724
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("; ")}.`;
31725
31981
  }
31726
- //#endregion
31727
- //#region packages/core/src/commands/report.ts
31982
+ // packages/core/src/commands/report.ts
31728
31983
  const SECTIONS = {
31729
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.",
31730
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.",
@@ -32093,7 +32348,17 @@ async function allSections(input, deps) {
32093
32348
  parts.push(`## ${AUDIT_SECTION}\n${audit ?? "This site is not in the weekly site audit."}`);
32094
32349
  sections[AUDIT_SECTION] = { line: audit };
32095
32350
  }
32096
- 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
+ ];
32097
32362
  parts.push(`More: ${deps.call("report", {
32098
32363
  site: input.site,
32099
32364
  section: "<section>"
@@ -32187,6 +32452,27 @@ async function oneSection(input, deps) {
32187
32452
  };
32188
32453
  }
32189
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
+ }
32190
32476
  if (input.section === "weekly-audit") {
32191
32477
  const audit = deps.audit;
32192
32478
  if (!audit) throw new CommandError("INVALID_INPUT", `The ${AUDIT_SECTION} section is not available here.`);
@@ -32218,8 +32504,7 @@ async function runBingReport(input, deps) {
32218
32504
  data: report
32219
32505
  };
32220
32506
  }
32221
- //#endregion
32222
- //#region packages/core/src/commands/research.ts
32507
+ // packages/core/src/commands/research.ts
32223
32508
  const RESEARCH_ACTIONS = [
32224
32509
  "keyword_metrics",
32225
32510
  "ranked_keywords",
@@ -32324,8 +32609,7 @@ async function runResearch(input, keys, options) {
32324
32609
  }
32325
32610
  };
32326
32611
  }
32327
- //#endregion
32328
- //#region packages/core/src/commands/sitemap-urls.ts
32612
+ // packages/core/src/commands/sitemap-urls.ts
32329
32613
  const MAX_FILES = 20;
32330
32614
  const MAX_URLS = 5e4;
32331
32615
  const MAX_BYTES$1 = 2e7;
@@ -32438,8 +32722,7 @@ async function sitemapUrls(site) {
32438
32722
  notes: [...new Set(notes)]
32439
32723
  };
32440
32724
  }
32441
- //#endregion
32442
- //#region packages/core/src/commands/site.ts
32725
+ // packages/core/src/commands/site.ts
32443
32726
  function indexStatusFrom(result) {
32444
32727
  const status = result?.inspectionResult?.indexStatusResult ?? result?.indexStatusResult ?? {};
32445
32728
  const text = (value) => typeof value === "string" ? value : null;
@@ -32772,8 +33055,7 @@ async function runLostPages(input, deps) {
32772
33055
  }
32773
33056
  };
32774
33057
  }
32775
- //#endregion
32776
- //#region packages/core/src/commands/week.ts
33058
+ // packages/core/src/commands/week.ts
32777
33059
  const DAY_MS = 864e5;
32778
33060
  function weekLabel(start, end) {
32779
33061
  const parts = (date) => {
@@ -32795,6 +33077,7 @@ const siteName = (property) => property.replace(/^sc-domain:|^https?:\/\//, "").
32795
33077
  const count = (n) => n.toLocaleString("en-US");
32796
33078
  const plural$1 = (n, one, many = `${one}s`) => `${count(n)} ${n === 1 ? one : many}`;
32797
33079
  const list = (items) => items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
33080
+ const sentence = (text) => /[.!?]$/.test(text) ? text : `${text}.`;
32798
33081
  function path(url) {
32799
33082
  try {
32800
33083
  const parsed = new URL(url);
@@ -32817,60 +33100,58 @@ const percent = (site) => {
32817
33100
  return Math.round(Math.abs(clicks.clicks - clicks.previous) / clicks.previous * 100);
32818
33101
  };
32819
33102
  function issues(site, startMs, endMs) {
32820
- const name = siteName(site.property);
32821
33103
  const found = [];
33104
+ if (site.index?.dropped) found.push({
33105
+ rank: 0,
33106
+ heading: `${plural$1(site.index.dropped, "page")} dropped`
33107
+ });
33108
+ const crawl = site.crawl;
33109
+ const crawled = crawl !== null && crawl.finished_at >= startMs && crawl.finished_at < endMs;
33110
+ if (crawled && crawl.status === "failed") found.push({
33111
+ rank: 1,
33112
+ heading: "crawl stopped early"
33113
+ });
33114
+ else if (!crawled && site.added_at < startMs) found.push({
33115
+ rank: 1,
33116
+ heading: "no crawl this week"
33117
+ });
33118
+ if (clicksChange(site) === "fell") found.push({
33119
+ rank: 2,
33120
+ heading: `clicks down ${percent(site)}%`
33121
+ });
33122
+ if (crawled && crawl.new_issues > 0) found.push({
33123
+ rank: 3,
33124
+ heading: plural$1(crawl.new_issues, "new issue")
33125
+ });
33126
+ return found;
33127
+ }
33128
+ function siteLines(site, startMs, endMs) {
33129
+ const lines = [];
32822
33130
  const index = site.index;
32823
- if (index?.dropped) {
33131
+ if (!index) lines.push("Google index: no checks ran this week yet.");
33132
+ else if (index.dropped) {
32824
33133
  const paths = index.dropped_pages.map((page) => path(page.url));
32825
33134
  const more = index.dropped - paths.length;
32826
33135
  const shown = more > 0 ? `${paths.join(", ")} and ${plural$1(more, "more")}` : list(paths);
32827
33136
  const reasons = new Set(index.dropped_pages.map((page) => page.coverage));
32828
33137
  const reason = reasons.size === 1 ? [...reasons][0] : null;
32829
- found.push({
32830
- rank: 0,
32831
- heading: `${plural$1(index.dropped, "page")} dropped`,
32832
- line: `${shown} dropped out of Google’s index.${reason ? ` Google says: ${reason}.` : ""}`,
32833
- next: `check ${index.dropped === 1 ? "it" : "they"} still return${index.dropped === 1 ? "s" : ""} 200 and ${index.dropped === 1 ? "is" : "are"} not noindexed.`,
32834
- prompt: `Why did ${paths[0] ?? "a page"} on ${name} drop out of Google’s index this week, and how do I fix it?`
32835
- });
32836
- }
33138
+ lines.push(`Google index: ${plural$1(index.dropped, "page")} dropped out (${shown}).${reason ? ` Google says: ${reason}.` : ""}${index.returned ? ` ${plural$1(index.returned, "page")} came back.` : ""} ${plural$1(index.checked, "page")} checked.`);
33139
+ } else lines.push(`Google index: ${plural$1(index.checked, "page")} checked, none dropped${index.returned ? `, ${plural$1(index.returned, "page")} came back` : ""}.`);
32837
33140
  const crawl = site.crawl;
32838
33141
  const crawled = crawl !== null && crawl.finished_at >= startMs && crawl.finished_at < endMs;
32839
- if (crawled && crawl.status === "failed") found.push({
32840
- rank: 1,
32841
- heading: "crawl stopped early",
32842
- line: `The weekly crawl stopped after ${plural$1(crawl.pages, "page")}${crawl.error ? `: ${crawl.error}` : "."}`,
32843
- next: "check the site is up. The next weekly crawl tries again.",
32844
- prompt: `Why did the weekly crawl of ${name} stop early, and what should I check?`
32845
- });
32846
- else if (!crawled && site.added_at < startMs) found.push({
32847
- rank: 1,
32848
- heading: "no crawl this week",
32849
- line: `No weekly crawl finished this week${site.start_error ? `: ${site.start_error}` : "."}`,
32850
- next: "check the site is up. The crawl tries again each hour.",
32851
- prompt: `Why has the weekly crawl of ${name} not finished, and what should I check?`
32852
- });
32853
- if (clicksChange(site) === "fell") {
32854
- const clicks = site.clicks;
32855
- found.push({
32856
- rank: 2,
32857
- heading: `clicks down ${percent(site)}%`,
32858
- line: `Search clicks fell ${percent(site)}%, from ${count(clicks.previous)} to ${count(clicks.clicks)} (${weekLabel(clicks.start, clicks.end)}, against the 7 days before).`,
32859
- next: "ask your agent which pages and queries lost them.",
32860
- prompt: `Which pages and queries lost search clicks on ${name} in the last week, and why?`
32861
- });
32862
- }
32863
- if (crawled && crawl.new_issues > 0) {
33142
+ if (crawled && crawl.status === "failed") lines.push(`Weekly crawl: stopped after ${plural$1(crawl.pages, "page")}${crawl.error ? `: ${sentence(crawl.error)}` : "."} It tries again next week.`);
33143
+ else if (crawled) {
32864
33144
  const rule = crawl.top_rule ? AUDIT_RULES[crawl.top_rule] : void 0;
32865
- found.push({
32866
- rank: 3,
32867
- heading: plural$1(crawl.new_issues, "new issue"),
32868
- line: `The weekly crawl found ${plural$1(crawl.new_issues, "new issue")}${rule ? `, most of them ${rule.charAt(0).toLowerCase()}${rule.slice(1)}` : ""}.`,
32869
- next: "ask your agent for the weekly audit of this site.",
32870
- prompt: `What did the weekly crawl of ${name} find this week, and what should I fix first?`
32871
- });
32872
- }
32873
- return found;
33145
+ lines.push(crawl.new_issues ? `Weekly crawl: ${plural$1(crawl.pages, "page")}, ${plural$1(crawl.new_issues, "new issue")}${rule ? `, most of them ${rule.charAt(0).toLowerCase()}${rule.slice(1)}` : ""}.` : `Weekly crawl: ${plural$1(crawl.pages, "page")}, no new issues.`);
33146
+ } else if (site.added_at < startMs) lines.push(`Weekly crawl: none finished this week${site.start_error ? `: ${sentence(site.start_error)}` : "."} It tries again each hour.`);
33147
+ else lines.push("Weekly crawl: the first crawl is still running.");
33148
+ const clicks = site.clicks;
33149
+ if (clicks) {
33150
+ const change = clicks.previous > 0 ? Math.round((clicks.clicks - clicks.previous) / clicks.previous * 100) : null;
33151
+ const trend = change === null ? `from ${count(clicks.previous)}` : change === 0 ? "level with" : `${change > 0 ? "up" : "down"} ${Math.abs(change)}% on`;
33152
+ lines.push(`Search clicks: ${count(clicks.clicks)} (${weekLabel(clicks.start, clicks.end)}), ${trend}${change === null ? "" : ` ${count(clicks.previous)}`} the 7 days before.`);
33153
+ } else lines.push("Search clicks: not available this week.");
33154
+ return lines;
32874
33155
  }
32875
33156
  function weekMarkdown(data) {
32876
33157
  const startMs = Date.parse(`${data.start}T00:00:00Z`);
@@ -32878,43 +33159,21 @@ function weekMarkdown(data) {
32878
33159
  const title = `# ${weekTitle(data)}`;
32879
33160
  if (!data.sites.length) return `${title}\n\nNo site is in your weekly audit yet. Add one on the Sites tab: ${data.account}#sites`;
32880
33161
  const byName = (a, b) => siteName(a.property).localeCompare(siteName(b.property)) || a.property.localeCompare(b.property);
32881
- const sites = [...data.sites].sort(byName);
32882
- const attention = [];
32883
- const quiet = [];
32884
- const waiting = [];
32885
- for (const site of sites) {
32886
- const found = issues(site, startMs, endMs);
32887
- if (found.length) attention.push({
32888
- site,
32889
- found
32890
- });
32891
- else if (!site.crawl || !site.index) waiting.push(site);
32892
- else quiet.push(site);
32893
- }
32894
- attention.sort((a, b) => a.found[0].rank - b.found[0].rank || byName(a.site, b.site));
32895
- const total = sites.length;
33162
+ const rows = data.sites.map((site) => ({
33163
+ site,
33164
+ found: issues(site, startMs, endMs),
33165
+ waiting: !site.crawl || !site.index
33166
+ }));
33167
+ const attention = rows.filter((row) => row.found.length);
33168
+ const quiet = rows.filter((row) => !row.found.length && !row.waiting);
33169
+ rows.sort((a, b) => (a.found[0]?.rank ?? Number.POSITIVE_INFINITY) - (b.found[0]?.rank ?? Number.POSITIVE_INFINITY) || byName(a.site, b.site));
33170
+ const total = rows.length;
32896
33171
  const out = [
32897
33172
  title,
32898
33173
  "",
32899
- attention.length ? total === 1 ? "Your site needs a look this week." : `${attention.length} of your ${total} sites ${attention.length === 1 ? "needs" : "need"} a look this week.` : quiet.length ? total === 1 ? "Your site had a quiet week." : quiet.length === total ? `All ${total} of your sites had a quiet week.` : `${quiet.length} of your ${total} sites had a quiet week.` : "Your sites are still on their first checks."
33174
+ attention.length ? total === 1 ? "Your site had changes worth a look this week." : `${attention.length} of your ${total} sites had changes worth a look this week.` : quiet.length ? total === 1 ? "Your site had a quiet week." : quiet.length === total ? `All ${total} of your sites had a quiet week.` : `${quiet.length} of your ${total} sites had a quiet week.` : "Your sites are still on their first checks."
32900
33175
  ];
32901
- if (attention.length) {
32902
- out.push("", "## Needs attention");
32903
- for (const { site, found } of attention) {
32904
- const [first, ...rest] = found;
32905
- const also = rest.length ? ` Also: ${list(rest.map((item) => item.heading))}.` : "";
32906
- const rose = clicksChange(site) === "rose" ? ` Search clicks rose ${percent(site)}%.` : "";
32907
- out.push("", `### ${siteName(site.property)}: ${first.heading}`, "", `- ${first.line}${also}${rose}`, `- Next step: ${first.next}`, ...site.link ? [`- [View site](${site.link})`] : []);
32908
- }
32909
- }
32910
- if (quiet.length) {
32911
- const risen = quiet.filter((site) => clicksChange(site) === "rose");
32912
- out.push("", "## All quiet", "", `${list(quiet.map((site) => siteName(site.property)))}: no drops, no new issues.${risen.map((site) => ` Search clicks rose ${percent(site)}% on ${siteName(site.property)}.`).join("")}`);
32913
- }
32914
- if (waiting.length) out.push("", "## Not checked yet", "", `${list(waiting.map((site) => siteName(site.property)))}: the first crawl or Google checks are still running.`);
32915
- const first = attention[0];
32916
- const prompt = first ? first.found[0].prompt : `What are the best quick wins on ${siteName((quiet[0] ?? sites[0]).property)} this month?`;
32917
- out.push("", "## Ask your agent", "", "Paste this into your agent:", "", "```", prompt, "```");
33176
+ for (const { site, found } of rows) out.push("", `## ${siteName(site.property)}${found.length ? `: ${list(found.map((item) => item.heading))}` : ""}`, "", ...siteLines(site, startMs, endMs).map((line) => `- ${line}`), ...site.link ? [`- [View site](${site.link})`] : []);
32918
33177
  return out.join("\n");
32919
33178
  }
32920
33179
  async function runAllSites(deps, sites) {
@@ -32937,8 +33196,7 @@ async function runAllSites(deps, sites) {
32937
33196
  data
32938
33197
  };
32939
33198
  }
32940
- //#endregion
32941
- //#region packages/core/src/commands/tools.ts
33199
+ // packages/core/src/commands/tools.ts
32942
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.");
32943
33201
  const dates = {
32944
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."),
@@ -32952,6 +33210,12 @@ const detail = {
32952
33210
  ]).default("summary").describe("summary first. rows: the next page of rows. full: every field as JSON."),
32953
33211
  offset: z.number().int().min(0).default(0).describe("rows only: the first row to return.")
32954
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
+ };
32955
33219
  const DATA_SOURCES = [
32956
33220
  "search_console",
32957
33221
  "google_analytics",
@@ -32974,20 +33238,44 @@ const SHARED_TOOLS = [
32974
33238
  name: "seo_sites",
32975
33239
  command: "data",
32976
33240
  title: "List sites",
32977
- description: "Lists the Search Console and Google Analytics properties the connected Google accounts can read, the Bing Webmaster site for each, and which research keys are set. Call it first.",
32978
- input: z.object({}),
32979
- run: listSites$1
33241
+ description: "Lists the Search Console and Google Analytics properties the connected Google accounts can read, the Bing Webmaster site for each, and which research keys are set. Call it first. With add: starts the weekly site audit of that site on the hosted server (a weekly crawl, daily Google index checks, a Monday email and webhook with what changed, and alerts when a page drops out of Google's index). A site that needs paying for returns a Stripe Checkout link: give it to the person to open, and the audit starts once they pay.",
33242
+ input: z.object({ add: site.optional().describe("Optional. A site to add to the weekly site audit. Only when the person asks for it: a site can cost money.") }),
33243
+ run: async (surface, input) => {
33244
+ if (!input.add) return listSites$1(surface);
33245
+ const start = surface.deps.startAudit;
33246
+ if (!start) throw new CommandError("INVALID_INPUT", "The weekly site audit runs on the hosted server only. Connect https://seoskill.dev/mcp and call seo_sites with add there.");
33247
+ const property = await surface.resolveSite(input.add);
33248
+ const result = await start(property);
33249
+ return {
33250
+ text: {
33251
+ added: `Added ${property} to the weekly site audit. Its first crawl and index checks have started; ask ${surface.deps.call("report", {
33252
+ site: property,
33253
+ section: AUDIT_SECTION
33254
+ })} for the findings once the crawl is done.`,
33255
+ exists: `${property} is already in the weekly site audit.`,
33256
+ pending: "The payment went through and the plan is starting. Call seo_sites with add again in a minute.",
33257
+ checkout: result.status === "checkout" ? `${property} needs a plan: $${result.price} a month per site, cancel at any time. Give the person this Stripe Checkout link to open and pay: ${result.url}\nThe audit of ${property} starts once they pay. Call seo_sites with add again afterwards to confirm.` : "",
33258
+ full: result.status === "full" ? `This account already audits ${result.limit} sites, the most it can. Remove one on the account page first.` : "",
33259
+ payment_failed: `Stripe could not take the payment for another site, so ${property} was not added. The person can check their card under Manage billing on the account page.`
33260
+ }[result.status],
33261
+ data: {
33262
+ property,
33263
+ ...result
33264
+ }
33265
+ };
33266
+ }
32980
33267
  }),
32981
33268
  tool({
32982
33269
  name: "seo_report",
32983
33270
  command: "report",
32984
33271
  title: "Search Console report",
32985
- description: `Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with what needs a look and the next step for each, 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.`,
32986
33273
  input: z.object({
32987
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."),
32988
- 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."),
32989
33276
  ...dates,
32990
- ...detail
33277
+ ...detail,
33278
+ ...marks
32991
33279
  }),
32992
33280
  run: async (surface, input) => {
32993
33281
  if (!input.site) {
@@ -32995,9 +33283,22 @@ const SHARED_TOOLS = [
32995
33283
  site: "<site>",
32996
33284
  section: input.section
32997
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
+ })}.`);
32998
33291
  return runAllSites(surface.deps, () => listSites$1(surface));
32999
33292
  }
33000
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);
33001
33302
  const resolved = await surface.resolveSite(target).catch((error) => error instanceof Error ? error : new Error(String(error)));
33002
33303
  if (resolved instanceof Error && !input.section && surface.deps.bing) return runBingReport({
33003
33304
  site: target,
@@ -33013,7 +33314,8 @@ const SHARED_TOOLS = [
33013
33314
  startDate: input.start_date,
33014
33315
  endDate: input.end_date,
33015
33316
  detail: input.detail,
33016
- offset: input.offset
33317
+ offset: input.offset,
33318
+ saved: input.saved
33017
33319
  }, surface.deps);
33018
33320
  }
33019
33321
  }),
@@ -33208,10 +33510,13 @@ function indexFromStore(shared) {
33208
33510
  SHARED_TOOLS.map((shared) => shared.name === "seo_index" ? indexFromStore(shared) : shared.name === "seo_report" ? {
33209
33511
  ...shared,
33210
33512
  description: `${shared.description}\n- ${AUDIT_SECTION}: ${AUDIT_SECTION_TEXT}`,
33211
- 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.") })
33212
33518
  } : shared);
33213
- //#endregion
33214
- //#region packages/core/src/gsc/client/inspection-cache.ts
33519
+ // packages/core/src/gsc/client/inspection-cache.ts
33215
33520
  const INSPECTION_FRESH_MS = 6048e5;
33216
33521
  function readSavedInspections(site, urls, now = Date.now()) {
33217
33522
  const found = /* @__PURE__ */ new Map();
@@ -33240,8 +33545,7 @@ function saveInspection(site, url, status, now = Date.now()) {
33240
33545
  noteCacheWrite(Buffer.byteLength(json) + url.length, database);
33241
33546
  });
33242
33547
  }
33243
- //#endregion
33244
- //#region packages/core/src/providers/api-key-credentials.ts
33548
+ // packages/core/src/providers/api-key-credentials.ts
33245
33549
  function createApiKeyCredentialStore(input) {
33246
33550
  const normalize = (value) => {
33247
33551
  const apiKey = value.trim();
@@ -33273,8 +33577,7 @@ function createApiKeyCredentialStore(input) {
33273
33577
  }
33274
33578
  };
33275
33579
  }
33276
- //#endregion
33277
- //#region packages/core/src/providers/ahrefs/credentials.ts
33580
+ // packages/core/src/providers/ahrefs/credentials.ts
33278
33581
  const AHREFS_API_KEY_ENV = "SEO_AHREFS_API_KEY";
33279
33582
  const AHREFS_API_KEY_SECRET = PROVIDER_SECRET_NAMES.ahrefsApiKey;
33280
33583
  const store$1 = createApiKeyCredentialStore({
@@ -33292,8 +33595,7 @@ async function readAhrefsApiKey(input = {}) {
33292
33595
  async function deleteAhrefsApiKey() {
33293
33596
  await store$1.remove();
33294
33597
  }
33295
- //#endregion
33296
- //#region packages/core/src/providers/bing/credentials.ts
33598
+ // packages/core/src/providers/bing/credentials.ts
33297
33599
  const BING_API_KEY_ENV = "SEO_BING_API_KEY";
33298
33600
  const BING_API_KEY_SECRET = PROVIDER_SECRET_NAMES.bingApiKey;
33299
33601
  const store = createApiKeyCredentialStore({
@@ -33311,8 +33613,7 @@ async function readBingApiKey(input = {}) {
33311
33613
  async function deleteBingApiKey() {
33312
33614
  await store.remove();
33313
33615
  }
33314
- //#endregion
33315
- //#region packages/core/src/providers/dataforseo/credentials.ts
33616
+ // packages/core/src/providers/dataforseo/credentials.ts
33316
33617
  const DATAFORSEO_LOGIN_ENV = "SEO_DATAFORSEO_LOGIN";
33317
33618
  const DATAFORSEO_PASSWORD_ENV = "SEO_DATAFORSEO_PASSWORD";
33318
33619
  const DATAFORSEO_CREDENTIAL_SECRET = PROVIDER_SECRET_NAMES.dataForSeoCredentials;
@@ -33427,8 +33728,24 @@ async function deleteDataForSeoCredentials() {
33427
33728
  await deleteProviderSecret(DATAFORSEO_CREDENTIAL_SECRET);
33428
33729
  clearLegacyConfigCredentials();
33429
33730
  }
33430
- //#endregion
33431
- //#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
33432
33749
  function nodeCommandDeps(options) {
33433
33750
  return {
33434
33751
  searchAnalytics: (site, body) => querySearchAnalytics(site, body, {
@@ -33458,6 +33775,7 @@ function nodeCommandDeps(options) {
33458
33775
  accounts: localAccounts,
33459
33776
  signIn: LOCAL_SIGN_IN.signIn
33460
33777
  },
33778
+ issues: localIssues,
33461
33779
  ...cloudLinked() ? { week: cloudWeek } : {}
33462
33780
  };
33463
33781
  }
@@ -33543,8 +33861,7 @@ async function localGoogleFetch(account) {
33543
33861
  const { client } = await getAuthorized(account);
33544
33862
  return googleFetchFor(client);
33545
33863
  }
33546
- //#endregion
33547
- //#region packages/core/src/doctor.ts
33864
+ // packages/core/src/doctor.ts
33548
33865
  const REQUIRED_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
33549
33866
  async function runDoctor(dependencies = {}) {
33550
33867
  const paths = getSeoCliPaths();
@@ -33612,8 +33929,7 @@ async function runDoctor(dependencies = {}) {
33612
33929
  checks
33613
33930
  };
33614
33931
  }
33615
- //#endregion
33616
- //#region packages/core/src/export/update-postmortem.ts
33932
+ // packages/core/src/export/update-postmortem.ts
33617
33933
  const HEADERS = {
33618
33934
  summary: [
33619
33935
  "workflow",
@@ -33935,8 +34251,7 @@ function updatePostmortemCsvFiles(report) {
33935
34251
  }))
33936
34252
  ];
33937
34253
  }
33938
- //#endregion
33939
- //#region packages/core/src/gsc/local-export.ts
34254
+ // packages/core/src/gsc/local-export.ts
33940
34255
  const DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT = 5e3;
33941
34256
  const MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT = 1e5;
33942
34257
  const MAX_SEARCH_CONSOLE_EXPORT_FILES = 20;
@@ -34214,8 +34529,7 @@ async function loadSearchConsoleExport(input) {
34214
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."]
34215
34530
  };
34216
34531
  }
34217
- //#endregion
34218
- //#region packages/core/src/gsc/local-export-reconcile.ts
34532
+ // packages/core/src/gsc/local-export-reconcile.ts
34219
34533
  const DEFAULT_UNREACHED_PAGE_LIMIT = 100;
34220
34534
  function unreachedLimit(value) {
34221
34535
  const result = value ?? 100;
@@ -34272,8 +34586,7 @@ function reconcileExportPagesWithCrawl(input) {
34272
34586
  capped: unreached.length > limit
34273
34587
  };
34274
34588
  }
34275
- //#endregion
34276
- //#region packages/core/src/storage/log-retention.ts
34589
+ // packages/core/src/storage/log-retention.ts
34277
34590
  const MEBIBYTE = 1048576;
34278
34591
  const LOG_MAX_FILE_BYTES = 8 * MEBIBYTE;
34279
34592
  const LOG_MAX_TOTAL_BYTES = 64 * MEBIBYTE;
@@ -34336,8 +34649,7 @@ function pruneLogs(options = {}) {
34336
34649
  sizeBytes
34337
34650
  };
34338
34651
  }
34339
- //#endregion
34340
- //#region packages/core/src/logger.ts
34652
+ // packages/core/src/logger.ts
34341
34653
  let loggerInstance;
34342
34654
  function createLogger(runId) {
34343
34655
  if (loggerInstance) return loggerInstance.child({ runId });
@@ -34355,8 +34667,7 @@ function createLogger(runId) {
34355
34667
  function makeRunId() {
34356
34668
  return Math.random().toString(36).slice(2, 10);
34357
34669
  }
34358
- //#endregion
34359
- //#region packages/core/src/provider-extensions/actions.ts
34670
+ // packages/core/src/provider-extensions/actions.ts
34360
34671
  const MAX_ACTION_PARAM_BYTES = 65536;
34361
34672
  const MAX_ACTION_PARAM_DEPTH = 16;
34362
34673
  const MAX_ACTION_PARAM_VALUES = 5e3;
@@ -34489,8 +34800,7 @@ async function runProviderExtensionAction(input) {
34489
34800
  data
34490
34801
  };
34491
34802
  }
34492
- //#endregion
34493
- //#region packages/core/src/provider-extensions/connections.ts
34803
+ // packages/core/src/provider-extensions/connections.ts
34494
34804
  function readProviderExtensionAccount(providerIdValue) {
34495
34805
  const providerId = providerExtensionIdSchema.parse(providerIdValue);
34496
34806
  return { ...readConfig().providers.connections?.[providerId] ?? {} };
@@ -34524,8 +34834,7 @@ function deleteProviderExtensionAccount(providerIdValue) {
34524
34834
  }
34525
34835
  });
34526
34836
  }
34527
- //#endregion
34528
- //#region packages/core/src/provider-extensions/discovery.ts
34837
+ // packages/core/src/provider-extensions/discovery.ts
34529
34838
  async function listInstalledProviderExtensions() {
34530
34839
  const installed = readInstalledProviderPackages().packages;
34531
34840
  const loaded = await loadInstalledProviderExtensions({ installed });
@@ -34572,8 +34881,7 @@ async function describeInstalledProviderExtension(id) {
34572
34881
  }))
34573
34882
  };
34574
34883
  }
34575
- //#endregion
34576
- //#region packages/core/src/provider-extensions/installer.ts
34884
+ // packages/core/src/provider-extensions/installer.ts
34577
34885
  const execFileAsync = promisify(execFile);
34578
34886
  const MAX_PROVIDER_UNPACKED_BYTES = 5242880;
34579
34887
  const MAX_NPM_OUTPUT_BYTES = 1048576;
@@ -34785,8 +35093,7 @@ async function uninstallProviderExtension(id, options = {}) {
34785
35093
  ], { cwd: packagesDir });
34786
35094
  return removeInstalledProviderPackage(id);
34787
35095
  }
34788
- //#endregion
34789
- //#region packages/core/src/providers/ahrefs/schema.ts
35096
+ // packages/core/src/providers/ahrefs/schema.ts
34790
35097
  const ahrefsLimitsAndUsageResponseSchema = z.object({ limits_and_usage: z.object({
34791
35098
  api_key_expiration_date: z.string().trim().min(1).max(100),
34792
35099
  subscription: z.string().trim().min(1).max(200),
@@ -34796,8 +35103,7 @@ const ahrefsLimitsAndUsageResponseSchema = z.object({ limits_and_usage: z.object
34796
35103
  units_usage_workspace: z.number().int().nonnegative().nullable(),
34797
35104
  usage_reset_date: z.string().trim().min(1).max(100)
34798
35105
  }).strict() }).strict();
34799
- //#endregion
34800
- //#region packages/core/src/providers/ahrefs/client.ts
35106
+ // packages/core/src/providers/ahrefs/client.ts
34801
35107
  const DEFAULT_BASE_URL$1 = "https://api.ahrefs.com/v3/";
34802
35108
  const LIMITS_AND_USAGE_PATH = "subscription-info/limits-and-usage";
34803
35109
  const DEFAULT_TIMEOUT_MS$1 = 1e4;
@@ -34884,8 +35190,7 @@ var AhrefsClient = class {
34884
35190
  return this.limitsAndUsageForKey(await this.getApiKey("limits-and-usage"));
34885
35191
  }
34886
35192
  };
34887
- //#endregion
34888
- //#region packages/core/src/providers/dataforseo/account-schema.ts
35193
+ // packages/core/src/providers/dataforseo/account-schema.ts
34889
35194
  const userDataMoneySchema = z.object({
34890
35195
  total: z.number().finite().nonnegative().optional(),
34891
35196
  balance: z.number().finite().optional()
@@ -34912,8 +35217,7 @@ const dataForSeoUserDataResponseSchema = z.object({
34912
35217
  tasks_error: z.number().int().nonnegative(),
34913
35218
  tasks: z.array(userDataTaskSchema).max(10)
34914
35219
  }).passthrough();
34915
- //#endregion
34916
- //#region packages/core/src/providers/dataforseo/response.ts
35220
+ // packages/core/src/providers/dataforseo/response.ts
34917
35221
  function dataForSeoUsdToMicros(value) {
34918
35222
  if (value === void 0) return null;
34919
35223
  return Math.round(value * 1e6);
@@ -34923,8 +35227,7 @@ function dataForSeoTaskErrorCode(statusCode) {
34923
35227
  if (statusCode === 40202) return "rate-limit";
34924
35228
  return "remote-error";
34925
35229
  }
34926
- //#endregion
34927
- //#region packages/core/src/providers/dataforseo/client.ts
35230
+ // packages/core/src/providers/dataforseo/client.ts
34928
35231
  const DEFAULT_BASE_URL = "https://api.dataforseo.com/";
34929
35232
  const USER_DATA_PATH = "v3/appendix/user_data";
34930
35233
  const DEFAULT_TIMEOUT_MS = 15e3;
@@ -35019,8 +35322,7 @@ var DataForSeoClient = class {
35019
35322
  return this.userDataForCredentials(await this.getCredentials("account-status"));
35020
35323
  }
35021
35324
  };
35022
- //#endregion
35023
- //#region packages/core/src/runtime/interfaces.ts
35325
+ // packages/core/src/runtime/interfaces.ts
35024
35326
  var LocalCredentialsProvider = class {
35025
35327
  getGoogleOAuthClient() {
35026
35328
  const byo = readOauthClient();
@@ -35086,8 +35388,621 @@ var JsonFileStorageAdapter = class {
35086
35388
  }
35087
35389
  };
35088
35390
  const localCredentialsProvider = new LocalCredentialsProvider();
35089
- //#endregion
35090
- //#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
35091
36006
  async function resetSeoData() {
35092
36007
  const failures = (await Promise.allSettled([deleteTokens(), deleteManagedProviderSecrets()])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
35093
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) });
@@ -35105,8 +36020,7 @@ async function resetSeoData() {
35105
36020
  force: true
35106
36021
  });
35107
36022
  }
35108
- //#endregion
35109
- //#region packages/core/src/update-notice.ts
36023
+ // packages/core/src/update-notice.ts
35110
36024
  const REGISTRY_URL = "https://registry.npmjs.org/seo/latest";
35111
36025
  const CHECK_EVERY_MS = 864e5;
35112
36026
  const TIMEOUT_MS = 2e3;
@@ -35193,8 +36107,7 @@ function updateNotice(options = {}) {
35193
36107
  return;
35194
36108
  }
35195
36109
  }
35196
- //#endregion
35197
- //#region packages/core/src/utils/time.ts
36110
+ // packages/core/src/utils/time.ts
35198
36111
  function parseOlderThan(input) {
35199
36112
  if (!input) return;
35200
36113
  const match = input.trim().match(/^(\d+)([smhdw])$/i);
@@ -35227,7 +36140,6 @@ function isoDate(daysAgo = 0) {
35227
36140
  date.setUTCDate(date.getUTCDate() - daysAgo);
35228
36141
  return date.toISOString().slice(0, 10);
35229
36142
  }
35230
- //#endregion
35231
- 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 };
35232
36144
 
35233
36145
  //# sourceMappingURL=index.js.map