seo 0.4.1 → 0.5.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-CeYsRRpB.js";
2
+ import { n as load, t as version } from "./package-CsRkLZGn.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$1 = 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$1 })).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() {
2092
+ const current = credentials ? await credentials.catch(() => null) : null;
2093
+ if (current && !expiresSoon(current)) 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", { 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"),
@@ -1747,78 +2251,39 @@ const analyticsProviderSchema = z.enum(["google", "clicky"]);
1747
2251
  const analyticsExtensionIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
1748
2252
  const analyticsExtensionAccountSchema = z.record(z.string().min(1).max(64).regex(/^[A-Za-z][A-Za-z0-9-]*$/u).refine((value) => ![
1749
2253
  "constructor",
1750
- "prototype",
1751
- "__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
- }
2254
+ "prototype",
2255
+ "__proto__"
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
  }
@@ -16279,7 +16708,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
16279
16708
  label: "Days"
16280
16709
  });
16281
16710
  const now = dependencies.now();
16282
- const range = explicitRange?.range ?? defaultDateRange$1(days, now);
16711
+ const range = explicitRange?.range ?? defaultDateRange$2(days, now);
16283
16712
  const source = await dependencies.searchAnalytics(input.site, {
16284
16713
  ...range,
16285
16714
  dimensions: ["query", "page"],
@@ -16721,7 +17150,7 @@ function analyzeSegmentRows(input) {
16721
17150
  //#region packages/core/src/analyze/segment-impact-input.ts
16722
17151
  const DAY_MS = 864e5;
16723
17152
  const MAX_WINDOW_DAYS = 240;
16724
- function subtractDays$1(value, days) {
17153
+ function subtractDays(value, days) {
16725
17154
  const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
16726
17155
  date.setUTCDate(date.getUTCDate() - days);
16727
17156
  return date.toISOString().slice(0, 10);
@@ -16730,9 +17159,9 @@ function segmentRangeDays(range) {
16730
17159
  return Math.floor((Date.parse(`${range.endDate}T00:00:00.000Z`) - Date.parse(`${range.startDate}T00:00:00.000Z`)) / DAY_MS) + 1;
16731
17160
  }
16732
17161
  function segmentComparisonRange(after, compareDays) {
16733
- const endDate = subtractDays$1(after.startDate, 1);
17162
+ const endDate = subtractDays(after.startDate, 1);
16734
17163
  return {
16735
- startDate: subtractDays$1(endDate, compareDays - 1),
17164
+ startDate: subtractDays(endDate, compareDays - 1),
16736
17165
  endDate
16737
17166
  };
16738
17167
  }
@@ -16740,7 +17169,7 @@ function validateSegmentRanges(input) {
16740
17169
  const before = explicitDateRange(input.before, MAX_WINDOW_DAYS);
16741
17170
  const after = explicitDateRange(input.after, MAX_WINDOW_DAYS);
16742
17171
  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.");
17172
+ if (subtractDays(input.after.startDate, 1) !== input.before.endDate) throw new SeoError("INVALID_INPUT", "Segment comparison windows must be adjacent.");
16744
17173
  if (before.days !== after.days) throw new SeoError("INVALID_INPUT", "Segment comparison windows must have equal lengths.");
16745
17174
  return {
16746
17175
  beforeDays: before.days,
@@ -16755,7 +17184,7 @@ function resolveSegmentRanges(input) {
16755
17184
  maximum: MAX_WINDOW_DAYS,
16756
17185
  label: "days"
16757
17186
  });
16758
- const after = explicitDateRange(input, MAX_WINDOW_DAYS)?.range ?? defaultDateRange$1(days, input.now);
17187
+ const after = explicitDateRange(input, MAX_WINDOW_DAYS)?.range ?? defaultDateRange$2(days, input.now);
16759
17188
  const compareDays = integerOption$1({
16760
17189
  value: input.compareDays,
16761
17190
  fallback: segmentRangeDays(after),
@@ -16774,63 +17203,6 @@ function validateSegmentSite(site) {
16774
17203
  if (!site.trim()) throw new SeoError("INVALID_INPUT", "site must not be empty.");
16775
17204
  }
16776
17205
  //#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
17206
  //#region packages/core/src/analyze/segment-impact-fallback.ts
16835
17207
  function unavailableSegmentImpactReport(input) {
16836
17208
  const days = segmentRangeDays(input.after);
@@ -18691,7 +19063,7 @@ async function cannibalReportWith(input, dependencies) {
18691
19063
  });
18692
19064
  const now = dependencies.now();
18693
19065
  const explicit = explicitDateRange(input, 548);
18694
- const range = explicit?.range ?? defaultDateRange$1(days, now);
19066
+ const range = explicit?.range ?? defaultDateRange$2(days, now);
18695
19067
  const rangeDays = explicit?.days ?? days;
18696
19068
  const requestBase = {
18697
19069
  ...range,
@@ -19149,7 +19521,7 @@ async function decayingReportWith(input, dependencies) {
19149
19521
  });
19150
19522
  const now = dependencies.now();
19151
19523
  const explicit = explicitDateRange(input, 548);
19152
- const currentRange = explicit?.range ?? defaultDateRange$1(days, now);
19524
+ const currentRange = explicit?.range ?? defaultDateRange$2(days, now);
19153
19525
  const comparison = input.comparison ?? "previous-period";
19154
19526
  const previousRange = decayComparisonRange(currentRange, comparison);
19155
19527
  validateDecayRanges({
@@ -19622,7 +19994,7 @@ async function quickWinsReportWith(input, dependencies) {
19622
19994
  label: "Verification limit"
19623
19995
  });
19624
19996
  const generatedAt = dependencies.now().toISOString();
19625
- const range = explicitRange?.range ?? defaultDateRange$1(days, new Date(generatedAt));
19997
+ const range = explicitRange?.range ?? defaultDateRange$2(days, new Date(generatedAt));
19626
19998
  const source = await dependencies.searchAnalytics(input.site, {
19627
19999
  ...range,
19628
20000
  dimensions: ["query", "page"],
@@ -20046,7 +20418,7 @@ function reportDays(value) {
20046
20418
  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
20419
  return value;
20048
20420
  }
20049
- function dateRange$1(days, now) {
20421
+ function dateRange(days, now) {
20050
20422
  return finalGscDateRange(days, now);
20051
20423
  }
20052
20424
  function verifiedRecommendation(coverage) {
@@ -20086,7 +20458,7 @@ async function strikingDistanceWith(input, dependencies) {
20086
20458
  const explicitRange = explicitDateRange(input, MAX_DAYS);
20087
20459
  const days = explicitRange?.days ?? reportDays(input.days);
20088
20460
  const generatedAt = dependencies.now().toISOString();
20089
- const range = explicitRange?.range ?? dateRange$1(days, new Date(generatedAt));
20461
+ const range = explicitRange?.range ?? dateRange(days, new Date(generatedAt));
20090
20462
  const result = await dependencies.searchAnalytics(input.site, {
20091
20463
  ...range,
20092
20464
  dimensions: ["query", "page"],
@@ -20198,44 +20570,6 @@ function strikingDistance(input, dependencies = defaultDependencies$5) {
20198
20570
  return strikingDistanceWith(input, dependencies);
20199
20571
  }
20200
20572
  //#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
20573
  //#region packages/core/src/updates/search-status.ts
20240
20574
  const INCIDENTS_URL = "https://status.search.google.com/incidents.json";
20241
20575
  async function fetchSearchStatusIncidents() {
@@ -20612,7 +20946,7 @@ async function trafficAnomalyWith(input, dependencies) {
20612
20946
  const range = input.startDate && input.endDate ? {
20613
20947
  startDate: input.startDate,
20614
20948
  endDate: input.endDate
20615
- } : defaultDateRange$2(days, dependencies.now());
20949
+ } : defaultDateRange$1(days, dependencies.now());
20616
20950
  const result = await dependencies.searchAnalytics(input.site, {
20617
20951
  ...range,
20618
20952
  dimensions: ["date"],
@@ -20704,7 +21038,7 @@ function fallbackRange(input) {
20704
21038
  return input.startDate && input.endDate ? {
20705
21039
  startDate: input.startDate,
20706
21040
  endDate: input.endDate
20707
- } : defaultDateRange$2(input.days ?? 28);
21041
+ } : defaultDateRange$1(input.days ?? 28);
20708
21042
  }
20709
21043
  function emptyAnomaly(input) {
20710
21044
  return {
@@ -20909,7 +21243,7 @@ function emptyStriking(input) {
20909
21243
  return {
20910
21244
  site: input.site,
20911
21245
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20912
- range: defaultDateRange$2(input.days ?? 28),
21246
+ range: defaultDateRange$1(input.days ?? 28),
20913
21247
  rangeDays: input.days ?? 28,
20914
21248
  source: {
20915
21249
  provider: "google-search-console",
@@ -22826,7 +23160,7 @@ function rowMetrics(row) {
22826
23160
  }
22827
23161
  async function linkRecover(input) {
22828
23162
  const days = input.days ?? 90;
22829
- const range = defaultDateRange$2(days);
23163
+ const range = defaultDateRange$1(days);
22830
23164
  const result = await querySearchAnalytics(input.site, {
22831
23165
  ...range,
22832
23166
  dimensions: ["page"],
@@ -23990,7 +24324,7 @@ async function queryClusterReport(input) {
23990
24324
  maximum: 548,
23991
24325
  label: "days"
23992
24326
  });
23993
- const range = defaultDateRange$2(days);
24327
+ const range = defaultDateRange$1(days);
23994
24328
  const filters = input.scope ? [{
23995
24329
  groupType: "and",
23996
24330
  filters: [{
@@ -25309,7 +25643,7 @@ async function refreshPrioritiesWorkflow(input) {
25309
25643
  verifyContent: input.verifyContent ?? true,
25310
25644
  verifyLimit: input.verifyContent === false ? void 0 : input.verifyLimit ?? Math.min(input.limit ?? 5, 5)
25311
25645
  });
25312
- const range = defaultDateRange$2(input.days ?? 28);
25646
+ const range = defaultDateRange$1(input.days ?? 28);
25313
25647
  const analytics = await fetchLandingPageValues({
25314
25648
  connection: input.analyticsConnection,
25315
25649
  propertyId: input.googleAnalyticsPropertyId,
@@ -25840,67 +26174,6 @@ async function detectBrandTerms(input) {
25840
26174
  candidates: ranked
25841
26175
  };
25842
26176
  }
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
26177
  const DIMENSIONS = [
25905
26178
  "query",
25906
26179
  "page",
@@ -26038,12 +26311,6 @@ const SECTIONS = {
26038
26311
  };
26039
26312
  const SECTION_IDS = Object.keys(SECTIONS);
26040
26313
  //#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
26314
  //#region packages/core/src/commands/research.ts
26048
26315
  const RESEARCH_ACTIONS = [
26049
26316
  "keyword_metrics",
@@ -26104,7 +26371,7 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
26104
26371
  }), `${LOST_PAGES_RULE}`, z.object({
26105
26372
  site,
26106
26373
  ...dates
26107
- }), `${Object.entries(KEY_HEADERS).map(([provider, header]) => `${provider} ${header}`).join("; ")}`, z.object({
26374
+ }), z.object({
26108
26375
  provider: z.enum(RESEARCH_PROVIDERS),
26109
26376
  action: z.enum(RESEARCH_ACTIONS),
26110
26377
  keywords: z.array(z.string().min(1)).max(100).default([]),
@@ -26332,15 +26599,29 @@ function nodeCommandDeps(options) {
26332
26599
  bingConnect: LOCAL_BING_CONNECT
26333
26600
  };
26334
26601
  }
26335
- const LOCAL_BING_CONNECT = "Run `seo providers bing connect` with an API key from Bing Webmaster Tools (Settings, API access).";
26602
+ 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
26603
  let bing;
26337
26604
  function localBing() {
26338
- bing ??= readBingApiKey().catch(() => void 0).then((key) => key ? bingCall({ apiKey: key.apiKey }, { signIn: LOCAL_BING_CONNECT }) : null);
26605
+ bing ??= readBingApiKey().catch(() => void 0).then((key) => key ? bingCall({ apiKey: key.apiKey }, { signIn: LOCAL_BING_CONNECT }) : cloudLinked() ? cloudBing() : null);
26339
26606
  return bing;
26340
26607
  }
26608
+ async function localAccounts() {
26609
+ if (!cloudLinked()) return [{
26610
+ email: null,
26611
+ google: await localGoogleFetch()
26612
+ }];
26613
+ return (await cloudGoogleEmails()).map((email) => ({
26614
+ email,
26615
+ google: bearerFetch(() => cloudGoogleToken(email))
26616
+ }));
26617
+ }
26341
26618
  async function localResearchKeys() {
26342
- const [dataForSeo, ahrefs] = await Promise.all([readDataForSeoCredentials().catch(() => void 0), readAhrefsApiKey().catch(() => void 0)]);
26343
- const keys = new Headers();
26619
+ const [dataForSeo, ahrefs, saved] = await Promise.all([
26620
+ readDataForSeoCredentials().catch(() => void 0),
26621
+ readAhrefsApiKey().catch(() => void 0),
26622
+ cloudLinked() ? cloudKeys().catch(() => ({})) : {}
26623
+ ]);
26624
+ const keys = new Headers(saved);
26344
26625
  if (dataForSeo) {
26345
26626
  keys.set("x-dataforseo-login", dataForSeo.login);
26346
26627
  keys.set("x-dataforseo-password", dataForSeo.password);
@@ -28653,6 +28934,6 @@ function isoDate(daysAgo = 0) {
28653
28934
  return date.toISOString().slice(0, 10);
28654
28935
  }
28655
28936
  //#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 };
28937
+ 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, 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, 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
28938
 
28658
28939
  //# sourceMappingURL=index.js.map