seo 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as load, t as version } from "./package-D46BDwUk.js";
2
+ import { n as load, t as version } from "./package-CwLtPH6B.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";
6
- import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7
- import { arch, homedir, platform, totalmem } from "node:os";
6
+ import { arch, homedir, hostname, platform, totalmem } from "node:os";
8
7
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import open from "open";
9
+ import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
10
  import lockfile from "proper-lockfile";
10
11
  import http from "node:http";
11
- import open from "open";
12
12
  import { Headers as Headers$1, fetch as fetch$1 } from "undici";
13
13
  import Libsql from "libsql";
14
14
  import { Parser } from "htmlparser2";
@@ -1596,6 +1596,327 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1596
1596
  });
1597
1597
  }
1598
1598
  //#endregion
1599
+ //#region packages/core/src/bing/api.ts
1600
+ const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1601
+ const MAX_BYTES$2 = 8e6;
1602
+ function bingCall(auth, options) {
1603
+ return async (method, params = {}) => {
1604
+ const url = new URL(`${BASE}/${method}`);
1605
+ for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
1606
+ const headers = { accept: "application/json" };
1607
+ if ("apiKey" in auth) url.searchParams.set("apikey", auth.apiKey);
1608
+ else headers.authorization = `Bearer ${await auth.accessToken()}`;
1609
+ let response;
1610
+ try {
1611
+ response = await fetch(url, {
1612
+ headers,
1613
+ signal: AbortSignal.timeout(3e4)
1614
+ });
1615
+ } catch (error) {
1616
+ throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster did not respond: ${error instanceof Error ? error.message : String(error)}`);
1617
+ }
1618
+ if (!response.ok) {
1619
+ const detail = (await response.text().catch(() => "")).slice(0, 200);
1620
+ if (response.status === 401 || /invalidapikey/i.test(detail)) throw new SeoError("AUTH_REQUIRED", `Bing Webmaster rejected the sign-in. ${options.signIn}`);
1621
+ if (response.status === 403 || /notauthorized/i.test(detail)) throw new SeoError("ACCESS_DENIED", "Bing Webmaster denied access to this site. It must be verified in the connected Bing account.");
1622
+ if (/ThrottleIP/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is blocking requests from this network (ThrottleIP). Run the same report with the CLI on your own machine, where Bing answers.");
1623
+ if (response.status === 429 || /throttle/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is limiting requests. Try again in a few minutes.");
1624
+ throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
1625
+ }
1626
+ return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$2 })).d;
1627
+ };
1628
+ }
1629
+ //#endregion
1630
+ //#region packages/core/src/gsc/dates.ts
1631
+ const GSC_TIME_ZONE = "America/Los_Angeles";
1632
+ const GSC_FINAL_DATA_LAG_DAYS = 4;
1633
+ function dateInTimeZone(now, timeZone) {
1634
+ const parts = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
1635
+ timeZone,
1636
+ year: "numeric",
1637
+ month: "2-digit",
1638
+ day: "2-digit"
1639
+ }).formatToParts(now).map((part) => [part.type, part.value]));
1640
+ return new Date(Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day)));
1641
+ }
1642
+ function shiftIsoDate(date, days) {
1643
+ const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
1644
+ value.setUTCDate(value.getUTCDate() + days);
1645
+ return value.toISOString().slice(0, 10);
1646
+ }
1647
+ function latestFinalGscDate(now = /* @__PURE__ */ new Date()) {
1648
+ const pacificToday = dateInTimeZone(now, GSC_TIME_ZONE);
1649
+ pacificToday.setUTCDate(pacificToday.getUTCDate() - 4);
1650
+ return pacificToday.toISOString().slice(0, 10);
1651
+ }
1652
+ function finalGscDateRange(days = 28, now = /* @__PURE__ */ new Date()) {
1653
+ const endDate = latestFinalGscDate(now);
1654
+ return {
1655
+ startDate: shiftIsoDate(endDate, -(days - 1)),
1656
+ endDate
1657
+ };
1658
+ }
1659
+ //#endregion
1660
+ //#region packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
1661
+ function integerOption$1(input) {
1662
+ if (input.value === void 0) return input.fallback;
1663
+ 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}.`);
1664
+ return input.value;
1665
+ }
1666
+ function defaultDateRange$2(days, now) {
1667
+ return finalGscDateRange(days, now);
1668
+ }
1669
+ function explicitDateRange(input, maximumDays) {
1670
+ if (!input.startDate && !input.endDate) return void 0;
1671
+ if (!input.startDate || !input.endDate) throw new SeoError("INVALID_INPUT", "Start date and end date must be provided together.");
1672
+ const pattern = /^\d{4}-\d{2}-\d{2}$/;
1673
+ const start = /* @__PURE__ */ new Date(`${input.startDate}T00:00:00.000Z`);
1674
+ const end = /* @__PURE__ */ new Date(`${input.endDate}T00:00:00.000Z`);
1675
+ if (!pattern.test(input.startDate) || !pattern.test(input.endDate) || Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start.toISOString().slice(0, 10) !== input.startDate || end.toISOString().slice(0, 10) !== input.endDate || start > end) throw new SeoError("INVALID_INPUT", "Start date and end date must be valid YYYY-MM-DD values in ascending order.");
1676
+ const days = Math.floor((end.getTime() - start.getTime()) / 864e5) + 1;
1677
+ if (days > maximumDays) throw new SeoError("INVALID_INPUT", `Date range must not exceed ${maximumDays} days.`);
1678
+ return {
1679
+ range: {
1680
+ startDate: input.startDate,
1681
+ endDate: input.endDate
1682
+ },
1683
+ days
1684
+ };
1685
+ }
1686
+ //#endregion
1687
+ //#region packages/core/src/analyze/site-diagnostics/decay-report-input.ts
1688
+ function decayNumberOption(input) {
1689
+ const value = input.value ?? input.fallback;
1690
+ 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}.`);
1691
+ return value;
1692
+ }
1693
+ function rangeDays$1(range) {
1694
+ return Math.floor(((/* @__PURE__ */ new Date(`${range.endDate}T00:00:00.000Z`)).getTime() - (/* @__PURE__ */ new Date(`${range.startDate}T00:00:00.000Z`)).getTime()) / 864e5) + 1;
1695
+ }
1696
+ function dateString(date) {
1697
+ return date.toISOString().slice(0, 10);
1698
+ }
1699
+ function subtractDays$1(value, days) {
1700
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
1701
+ date.setUTCDate(date.getUTCDate() - days);
1702
+ return dateString(date);
1703
+ }
1704
+ function priorYearDate(value) {
1705
+ const source = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
1706
+ const year = source.getUTCFullYear() - 1;
1707
+ const month = source.getUTCMonth();
1708
+ const day = source.getUTCDate();
1709
+ const candidate = new Date(Date.UTC(year, month, day));
1710
+ if (candidate.getUTCMonth() !== month) return dateString(new Date(Date.UTC(year, month + 1, 0)));
1711
+ return dateString(candidate);
1712
+ }
1713
+ function subtractCalendarMonths(date, months) {
1714
+ const day = date.getUTCDate();
1715
+ const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() - months, 1));
1716
+ const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
1717
+ target.setUTCDate(Math.min(day, lastDay));
1718
+ return dateString(target);
1719
+ }
1720
+ function validateDecayRanges(input) {
1721
+ const latestFinalDate = defaultDateRange$2(1, input.now).endDate;
1722
+ if (input.current.endDate > latestFinalDate) throw new SeoError("INVALID_INPUT", `Decay end date must be ${latestFinalDate} or earlier so both GSC windows use final data.`);
1723
+ const pacificToday = /* @__PURE__ */ new Date(`${latestFinalDate}T00:00:00.000Z`);
1724
+ pacificToday.setUTCDate(pacificToday.getUTCDate() + 4);
1725
+ const retentionStart = subtractCalendarMonths(pacificToday, 16);
1726
+ if (input.previous.startDate < retentionStart) throw new SeoError("INVALID_INPUT", `Comparison range starts before Search Console's rolling 16-month API history (${retentionStart}). Choose a shorter range or use previously stored data.`);
1727
+ }
1728
+ function decayComparisonRange(current, comparison = "previous-period") {
1729
+ const days = rangeDays$1(current);
1730
+ if (comparison === "year-over-year") {
1731
+ const endDate = priorYearDate(current.endDate);
1732
+ return {
1733
+ startDate: subtractDays$1(endDate, days - 1),
1734
+ endDate
1735
+ };
1736
+ }
1737
+ const endDate = subtractDays$1(current.startDate, 1);
1738
+ return {
1739
+ startDate: subtractDays$1(endDate, days - 1),
1740
+ endDate
1741
+ };
1742
+ }
1743
+ //#endregion
1744
+ //#region packages/core/src/updates/search-updates.ts
1745
+ function classifyUpdate(name, product) {
1746
+ const lower = `${name} ${product}`.toLowerCase();
1747
+ if (lower.includes("core")) return "core";
1748
+ if (lower.includes("spam")) return "spam";
1749
+ if (lower.includes("discover")) return "discover";
1750
+ if (lower.includes("ranking")) return "ranking";
1751
+ if (lower.includes("index")) return "indexing";
1752
+ if (lower.includes("serving")) return "serving";
1753
+ return "other";
1754
+ }
1755
+ function searchUpdatesFromIncidents(incidents, input = {}) {
1756
+ const product = input.product?.toLowerCase();
1757
+ return incidents.filter((incident) => !product || incident.service_name.toLowerCase() === product).map((incident) => {
1758
+ const status = incident.end ? "complete" : incident.status_impact === "SERVICE_INFORMATION" ? "rolling-out" : "incident";
1759
+ return {
1760
+ id: incident.id,
1761
+ name: incident.external_desc,
1762
+ type: classifyUpdate(incident.external_desc, incident.service_name),
1763
+ product: incident.service_name,
1764
+ start: incident.begin,
1765
+ end: incident.end,
1766
+ status,
1767
+ sourceUrl: `https://status.search.google.com/${incident.uri}`
1768
+ };
1769
+ }).slice(0, input.limit ?? 25);
1770
+ }
1771
+ function findOverlappingSearchUpdates(input) {
1772
+ const paddingMs = (input.paddingDays ?? 3) * 864e5;
1773
+ const windowStart = new Date(input.startDate).getTime() - paddingMs;
1774
+ const windowEnd = new Date(input.endDate).getTime() + paddingMs;
1775
+ return input.updates.filter((update) => {
1776
+ const updateStart = new Date(update.start).getTime();
1777
+ const updateEnd = update.end ? new Date(update.end).getTime() : Date.now();
1778
+ return updateStart <= windowEnd && updateEnd >= windowStart;
1779
+ });
1780
+ }
1781
+ //#endregion
1782
+ //#region packages/core/src/commands/types.ts
1783
+ var CommandError = class extends Error {
1784
+ code;
1785
+ constructor(code, message) {
1786
+ super(message);
1787
+ this.code = code;
1788
+ }
1789
+ };
1790
+ const ADVICE_KEYS = /^(recommendations?|actions?|advice|suggested[A-Z]\w*|nextSteps?)$/;
1791
+ function factsOnly(value) {
1792
+ if (Array.isArray(value)) return value.map(factsOnly);
1793
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !ADVICE_KEYS.test(key)).map(([key, child]) => [key, factsOnly(child)]));
1794
+ return value;
1795
+ }
1796
+ function matchProperty(names, site) {
1797
+ if (names.includes(site)) return site;
1798
+ let host;
1799
+ let origin;
1800
+ try {
1801
+ const url = new URL(site.includes("://") ? site : `https://${site.replace(/^sc-domain:/, "")}`);
1802
+ host = url.hostname.replace(/^www\./, "");
1803
+ origin = url.origin;
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ const domain = names.filter((name) => name.startsWith("sc-domain:")).map((name) => name.slice(10)).filter((name) => host === name || host.endsWith(`.${name}`)).sort((a, b) => b.length - a.length)[0];
1808
+ if (domain) return `sc-domain:${domain}`;
1809
+ return names.find((name) => name === `${origin}/` || name === `${origin.replace("://", "://www.")}/` || name === `${origin.replace("://www.", "://")}/`) ?? null;
1810
+ }
1811
+ //#endregion
1812
+ //#region packages/core/src/commands/sites.ts
1813
+ async function readableSites(google, signIn) {
1814
+ return (await listSearchConsoleSites(google, { signIn })).filter((entry) => entry.permissionLevel !== "siteUnverifiedUser").map((entry) => ({
1815
+ siteUrl: entry.siteUrl,
1816
+ permissionLevel: entry.permissionLevel ?? ""
1817
+ })).sort((a, b) => a.siteUrl.localeCompare(b.siteUrl));
1818
+ }
1819
+ //#endregion
1820
+ //#region packages/core/src/commands/linked.ts
1821
+ const MAX_ROWS$1 = 1e5;
1822
+ const PERMISSION_RANK = {
1823
+ siteOwner: 0,
1824
+ siteFullUser: 1,
1825
+ siteRestrictedUser: 2
1826
+ };
1827
+ function bingRouter(calls) {
1828
+ if (calls.length === 0) return null;
1829
+ const only = calls[0];
1830
+ if (calls.length === 1 && only) return only;
1831
+ let lists;
1832
+ const sitesOf = () => {
1833
+ lists ??= Promise.all(calls.map(async (call) => ({
1834
+ call,
1835
+ sites: await call("GetUserSites").then((data) => Array.isArray(data) ? data : []).catch(() => [])
1836
+ })));
1837
+ return lists;
1838
+ };
1839
+ return async (method, params = {}) => {
1840
+ const all = await sitesOf();
1841
+ if (method === "GetUserSites") return all.flatMap((item) => item.sites);
1842
+ return (all.find((item) => item.sites.some((entry) => entry.IsVerified === true && entry.Url === params.siteUrl))?.call ?? only)(method, params);
1843
+ };
1844
+ }
1845
+ var LinkedAccounts = class {
1846
+ hint;
1847
+ google;
1848
+ bing;
1849
+ sites = /* @__PURE__ */ new Map();
1850
+ owners;
1851
+ constructor(input, hint) {
1852
+ this.hint = hint;
1853
+ this.google = input.google.map((account) => ({
1854
+ email: account.email,
1855
+ google: bearerFetch(account.token)
1856
+ }));
1857
+ this.bing = bingRouter(input.bing);
1858
+ }
1859
+ accounts() {
1860
+ return this.google.map((linked) => ({
1861
+ email: linked.email,
1862
+ google: linked.google
1863
+ }));
1864
+ }
1865
+ sitesOf(linked) {
1866
+ let found = this.sites.get(linked);
1867
+ if (!found) {
1868
+ found = readableSites(linked.google, this.hint.signIn);
1869
+ this.sites.set(linked, found);
1870
+ }
1871
+ return found;
1872
+ }
1873
+ ownerMap() {
1874
+ this.owners ??= (async () => {
1875
+ const lists = await Promise.all(this.google.map(async (linked) => ({
1876
+ linked,
1877
+ sites: await this.sitesOf(linked).catch((error) => {
1878
+ if (this.google.length === 1) throw error;
1879
+ return [];
1880
+ })
1881
+ })));
1882
+ const best = /* @__PURE__ */ new Map();
1883
+ for (const { linked, sites } of lists) for (const site of sites) {
1884
+ const rank = PERMISSION_RANK[site.permissionLevel] ?? 3;
1885
+ const found = best.get(site.siteUrl);
1886
+ if (!found || rank < found.rank) best.set(site.siteUrl, {
1887
+ linked,
1888
+ rank
1889
+ });
1890
+ }
1891
+ return new Map([...best].map(([site, { linked }]) => [site, linked]));
1892
+ })();
1893
+ return this.owners;
1894
+ }
1895
+ async resolve(site) {
1896
+ if (this.google.length === 0) throw new CommandError("NOT_CONNECTED", `No Google account is connected. ${this.hint.signIn}`);
1897
+ const found = matchProperty([...(await this.ownerMap()).keys()], site);
1898
+ if (found) return found;
1899
+ throw new CommandError("NO_ACCESS", `None of the connected Google accounts (${this.google.map((linked) => linked.email).join(", ")}) has a Search Console property for ${site}. ${this.hint.addGoogle}`);
1900
+ }
1901
+ async owner(site) {
1902
+ const owner = (await this.ownerMap()).get(await this.resolve(site));
1903
+ if (!owner) throw new CommandError("NO_ACCESS", `No access to ${site}.`);
1904
+ return owner;
1905
+ }
1906
+ searchAnalytics() {
1907
+ return async (site, body) => searchAnalyticsRows((await this.owner(site)).google, site, {
1908
+ ...body,
1909
+ maxRows: body.maxRows ?? body.rowLimit ?? MAX_ROWS$1
1910
+ }, { signIn: this.hint.signIn });
1911
+ }
1912
+ async inspect(site, url) {
1913
+ return inspectSearchConsoleUrl((await this.owner(site)).google, site, url, {
1914
+ signIn: this.hint.signIn,
1915
+ languageCode: "en-US"
1916
+ });
1917
+ }
1918
+ };
1919
+ //#endregion
1599
1920
  //#region packages/core/src/paths.ts
1600
1921
  let memoizedPaths;
1601
1922
  function defaultConfigDir(input) {
@@ -1692,6 +2013,189 @@ function fileSize(path) {
1692
2013
  }
1693
2014
  }
1694
2015
  //#endregion
2016
+ //#region packages/core/src/storage/files.ts
2017
+ function ensureParentDir(path, mode = 448) {
2018
+ mkdirSync(dirname(path), {
2019
+ recursive: true,
2020
+ mode
2021
+ });
2022
+ }
2023
+ function readJsonFile(path) {
2024
+ try {
2025
+ return JSON.parse(readFileSync(path, "utf8"));
2026
+ } catch {
2027
+ return;
2028
+ }
2029
+ }
2030
+ function writeJsonAtomic(path, data, mode) {
2031
+ ensureParentDir(path);
2032
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
2033
+ writeFileSync(tmp, JSON.stringify(data, null, 2), {
2034
+ encoding: "utf8",
2035
+ mode
2036
+ });
2037
+ chmodSync(tmp, mode);
2038
+ renameSync(tmp, path);
2039
+ chmodSync(path, mode);
2040
+ }
2041
+ function fileMode(path) {
2042
+ try {
2043
+ return `0${(statSync(path).mode & 511).toString(8)}`;
2044
+ } catch {
2045
+ return "missing";
2046
+ }
2047
+ }
2048
+ function safeRemove(path) {
2049
+ rmSync(path, {
2050
+ recursive: true,
2051
+ force: true
2052
+ });
2053
+ }
2054
+ //#endregion
2055
+ //#region packages/core/src/cloud.ts
2056
+ const CLOUD_URL = "https://seoskill.dev";
2057
+ const PRIVATE = 384;
2058
+ const EARLY = 6e4;
2059
+ const linkFile = () => join(getSeoCliPaths().configDir, "cloud.json");
2060
+ const pendingFile = () => join(getSeoCliPaths().configDir, "cloud-login.json");
2061
+ function cloudToken() {
2062
+ const fromEnv = process.env.SEO_TOKEN?.trim();
2063
+ if (fromEnv) return fromEnv;
2064
+ return readJsonFile(linkFile())?.token;
2065
+ }
2066
+ function cloudLinked() {
2067
+ return Boolean(cloudToken());
2068
+ }
2069
+ const CLOUD_SIGN_IN = "Run `seo login` to link this machine to your seoskill.dev account again.";
2070
+ async function call$1(path, init = {}) {
2071
+ const response = await fetch(`${CLOUD_URL}${path}`, {
2072
+ ...init,
2073
+ headers: {
2074
+ "content-type": "application/json",
2075
+ ...init.token ? { authorization: `Bearer ${init.token}` } : {}
2076
+ }
2077
+ }).catch(() => {
2078
+ throw new SeoError("PROVIDER_UNAVAILABLE", `Could not reach ${CLOUD_URL}. Check the network and try again.`);
2079
+ });
2080
+ const body = await readJson(response, "seoskill.dev").catch(() => ({}));
2081
+ return {
2082
+ ok: response.ok,
2083
+ status: response.status,
2084
+ body
2085
+ };
2086
+ }
2087
+ let credentials;
2088
+ function expiresSoon(value) {
2089
+ return [...value.google, ...value.bing].some((item) => "expiresAt" in item && item.expiresAt - EARLY < Date.now());
2090
+ }
2091
+ async function cloudCredentials(options = {}) {
2092
+ const current = credentials ? await credentials.catch(() => null) : null;
2093
+ if (current && !expiresSoon(current) && !options.refreshClicky) return current;
2094
+ credentials = (async () => {
2095
+ const token = cloudToken();
2096
+ if (!token) throw new SeoError("AUTH_REQUIRED", notSignedIn());
2097
+ const result = await call$1(`/account/cli/credentials${options.refreshClicky ? "?clicky=refresh" : ""}`, { token });
2098
+ if (result.status === 401) throw new SeoError("AUTH_EXPIRED", `This machine is not linked to a seoskill.dev account any more. ${CLOUD_SIGN_IN}`);
2099
+ if (!result.ok) throw new SeoError("PROVIDER_UNAVAILABLE", `seoskill.dev returned ${result.status}. Try again in a minute.`);
2100
+ return result.body;
2101
+ })();
2102
+ return credentials;
2103
+ }
2104
+ function notSignedIn() {
2105
+ return "Not signed in. Run `seo login` to use your seoskill.dev account on this machine (it prints a link to approve), or `seo auth login` to sign in with Google on this machine only.";
2106
+ }
2107
+ function tokenOf(item, what) {
2108
+ if ("error" in item) throw new SeoError("AUTH_EXPIRED", `${what}: ${item.error}`);
2109
+ return item.token;
2110
+ }
2111
+ async function cloudGoogleToken(email) {
2112
+ const { google, account } = await cloudCredentials();
2113
+ const found = email ? google.find((item) => item.email.toLowerCase() === email.toLowerCase()) : google[0];
2114
+ if (!found) throw new SeoError("AUTH_REQUIRED", email ? `${email} is not linked to your seoskill.dev account. Add it at ${account}#google.` : `No Google account is linked to your seoskill.dev account. Add one at ${account}#google.`);
2115
+ return tokenOf(found, `Google sign-in for ${found.email}`);
2116
+ }
2117
+ async function cloudGoogleEmails() {
2118
+ return (await cloudCredentials()).google.map((item) => item.email);
2119
+ }
2120
+ let linked;
2121
+ async function cloudAccountFor(site) {
2122
+ if (!cloudLinked()) return void 0;
2123
+ linked ??= cloudGoogleEmails().then((emails) => new LinkedAccounts({
2124
+ google: emails.map((email) => ({
2125
+ email,
2126
+ token: () => cloudGoogleToken(email)
2127
+ })),
2128
+ bing: []
2129
+ }, {
2130
+ signIn: CLOUD_SIGN_IN,
2131
+ addGoogle: "Add it on your seoskill.dev account page."
2132
+ }));
2133
+ const accounts = await linked;
2134
+ if (accounts.google.length < 2) return accounts.google[0]?.email;
2135
+ return (await accounts.owner(site)).email;
2136
+ }
2137
+ async function cloudBing() {
2138
+ const { bing, account } = await cloudCredentials();
2139
+ return bingRouter(bing.map((_, index) => bingCall({ accessToken: async () => {
2140
+ const item = (await cloudCredentials()).bing[index];
2141
+ if (!item) throw new SeoError("AUTH_EXPIRED", CLOUD_SIGN_IN);
2142
+ return tokenOf(item, "Bing sign-in");
2143
+ } }, { signIn: `Reconnect Bing at ${account}.` })));
2144
+ }
2145
+ async function cloudKeys() {
2146
+ return (await cloudCredentials()).keys;
2147
+ }
2148
+ async function startCloudLogin() {
2149
+ const result = await call$1("/account/cli/start", {
2150
+ method: "POST",
2151
+ body: JSON.stringify({ label: `seo on ${hostname().split(".")[0] || "this machine"}`.slice(0, 60) })
2152
+ });
2153
+ const { device, code, url, expiresIn } = result.body;
2154
+ if (!result.ok || !device || !code || !url) throw new SeoError("PROVIDER_UNAVAILABLE", `seoskill.dev could not start a login (${result.status}). Try again.`);
2155
+ const pending = {
2156
+ device,
2157
+ code,
2158
+ url,
2159
+ expiresAt: Date.now() + (expiresIn ?? 600) * 1e3
2160
+ };
2161
+ ensureSeoCliDirs();
2162
+ writeJsonAtomic(pendingFile(), pending, PRIVATE);
2163
+ return pending;
2164
+ }
2165
+ function pendingCloudLogin() {
2166
+ const pending = readJsonFile(pendingFile());
2167
+ if (!pending || pending.expiresAt < Date.now()) return null;
2168
+ return pending;
2169
+ }
2170
+ async function finishCloudLogin() {
2171
+ const pending = pendingCloudLogin();
2172
+ if (!pending) return "expired";
2173
+ const result = await call$1("/account/cli/token", {
2174
+ method: "POST",
2175
+ body: JSON.stringify({ device: pending.device })
2176
+ });
2177
+ if (result.body.error === "pending") return "pending";
2178
+ safeRemove(pendingFile());
2179
+ if (!result.ok || !result.body.token) return "expired";
2180
+ writeJsonAtomic(linkFile(), { token: result.body.token }, PRIVATE);
2181
+ credentials = void 0;
2182
+ return "linked";
2183
+ }
2184
+ async function cloudLogout() {
2185
+ const token = readJsonFile(linkFile())?.token;
2186
+ if (!token) return false;
2187
+ await call$1("/account/cli/logout", {
2188
+ method: "POST",
2189
+ token
2190
+ }).catch(() => void 0);
2191
+ safeRemove(linkFile());
2192
+ credentials = void 0;
2193
+ return true;
2194
+ }
2195
+ async function openCloudLink(url) {
2196
+ await open(url).catch(() => void 0);
2197
+ }
2198
+ //#endregion
1695
2199
  //#region packages/core/src/types/auth.ts
1696
2200
  const tokenSchema = z.object({
1697
2201
  provider: z.literal("google"),
@@ -1749,76 +2253,37 @@ const analyticsExtensionAccountSchema = z.record(z.string().min(1).max(64).regex
1749
2253
  "constructor",
1750
2254
  "prototype",
1751
2255
  "__proto__"
1752
- ].includes(value)), z.string().trim().min(1).max(1e3)).refine((value) => Object.keys(value).length <= 16, { message: "An analytics provider account can have at most 16 fields." });
1753
- const providerConnectionAccountsSchema = z.record(providerIdSchema, analyticsExtensionAccountSchema).refine((value) => Object.keys(value).length <= 20, { message: "There can be at most 20 provider connection records." });
1754
- const analyticsConnectionSchema = z.discriminatedUnion("provider", [
1755
- z.object({
1756
- provider: z.literal("google"),
1757
- propertyId: z.string(),
1758
- accountEmail: z.string().email().optional()
1759
- }),
1760
- z.object({
1761
- provider: z.literal("clicky"),
1762
- siteId: z.string().regex(/^\d{1,30}$/u)
1763
- }),
1764
- z.object({
1765
- provider: z.literal("extension"),
1766
- providerId: analyticsExtensionIdSchema,
1767
- account: analyticsExtensionAccountSchema
1768
- })
1769
- ]);
1770
- const configSchema = z.looseObject({
1771
- providers: z.object({
1772
- dataForSeoLogin: z.string().optional(),
1773
- dataForSeoPassword: z.string().optional(),
1774
- prefer: providerPreferenceSchema.default("cheap"),
1775
- connections: providerConnectionAccountsSchema.optional()
1776
- }).default({ prefer: "cheap" }),
1777
- security: z.object({ useKeychain: z.boolean().default(true) }).default({ useKeychain: true }),
1778
- auth: z.object({
1779
- sharedClientId: z.string().optional(),
1780
- sharedClientSecret: z.string().optional()
1781
- }).default({})
1782
- });
1783
- //#endregion
1784
- //#region packages/core/src/storage/files.ts
1785
- function ensureParentDir(path, mode = 448) {
1786
- mkdirSync(dirname(path), {
1787
- recursive: true,
1788
- mode
1789
- });
1790
- }
1791
- function readJsonFile(path) {
1792
- try {
1793
- return JSON.parse(readFileSync(path, "utf8"));
1794
- } catch {
1795
- return;
1796
- }
1797
- }
1798
- function writeJsonAtomic(path, data, mode) {
1799
- ensureParentDir(path);
1800
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
1801
- writeFileSync(tmp, JSON.stringify(data, null, 2), {
1802
- encoding: "utf8",
1803
- mode
1804
- });
1805
- chmodSync(tmp, mode);
1806
- renameSync(tmp, path);
1807
- chmodSync(path, mode);
1808
- }
1809
- function fileMode(path) {
1810
- try {
1811
- return `0${(statSync(path).mode & 511).toString(8)}`;
1812
- } catch {
1813
- return "missing";
1814
- }
1815
- }
1816
- function safeRemove(path) {
1817
- rmSync(path, {
1818
- recursive: true,
1819
- force: true
1820
- });
1821
- }
2256
+ ].includes(value)), z.string().trim().min(1).max(1e3)).refine((value) => Object.keys(value).length <= 16, { message: "An analytics provider account can have at most 16 fields." });
2257
+ const providerConnectionAccountsSchema = z.record(providerIdSchema, analyticsExtensionAccountSchema).refine((value) => Object.keys(value).length <= 20, { message: "There can be at most 20 provider connection records." });
2258
+ const analyticsConnectionSchema = z.discriminatedUnion("provider", [
2259
+ z.object({
2260
+ provider: z.literal("google"),
2261
+ propertyId: z.string(),
2262
+ accountEmail: z.string().email().optional()
2263
+ }),
2264
+ z.object({
2265
+ provider: z.literal("clicky"),
2266
+ siteId: z.string().regex(/^\d{1,30}$/u)
2267
+ }),
2268
+ z.object({
2269
+ provider: z.literal("extension"),
2270
+ providerId: analyticsExtensionIdSchema,
2271
+ account: analyticsExtensionAccountSchema
2272
+ })
2273
+ ]);
2274
+ const configSchema = z.looseObject({
2275
+ providers: z.object({
2276
+ dataForSeoLogin: z.string().optional(),
2277
+ dataForSeoPassword: z.string().optional(),
2278
+ prefer: providerPreferenceSchema.default("cheap"),
2279
+ connections: providerConnectionAccountsSchema.optional()
2280
+ }).default({ prefer: "cheap" }),
2281
+ security: z.object({ useKeychain: z.boolean().default(true) }).default({ useKeychain: true }),
2282
+ auth: z.object({
2283
+ sharedClientId: z.string().optional(),
2284
+ sharedClientSecret: z.string().optional()
2285
+ }).default({})
2286
+ });
1822
2287
  let loadedKeyring;
1823
2288
  async function resolveKeyring() {
1824
2289
  if (!loadedKeyring) loadedKeyring = import("@napi-rs/keyring/keytar.js").then((module) => module);
@@ -2407,7 +2872,7 @@ async function exchangeGoogleCode(input, fetchImpl = fetch) {
2407
2872
  //#region packages/core/src/gsc/auth/authorized-client.ts
2408
2873
  const REFRESH_BUFFER_MS = 6e4;
2409
2874
  function authRequired(accountEmail) {
2410
- return new SeoError("AUTH_REQUIRED", accountEmail ? `Google account ${accountEmail} is not connected. Run \`seo auth login\` first.` : "Not logged in. Run `seo auth login` first.");
2875
+ return new SeoError("AUTH_REQUIRED", accountEmail ? `Google account ${accountEmail} is not connected. Run \`seo auth login\` first.` : notSignedIn());
2411
2876
  }
2412
2877
  function authExpired() {
2413
2878
  return new SeoError("AUTH_EXPIRED", "Google refresh token is no longer valid. Run `seo auth login` again.");
@@ -2506,6 +2971,18 @@ async function createGoogleAccessTokenClient(accountEmail) {
2506
2971
  accountEmail: serviceAccount.clientEmail
2507
2972
  }
2508
2973
  };
2974
+ if (cloudLinked()) {
2975
+ const email = accountEmail ?? (await cloudGoogleEmails())[0] ?? "";
2976
+ return {
2977
+ client: { getAccessToken: () => cloudGoogleToken(email || void 0) },
2978
+ mode: "cloud",
2979
+ identity: email,
2980
+ quotaIdentity: {
2981
+ clientId: "seoskill.dev",
2982
+ accountEmail: email
2983
+ }
2984
+ };
2985
+ }
2509
2986
  const { client, tokens } = await createAuthorizedClient(accountEmail);
2510
2987
  const clientConfig = requireClientConfig(tokens);
2511
2988
  return {
@@ -6696,7 +7173,7 @@ function inspectionResponseError(response, input) {
6696
7173
  }
6697
7174
  async function inspectUrl(input) {
6698
7175
  const inspectionUrl = assertUrlMatchesGscProperty(input.siteUrl, input.inspectionUrl);
6699
- const { client, quotaIdentity } = await getAuthorized(input.accountEmail);
7176
+ const { client, quotaIdentity } = await getAuthorized(input.accountEmail ?? await cloudAccountFor(input.siteUrl));
6700
7177
  const reservation = reserveUrlInspectionQuota({
6701
7178
  credentialKey: credentialKey(quotaIdentity),
6702
7179
  property: input.siteUrl,
@@ -6746,36 +7223,6 @@ async function inspectUrl(input) {
6746
7223
  return await readJson(response, "URL Inspection");
6747
7224
  }
6748
7225
  //#endregion
6749
- //#region packages/core/src/gsc/dates.ts
6750
- const GSC_TIME_ZONE = "America/Los_Angeles";
6751
- const GSC_FINAL_DATA_LAG_DAYS = 4;
6752
- function dateInTimeZone(now, timeZone) {
6753
- const parts = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
6754
- timeZone,
6755
- year: "numeric",
6756
- month: "2-digit",
6757
- day: "2-digit"
6758
- }).formatToParts(now).map((part) => [part.type, part.value]));
6759
- return new Date(Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day)));
6760
- }
6761
- function shiftIsoDate(date, days) {
6762
- const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
6763
- value.setUTCDate(value.getUTCDate() + days);
6764
- return value.toISOString().slice(0, 10);
6765
- }
6766
- function latestFinalGscDate(now = /* @__PURE__ */ new Date()) {
6767
- const pacificToday = dateInTimeZone(now, GSC_TIME_ZONE);
6768
- pacificToday.setUTCDate(pacificToday.getUTCDate() - 4);
6769
- return pacificToday.toISOString().slice(0, 10);
6770
- }
6771
- function finalGscDateRange(days = 28, now = /* @__PURE__ */ new Date()) {
6772
- const endDate = latestFinalGscDate(now);
6773
- return {
6774
- startDate: shiftIsoDate(endDate, -(days - 1)),
6775
- endDate
6776
- };
6777
- }
6778
- //#endregion
6779
7226
  //#region packages/core/src/gsc/client/search-analytics.ts
6780
7227
  function searchDateRange(days) {
6781
7228
  return finalGscDateRange(days);
@@ -6784,7 +7231,7 @@ function resolveSearchDateWindow(input = 28) {
6784
7231
  return typeof input === "number" ? searchDateRange(input) : input;
6785
7232
  }
6786
7233
  async function querySearchAnalytics(site, body, opts = {}) {
6787
- const { client, identity } = await getAuthorized(opts.accountEmail);
7234
+ const { client, identity } = await getAuthorized(opts.accountEmail ?? await cloudAccountFor(site));
6788
7235
  const db = getDb();
6789
7236
  const queryHash = hashKey([
6790
7237
  site,
@@ -6943,12 +7390,21 @@ async function queryPagesTopQueriesBatch(site, pageUrls, range = 28) {
6943
7390
  //#endregion
6944
7391
  //#region packages/core/src/gsc/client/sites.ts
6945
7392
  async function listSites(_refresh = false, accountEmail) {
6946
- const { client } = await getAuthorized(accountEmail);
6947
- const entries = await listSearchConsoleSites(googleFetchFor(client), LOCAL_SIGN_IN);
7393
+ const entries = !accountEmail && cloudLinked() ? await allCloudSites() : await listSearchConsoleSites(googleFetchFor((await getAuthorized(accountEmail)).client), LOCAL_SIGN_IN);
6948
7394
  const insert = getDb().prepare("INSERT OR REPLACE INTO sites (site_url, display_name, permission, added_at, is_default) VALUES (?, ?, ?, ?, COALESCE((SELECT is_default FROM sites WHERE site_url = ?), 0))");
6949
7395
  for (const site of entries) insert.run(site.siteUrl, site.siteUrl, site.permissionLevel ?? null, Date.now(), site.siteUrl);
6950
7396
  return entries;
6951
7397
  }
7398
+ async function allCloudSites() {
7399
+ const lists = await Promise.all((await cloudGoogleEmails()).map(async (email) => listSearchConsoleSites(googleFetchFor((await getAuthorized(email)).client), LOCAL_SIGN_IN)));
7400
+ const best = /* @__PURE__ */ new Map();
7401
+ const rank = (site) => PERMISSION_RANK[site.permissionLevel ?? ""] ?? 3;
7402
+ for (const site of lists.flat()) {
7403
+ const found = best.get(site.siteUrl);
7404
+ if (!found || rank(site) < rank(found)) best.set(site.siteUrl, site);
7405
+ }
7406
+ return [...best.values()];
7407
+ }
6952
7408
  //#endregion
6953
7409
  //#region packages/core/src/analyze/page-technical-signals.ts
6954
7410
  function normalizePageUrl(value) {
@@ -7654,7 +8110,7 @@ function jaccard(a, b) {
7654
8110
  const union = /* @__PURE__ */ new Set([...setA, ...setB]);
7655
8111
  return union.size === 0 ? 0 : intersection.length / union.size;
7656
8112
  }
7657
- function defaultDateRange$2(days = 28, now = /* @__PURE__ */ new Date()) {
8113
+ function defaultDateRange$1(days = 28, now = /* @__PURE__ */ new Date()) {
7658
8114
  return finalGscDateRange(days, now);
7659
8115
  }
7660
8116
  //#endregion
@@ -8746,7 +9202,7 @@ function reportCaveats$1(input) {
8746
9202
  async function pageOpportunitiesReport(input, dependencies = defaultDependencies$12) {
8747
9203
  const url = assertUrlMatchesGscProperty(input.site, input.url);
8748
9204
  const days = reportDays$1(input.days);
8749
- const range = defaultDateRange$2(days);
9205
+ const range = defaultDateRange$1(days);
8750
9206
  const targetResult = await dependencies.searchAnalytics(input.site, {
8751
9207
  ...range,
8752
9208
  dimensions: ["query", "page"],
@@ -12725,37 +13181,10 @@ function collectCrawlAiSignals(input) {
12725
13181
  ]);
12726
13182
  }
12727
13183
  //#endregion
12728
- //#region packages/core/src/analyze/site-diagnostics/quick-wins-report-input.ts
12729
- function integerOption$1(input) {
12730
- if (input.value === void 0) return input.fallback;
12731
- 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}.`);
12732
- return input.value;
12733
- }
12734
- function defaultDateRange$1(days, now) {
12735
- return finalGscDateRange(days, now);
12736
- }
12737
- function explicitDateRange(input, maximumDays) {
12738
- if (!input.startDate && !input.endDate) return void 0;
12739
- if (!input.startDate || !input.endDate) throw new SeoError("INVALID_INPUT", "Start date and end date must be provided together.");
12740
- const pattern = /^\d{4}-\d{2}-\d{2}$/;
12741
- const start = /* @__PURE__ */ new Date(`${input.startDate}T00:00:00.000Z`);
12742
- const end = /* @__PURE__ */ new Date(`${input.endDate}T00:00:00.000Z`);
12743
- if (!pattern.test(input.startDate) || !pattern.test(input.endDate) || Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start.toISOString().slice(0, 10) !== input.startDate || end.toISOString().slice(0, 10) !== input.endDate || start > end) throw new SeoError("INVALID_INPUT", "Start date and end date must be valid YYYY-MM-DD values in ascending order.");
12744
- const days = Math.floor((end.getTime() - start.getTime()) / 864e5) + 1;
12745
- if (days > maximumDays) throw new SeoError("INVALID_INPUT", `Date range must not exceed ${maximumDays} days.`);
12746
- return {
12747
- range: {
12748
- startDate: input.startDate,
12749
- endDate: input.endDate
12750
- },
12751
- days
12752
- };
12753
- }
12754
- //#endregion
12755
13184
  //#region packages/core/src/analyze/crawler/site-crawl-providers.ts
12756
13185
  function crawlMetricsWindow(now) {
12757
13186
  return {
12758
- ...defaultDateRange$1(28, now),
13187
+ ...defaultDateRange$2(28, now),
12759
13188
  days: 28
12760
13189
  };
12761
13190
  }
@@ -14089,6 +14518,9 @@ async function storedSiteKeys() {
14089
14518
  source: credential.source
14090
14519
  };
14091
14520
  }
14521
+ async function readClickySiteKeys() {
14522
+ return (await storedSiteKeys()).siteKeys;
14523
+ }
14092
14524
  async function readClickySiteKey(siteIdValue, input = {}) {
14093
14525
  const siteId = normalizeClickySiteId(siteIdValue);
14094
14526
  const environmentKey = (input.env ?? process.env)[CLICKY_SITEKEY_ENV]?.trim();
@@ -16279,7 +16711,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
16279
16711
  label: "Days"
16280
16712
  });
16281
16713
  const now = dependencies.now();
16282
- const range = explicitRange?.range ?? defaultDateRange$1(days, now);
16714
+ const range = explicitRange?.range ?? defaultDateRange$2(days, now);
16283
16715
  const source = await dependencies.searchAnalytics(input.site, {
16284
16716
  ...range,
16285
16717
  dimensions: ["query", "page"],
@@ -16721,7 +17153,7 @@ function analyzeSegmentRows(input) {
16721
17153
  //#region packages/core/src/analyze/segment-impact-input.ts
16722
17154
  const DAY_MS = 864e5;
16723
17155
  const MAX_WINDOW_DAYS = 240;
16724
- function subtractDays$1(value, days) {
17156
+ function subtractDays(value, days) {
16725
17157
  const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
16726
17158
  date.setUTCDate(date.getUTCDate() - days);
16727
17159
  return date.toISOString().slice(0, 10);
@@ -16730,9 +17162,9 @@ function segmentRangeDays(range) {
16730
17162
  return Math.floor((Date.parse(`${range.endDate}T00:00:00.000Z`) - Date.parse(`${range.startDate}T00:00:00.000Z`)) / DAY_MS) + 1;
16731
17163
  }
16732
17164
  function segmentComparisonRange(after, compareDays) {
16733
- const endDate = subtractDays$1(after.startDate, 1);
17165
+ const endDate = subtractDays(after.startDate, 1);
16734
17166
  return {
16735
- startDate: subtractDays$1(endDate, compareDays - 1),
17167
+ startDate: subtractDays(endDate, compareDays - 1),
16736
17168
  endDate
16737
17169
  };
16738
17170
  }
@@ -16740,7 +17172,7 @@ function validateSegmentRanges(input) {
16740
17172
  const before = explicitDateRange(input.before, MAX_WINDOW_DAYS);
16741
17173
  const after = explicitDateRange(input.after, MAX_WINDOW_DAYS);
16742
17174
  if (!before || !after) throw new SeoError("INVALID_INPUT", "Both segment ranges are required.");
16743
- if (subtractDays$1(input.after.startDate, 1) !== input.before.endDate) throw new SeoError("INVALID_INPUT", "Segment comparison windows must be adjacent.");
17175
+ if (subtractDays(input.after.startDate, 1) !== input.before.endDate) throw new SeoError("INVALID_INPUT", "Segment comparison windows must be adjacent.");
16744
17176
  if (before.days !== after.days) throw new SeoError("INVALID_INPUT", "Segment comparison windows must have equal lengths.");
16745
17177
  return {
16746
17178
  beforeDays: before.days,
@@ -16755,7 +17187,7 @@ function resolveSegmentRanges(input) {
16755
17187
  maximum: MAX_WINDOW_DAYS,
16756
17188
  label: "days"
16757
17189
  });
16758
- const after = explicitDateRange(input, MAX_WINDOW_DAYS)?.range ?? defaultDateRange$1(days, input.now);
17190
+ const after = explicitDateRange(input, MAX_WINDOW_DAYS)?.range ?? defaultDateRange$2(days, input.now);
16759
17191
  const compareDays = integerOption$1({
16760
17192
  value: input.compareDays,
16761
17193
  fallback: segmentRangeDays(after),
@@ -16774,63 +17206,6 @@ function validateSegmentSite(site) {
16774
17206
  if (!site.trim()) throw new SeoError("INVALID_INPUT", "site must not be empty.");
16775
17207
  }
16776
17208
  //#endregion
16777
- //#region packages/core/src/analyze/site-diagnostics/decay-report-input.ts
16778
- function decayNumberOption(input) {
16779
- const value = input.value ?? input.fallback;
16780
- 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}.`);
16781
- return value;
16782
- }
16783
- function rangeDays$1(range) {
16784
- return Math.floor(((/* @__PURE__ */ new Date(`${range.endDate}T00:00:00.000Z`)).getTime() - (/* @__PURE__ */ new Date(`${range.startDate}T00:00:00.000Z`)).getTime()) / 864e5) + 1;
16785
- }
16786
- function dateString(date) {
16787
- return date.toISOString().slice(0, 10);
16788
- }
16789
- function subtractDays(value, days) {
16790
- const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
16791
- date.setUTCDate(date.getUTCDate() - days);
16792
- return dateString(date);
16793
- }
16794
- function priorYearDate(value) {
16795
- const source = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
16796
- const year = source.getUTCFullYear() - 1;
16797
- const month = source.getUTCMonth();
16798
- const day = source.getUTCDate();
16799
- const candidate = new Date(Date.UTC(year, month, day));
16800
- if (candidate.getUTCMonth() !== month) return dateString(new Date(Date.UTC(year, month + 1, 0)));
16801
- return dateString(candidate);
16802
- }
16803
- function subtractCalendarMonths(date, months) {
16804
- const day = date.getUTCDate();
16805
- const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() - months, 1));
16806
- const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
16807
- target.setUTCDate(Math.min(day, lastDay));
16808
- return dateString(target);
16809
- }
16810
- function validateDecayRanges(input) {
16811
- const latestFinalDate = defaultDateRange$1(1, input.now).endDate;
16812
- if (input.current.endDate > latestFinalDate) throw new SeoError("INVALID_INPUT", `Decay end date must be ${latestFinalDate} or earlier so both GSC windows use final data.`);
16813
- const pacificToday = /* @__PURE__ */ new Date(`${latestFinalDate}T00:00:00.000Z`);
16814
- pacificToday.setUTCDate(pacificToday.getUTCDate() + 4);
16815
- const retentionStart = subtractCalendarMonths(pacificToday, 16);
16816
- if (input.previous.startDate < retentionStart) throw new SeoError("INVALID_INPUT", `Comparison range starts before Search Console's rolling 16-month API history (${retentionStart}). Choose a shorter range or use previously stored data.`);
16817
- }
16818
- function decayComparisonRange(current, comparison = "previous-period") {
16819
- const days = rangeDays$1(current);
16820
- if (comparison === "year-over-year") {
16821
- const endDate = priorYearDate(current.endDate);
16822
- return {
16823
- startDate: subtractDays(endDate, days - 1),
16824
- endDate
16825
- };
16826
- }
16827
- const endDate = subtractDays(current.startDate, 1);
16828
- return {
16829
- startDate: subtractDays(endDate, days - 1),
16830
- endDate
16831
- };
16832
- }
16833
- //#endregion
16834
17209
  //#region packages/core/src/analyze/segment-impact-fallback.ts
16835
17210
  function unavailableSegmentImpactReport(input) {
16836
17211
  const days = segmentRangeDays(input.after);
@@ -18691,7 +19066,7 @@ async function cannibalReportWith(input, dependencies) {
18691
19066
  });
18692
19067
  const now = dependencies.now();
18693
19068
  const explicit = explicitDateRange(input, 548);
18694
- const range = explicit?.range ?? defaultDateRange$1(days, now);
19069
+ const range = explicit?.range ?? defaultDateRange$2(days, now);
18695
19070
  const rangeDays = explicit?.days ?? days;
18696
19071
  const requestBase = {
18697
19072
  ...range,
@@ -19149,7 +19524,7 @@ async function decayingReportWith(input, dependencies) {
19149
19524
  });
19150
19525
  const now = dependencies.now();
19151
19526
  const explicit = explicitDateRange(input, 548);
19152
- const currentRange = explicit?.range ?? defaultDateRange$1(days, now);
19527
+ const currentRange = explicit?.range ?? defaultDateRange$2(days, now);
19153
19528
  const comparison = input.comparison ?? "previous-period";
19154
19529
  const previousRange = decayComparisonRange(currentRange, comparison);
19155
19530
  validateDecayRanges({
@@ -19622,7 +19997,7 @@ async function quickWinsReportWith(input, dependencies) {
19622
19997
  label: "Verification limit"
19623
19998
  });
19624
19999
  const generatedAt = dependencies.now().toISOString();
19625
- const range = explicitRange?.range ?? defaultDateRange$1(days, new Date(generatedAt));
20000
+ const range = explicitRange?.range ?? defaultDateRange$2(days, new Date(generatedAt));
19626
20001
  const source = await dependencies.searchAnalytics(input.site, {
19627
20002
  ...range,
19628
20003
  dimensions: ["query", "page"],
@@ -20046,7 +20421,7 @@ function reportDays(value) {
20046
20421
  if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS}.`);
20047
20422
  return value;
20048
20423
  }
20049
- function dateRange$1(days, now) {
20424
+ function dateRange(days, now) {
20050
20425
  return finalGscDateRange(days, now);
20051
20426
  }
20052
20427
  function verifiedRecommendation(coverage) {
@@ -20086,7 +20461,7 @@ async function strikingDistanceWith(input, dependencies) {
20086
20461
  const explicitRange = explicitDateRange(input, MAX_DAYS);
20087
20462
  const days = explicitRange?.days ?? reportDays(input.days);
20088
20463
  const generatedAt = dependencies.now().toISOString();
20089
- const range = explicitRange?.range ?? dateRange$1(days, new Date(generatedAt));
20464
+ const range = explicitRange?.range ?? dateRange(days, new Date(generatedAt));
20090
20465
  const result = await dependencies.searchAnalytics(input.site, {
20091
20466
  ...range,
20092
20467
  dimensions: ["query", "page"],
@@ -20198,44 +20573,6 @@ function strikingDistance(input, dependencies = defaultDependencies$5) {
20198
20573
  return strikingDistanceWith(input, dependencies);
20199
20574
  }
20200
20575
  //#endregion
20201
- //#region packages/core/src/updates/search-updates.ts
20202
- function classifyUpdate(name, product) {
20203
- const lower = `${name} ${product}`.toLowerCase();
20204
- if (lower.includes("core")) return "core";
20205
- if (lower.includes("spam")) return "spam";
20206
- if (lower.includes("discover")) return "discover";
20207
- if (lower.includes("ranking")) return "ranking";
20208
- if (lower.includes("index")) return "indexing";
20209
- if (lower.includes("serving")) return "serving";
20210
- return "other";
20211
- }
20212
- function searchUpdatesFromIncidents(incidents, input = {}) {
20213
- const product = input.product?.toLowerCase();
20214
- return incidents.filter((incident) => !product || incident.service_name.toLowerCase() === product).map((incident) => {
20215
- const status = incident.end ? "complete" : incident.status_impact === "SERVICE_INFORMATION" ? "rolling-out" : "incident";
20216
- return {
20217
- id: incident.id,
20218
- name: incident.external_desc,
20219
- type: classifyUpdate(incident.external_desc, incident.service_name),
20220
- product: incident.service_name,
20221
- start: incident.begin,
20222
- end: incident.end,
20223
- status,
20224
- sourceUrl: `https://status.search.google.com/${incident.uri}`
20225
- };
20226
- }).slice(0, input.limit ?? 25);
20227
- }
20228
- function findOverlappingSearchUpdates(input) {
20229
- const paddingMs = (input.paddingDays ?? 3) * 864e5;
20230
- const windowStart = new Date(input.startDate).getTime() - paddingMs;
20231
- const windowEnd = new Date(input.endDate).getTime() + paddingMs;
20232
- return input.updates.filter((update) => {
20233
- const updateStart = new Date(update.start).getTime();
20234
- const updateEnd = update.end ? new Date(update.end).getTime() : Date.now();
20235
- return updateStart <= windowEnd && updateEnd >= windowStart;
20236
- });
20237
- }
20238
- //#endregion
20239
20576
  //#region packages/core/src/updates/search-status.ts
20240
20577
  const INCIDENTS_URL = "https://status.search.google.com/incidents.json";
20241
20578
  async function fetchSearchStatusIncidents() {
@@ -20612,7 +20949,7 @@ async function trafficAnomalyWith(input, dependencies) {
20612
20949
  const range = input.startDate && input.endDate ? {
20613
20950
  startDate: input.startDate,
20614
20951
  endDate: input.endDate
20615
- } : defaultDateRange$2(days, dependencies.now());
20952
+ } : defaultDateRange$1(days, dependencies.now());
20616
20953
  const result = await dependencies.searchAnalytics(input.site, {
20617
20954
  ...range,
20618
20955
  dimensions: ["date"],
@@ -20704,7 +21041,7 @@ function fallbackRange(input) {
20704
21041
  return input.startDate && input.endDate ? {
20705
21042
  startDate: input.startDate,
20706
21043
  endDate: input.endDate
20707
- } : defaultDateRange$2(input.days ?? 28);
21044
+ } : defaultDateRange$1(input.days ?? 28);
20708
21045
  }
20709
21046
  function emptyAnomaly(input) {
20710
21047
  return {
@@ -20909,7 +21246,7 @@ function emptyStriking(input) {
20909
21246
  return {
20910
21247
  site: input.site,
20911
21248
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20912
- range: defaultDateRange$2(input.days ?? 28),
21249
+ range: defaultDateRange$1(input.days ?? 28),
20913
21250
  rangeDays: input.days ?? 28,
20914
21251
  source: {
20915
21252
  provider: "google-search-console",
@@ -22826,7 +23163,7 @@ function rowMetrics(row) {
22826
23163
  }
22827
23164
  async function linkRecover(input) {
22828
23165
  const days = input.days ?? 90;
22829
- const range = defaultDateRange$2(days);
23166
+ const range = defaultDateRange$1(days);
22830
23167
  const result = await querySearchAnalytics(input.site, {
22831
23168
  ...range,
22832
23169
  dimensions: ["page"],
@@ -23990,7 +24327,7 @@ async function queryClusterReport(input) {
23990
24327
  maximum: 548,
23991
24328
  label: "days"
23992
24329
  });
23993
- const range = defaultDateRange$2(days);
24330
+ const range = defaultDateRange$1(days);
23994
24331
  const filters = input.scope ? [{
23995
24332
  groupType: "and",
23996
24333
  filters: [{
@@ -25309,7 +25646,7 @@ async function refreshPrioritiesWorkflow(input) {
25309
25646
  verifyContent: input.verifyContent ?? true,
25310
25647
  verifyLimit: input.verifyContent === false ? void 0 : input.verifyLimit ?? Math.min(input.limit ?? 5, 5)
25311
25648
  });
25312
- const range = defaultDateRange$2(input.days ?? 28);
25649
+ const range = defaultDateRange$1(input.days ?? 28);
25313
25650
  const analytics = await fetchLandingPageValues({
25314
25651
  connection: input.analyticsConnection,
25315
25652
  propertyId: input.googleAnalyticsPropertyId,
@@ -25840,67 +26177,6 @@ async function detectBrandTerms(input) {
25840
26177
  candidates: ranked
25841
26178
  };
25842
26179
  }
25843
- //#endregion
25844
- //#region packages/core/src/bing/api.ts
25845
- const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
25846
- const MAX_BYTES$1 = 8e6;
25847
- function bingCall(auth, options) {
25848
- return async (method, params = {}) => {
25849
- const url = new URL(`${BASE}/${method}`);
25850
- for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
25851
- const headers = { accept: "application/json" };
25852
- if ("apiKey" in auth) url.searchParams.set("apikey", auth.apiKey);
25853
- else headers.authorization = `Bearer ${await auth.accessToken()}`;
25854
- let response;
25855
- try {
25856
- response = await fetch(url, {
25857
- headers,
25858
- signal: AbortSignal.timeout(3e4)
25859
- });
25860
- } catch (error) {
25861
- throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster did not respond: ${error instanceof Error ? error.message : String(error)}`);
25862
- }
25863
- if (!response.ok) {
25864
- const detail = (await response.text().catch(() => "")).slice(0, 200);
25865
- if (response.status === 401 || /invalidapikey/i.test(detail)) throw new SeoError("AUTH_REQUIRED", `Bing Webmaster rejected the sign-in. ${options.signIn}`);
25866
- if (response.status === 403 || /notauthorized/i.test(detail)) throw new SeoError("ACCESS_DENIED", "Bing Webmaster denied access to this site. It must be verified in the connected Bing account.");
25867
- if (/ThrottleIP/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is blocking requests from this network (ThrottleIP). Run the same report with the CLI on your own machine, where Bing answers.");
25868
- if (response.status === 429 || /throttle/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is limiting requests. Try again in a few minutes.");
25869
- throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
25870
- }
25871
- return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$1 })).d;
25872
- };
25873
- }
25874
- //#endregion
25875
- //#region packages/core/src/commands/types.ts
25876
- var CommandError = class extends Error {
25877
- code;
25878
- constructor(code, message) {
25879
- super(message);
25880
- this.code = code;
25881
- }
25882
- };
25883
- const ADVICE_KEYS = /^(recommendations?|actions?|advice|suggested[A-Z]\w*|nextSteps?)$/;
25884
- function factsOnly(value) {
25885
- if (Array.isArray(value)) return value.map(factsOnly);
25886
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !ADVICE_KEYS.test(key)).map(([key, child]) => [key, factsOnly(child)]));
25887
- return value;
25888
- }
25889
- function matchProperty(names, site) {
25890
- if (names.includes(site)) return site;
25891
- let host;
25892
- let origin;
25893
- try {
25894
- const url = new URL(site.includes("://") ? site : `https://${site.replace(/^sc-domain:/, "")}`);
25895
- host = url.hostname.replace(/^www\./, "");
25896
- origin = url.origin;
25897
- } catch {
25898
- return null;
25899
- }
25900
- const domain = names.filter((name) => name.startsWith("sc-domain:")).map((name) => name.slice(10)).filter((name) => host === name || host.endsWith(`.${name}`)).sort((a, b) => b.length - a.length)[0];
25901
- if (domain) return `sc-domain:${domain}`;
25902
- return names.find((name) => name === `${origin}/` || name === `${origin.replace("://", "://www.")}/` || name === `${origin.replace("://www.", "://")}/`) ?? null;
25903
- }
25904
26180
  const DIMENSIONS = [
25905
26181
  "query",
25906
26182
  "page",
@@ -25910,6 +26186,9 @@ const DIMENSIONS = [
25910
26186
  "searchAppearance"
25911
26187
  ];
25912
26188
  //#endregion
26189
+ //#region packages/core/src/commands/clicky.ts
26190
+ const CLICKY_KEY_PREFIX = "x-clicky-";
26191
+ //#endregion
25913
26192
  //#region packages/core/src/commands/page-facts.ts
25914
26193
  const MAX_PAGES = 20;
25915
26194
  const TIMEOUT_MS = 6e3;
@@ -26038,12 +26317,6 @@ const SECTIONS = {
26038
26317
  };
26039
26318
  const SECTION_IDS = Object.keys(SECTIONS);
26040
26319
  //#endregion
26041
- //#region packages/core/src/commands/research-providers.ts
26042
- const KEY_HEADERS = {
26043
- dataforseo: "X-DataForSEO-Login and X-DataForSEO-Password",
26044
- ahrefs: "X-Ahrefs-Key"
26045
- };
26046
- //#endregion
26047
26320
  //#region packages/core/src/commands/research.ts
26048
26321
  const RESEARCH_ACTIONS = [
26049
26322
  "keyword_metrics",
@@ -26067,7 +26340,7 @@ function indexStatusFrom(result) {
26067
26340
  user_canonical: text(status.userCanonical)
26068
26341
  };
26069
26342
  }
26070
- const LOST_PAGES_RULE = `Pages that had at least 3 clicks in the previous 28 days and kept ${.2 * 100}% or less of them in the last 28 days, with a live check of the top 20 and Google’s index status for the top 10.`;
26343
+ const LOST_PAGES_RULE = `Pages that had at least 3 clicks in the previous 28 days and kept ${.2 * 100}% or less of them in the last 28 days, with a live check of the top 20 and Google’s index status for the top 10 and the home page.`;
26071
26344
  //#endregion
26072
26345
  //#region packages/core/src/commands/tools.ts
26073
26346
  const site = z.string().min(1).describe("Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.");
@@ -26086,7 +26359,8 @@ const detail = {
26086
26359
  const DATA_SOURCES = [
26087
26360
  "search_console",
26088
26361
  "google_analytics",
26089
- "bing"
26362
+ "bing",
26363
+ "clicky"
26090
26364
  ];
26091
26365
  z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}`, z.object({
26092
26366
  site,
@@ -26104,7 +26378,7 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
26104
26378
  }), `${LOST_PAGES_RULE}`, z.object({
26105
26379
  site,
26106
26380
  ...dates
26107
- }), `${Object.entries(KEY_HEADERS).map(([provider, header]) => `${provider} ${header}`).join("; ")}`, z.object({
26381
+ }), z.object({
26108
26382
  provider: z.enum(RESEARCH_PROVIDERS),
26109
26383
  action: z.enum(RESEARCH_ACTIONS),
26110
26384
  keywords: z.array(z.string().min(1)).max(100).default([]),
@@ -26113,7 +26387,7 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
26113
26387
  ...detail
26114
26388
  }), z.object({
26115
26389
  source: z.enum(DATA_SOURCES).default("search_console"),
26116
- site: z.string().min(1).describe("search_console and bing: the property or site. google_analytics: numeric property id from seo_sites."),
26390
+ site: z.string().min(1).describe("search_console and bing: the property or site. google_analytics: numeric property id from seo_sites. clicky: the site or its Clicky site ID."),
26117
26391
  dimensions: z.array(z.enum(DIMENSIONS)).max(4).default(["query"]).describe("search_console; bing uses the first, query or page."),
26118
26392
  page: z.string().min(1).optional().describe("search_console only: URL text, such as /blog/."),
26119
26393
  query: z.string().min(1).optional().describe("search_console only: query text."),
@@ -26332,15 +26606,34 @@ function nodeCommandDeps(options) {
26332
26606
  bingConnect: LOCAL_BING_CONNECT
26333
26607
  };
26334
26608
  }
26335
- const LOCAL_BING_CONNECT = "Run `seo providers bing connect` with an API key from Bing Webmaster Tools (Settings, API access).";
26609
+ const LOCAL_BING_CONNECT = "Connect Bing on your seoskill.dev account and run `seo login`, or run `seo providers bing connect` with an API key from Bing Webmaster Tools (Settings, API access).";
26336
26610
  let bing;
26337
26611
  function localBing() {
26338
- bing ??= readBingApiKey().catch(() => void 0).then((key) => key ? bingCall({ apiKey: key.apiKey }, { signIn: LOCAL_BING_CONNECT }) : null);
26612
+ bing ??= readBingApiKey().catch(() => void 0).then((key) => key ? bingCall({ apiKey: key.apiKey }, { signIn: LOCAL_BING_CONNECT }) : cloudLinked() ? cloudBing() : null);
26339
26613
  return bing;
26340
26614
  }
26615
+ async function localAccounts() {
26616
+ if (!cloudLinked()) return [{
26617
+ email: null,
26618
+ google: await localGoogleFetch()
26619
+ }];
26620
+ return (await cloudGoogleEmails()).map((email) => ({
26621
+ email,
26622
+ google: bearerFetch(() => cloudGoogleToken(email))
26623
+ }));
26624
+ }
26341
26625
  async function localResearchKeys() {
26342
- const [dataForSeo, ahrefs] = await Promise.all([readDataForSeoCredentials().catch(() => void 0), readAhrefsApiKey().catch(() => void 0)]);
26343
- const keys = new Headers();
26626
+ const [dataForSeo, ahrefs, clicky, saved] = await Promise.all([
26627
+ readDataForSeoCredentials().catch(() => void 0),
26628
+ readAhrefsApiKey().catch(() => void 0),
26629
+ readClickySiteKeys().catch(() => ({})),
26630
+ cloudLinked() ? cloudKeys().catch(() => ({})) : {}
26631
+ ]);
26632
+ const keys = new Headers(saved);
26633
+ for (const [siteId, sitekey] of Object.entries(clicky)) {
26634
+ const name = `${CLICKY_KEY_PREFIX}${siteId}`;
26635
+ if (!keys.has(name)) keys.set(name, sitekey);
26636
+ }
26344
26637
  if (dataForSeo) {
26345
26638
  keys.set("x-dataforseo-login", dataForSeo.login);
26346
26639
  keys.set("x-dataforseo-password", dataForSeo.password);
@@ -26348,6 +26641,11 @@ async function localResearchKeys() {
26348
26641
  if (ahrefs?.apiKey) keys.set("x-ahrefs-key", ahrefs.apiKey);
26349
26642
  return keys;
26350
26643
  }
26644
+ async function localRefreshClicky() {
26645
+ if (!cloudLinked()) return null;
26646
+ await cloudCredentials({ refreshClicky: true });
26647
+ return localResearchKeys();
26648
+ }
26351
26649
  async function resolveLocalSite(site, options = {}) {
26352
26650
  const found = matchProperty((await listSites(options.refresh, options.account)).map((entry) => entry.siteUrl), site);
26353
26651
  if (!found) throw new CommandError("NO_ACCESS", `This Google login has no Search Console property for ${site}. Run \`seo sites\` to list the properties it can read.`);
@@ -28653,6 +28951,6 @@ function isoDate(daysAgo = 0) {
28653
28951
  return date.toISOString().slice(0, 10);
28654
28952
  }
28655
28953
  //#endregion
28656
- 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, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, 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_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, 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, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localBing, localCredentialsProvider, localError, localGoogleFetch, localResearchKeys, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, noteCacheWrite, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, 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, 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, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeTokens, xRobotsDirectives };
28954
+ 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_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, 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_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, 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, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, 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, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeTokens, xRobotsDirectives };
28657
28955
 
28658
28956
  //# sourceMappingURL=index.js.map