seo 0.4.0 → 0.4.1
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/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/{dist-VKoRyTMH.js → dist-DjclF3Qk.js} +31 -31
- package/dist/{dist-VKoRyTMH.js.map → dist-DjclF3Qk.js.map} +1 -1
- package/dist/{dist-Cv-Z5qD5.js → dist-FO7n-64s.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +15 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +93 -23
- package/dist/mcp.js.map +1 -1
- package/dist/{package-BfqlCbxN.js → package-CeYsRRpB.js} +2 -2
- package/dist/{package-BfqlCbxN.js.map → package-CeYsRRpB.js.map} +1 -1
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { i as __toESM, n as load, r as __commonJSMin, t as version } from "./package-
|
|
2
|
+
import { i as __toESM, n as load, r as __commonJSMin, t as version } from "./package-CeYsRRpB.js";
|
|
3
3
|
import { createHash, randomUUID, sign } from "node:crypto";
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import { chmodSync, createReadStream, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -19914,7 +19914,7 @@ const TOPIC_WORDS = /* @__PURE__ */ new Set([
|
|
|
19914
19914
|
"free",
|
|
19915
19915
|
"guide",
|
|
19916
19916
|
"how",
|
|
19917
|
-
"
|
|
19917
|
+
"example",
|
|
19918
19918
|
"install",
|
|
19919
19919
|
"integration",
|
|
19920
19920
|
"local",
|
|
@@ -30733,12 +30733,13 @@ async function listSearchUpdates(input = {}) {
|
|
|
30733
30733
|
/** The step-change heuristic and its thresholds, returned with the report. */
|
|
30734
30734
|
const STEP_CHANGE_RULES = {
|
|
30735
30735
|
method: "best-split",
|
|
30736
|
-
description: "Heuristic. The change date is the split of the daily series where the mean before and the mean after differ most, weighted by segment length. It counts as a step change only when it is sudden and sustained. Sudden: the median of up to localDays observed days after the date differs from the median of up to localDays days before it by at least minLocalShiftInStdDevs pooled within-segment standard deviations, so one unusual day is not a step and a slow trend is not a step. Sustained: the mean of every day from the date differs from the mean before it by a z-score of at least minZScore, stricter than the recent-window test because the date is chosen from the data. Otherwise the last recentDays are compared with the days before them.",
|
|
30736
|
+
description: "Heuristic. The change date is the split of the daily series where the mean before and the mean after differ most, weighted by segment length. It counts as a step change only when it is sudden and sustained. Sudden: the median of up to localDays observed days after the date differs from the median of up to localDays days before it by at least minLocalShiftInStdDevs pooled within-segment standard deviations, so one unusual day is not a step and a slow trend is not a step. Sustained: the mean of every day from the date differs from the mean before it by a z-score of at least minZScore, stricter than the recent-window test because the date is chosen from the data. These tests use daily values capped at the capDays and 1 - capDays quantiles, so one spike day cannot hide a lasting step. Otherwise the last recentDays are compared with the days before them.",
|
|
30737
30737
|
minDaysBefore: 7,
|
|
30738
30738
|
minDaysAfter: 3,
|
|
30739
30739
|
localDays: 7,
|
|
30740
30740
|
minLocalShiftInStdDevs: 2,
|
|
30741
|
-
minZScore: 3
|
|
30741
|
+
minZScore: 3,
|
|
30742
|
+
capDays: .05
|
|
30742
30743
|
};
|
|
30743
30744
|
/**
|
|
30744
30745
|
* The dates to check against Google updates (before padding): the change
|
|
@@ -30914,10 +30915,18 @@ function sumOfSquaredDeviations(values) {
|
|
|
30914
30915
|
const average = mean(values);
|
|
30915
30916
|
return values.reduce((sum, value) => sum + (value - average) ** 2, 0);
|
|
30916
30917
|
}
|
|
30917
|
-
function
|
|
30918
|
+
function capped(values) {
|
|
30919
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
30920
|
+
const last = sorted.length - 1;
|
|
30921
|
+
const low = sorted[Math.floor(last * STEP_CHANGE_RULES.capDays)] ?? 0;
|
|
30922
|
+
const high = sorted[Math.ceil(last * (1 - STEP_CHANGE_RULES.capDays))] ?? 0;
|
|
30923
|
+
return values.map((value) => Math.min(high, Math.max(low, value)));
|
|
30924
|
+
}
|
|
30925
|
+
function findStepChange(observed) {
|
|
30918
30926
|
const { minDaysBefore, minDaysAfter, localDays, minLocalShiftInStdDevs, minZScore } = STEP_CHANGE_RULES;
|
|
30919
|
-
const count =
|
|
30927
|
+
const count = observed.length;
|
|
30920
30928
|
if (count < minDaysBefore + minDaysAfter) return null;
|
|
30929
|
+
const values = capped(observed);
|
|
30921
30930
|
const sums = [0];
|
|
30922
30931
|
for (const value of values) sums.push((sums.at(-1) ?? 0) + value);
|
|
30923
30932
|
const total = sums[count] ?? 0;
|
|
@@ -36213,7 +36222,7 @@ function bingSiteFor(sites, target) {
|
|
|
36213
36222
|
};
|
|
36214
36223
|
const wanted = host(target);
|
|
36215
36224
|
if (!wanted) return null;
|
|
36216
|
-
return sites.filter((site) =>
|
|
36225
|
+
return sites.filter((site) => host(site.url) === wanted).sort((a, b) => Number(b.verified) - Number(a.verified) || Number(b.url.startsWith("https://")) - Number(a.url.startsWith("https://")) || a.url.localeCompare(b.url, "en"))[0] ?? null;
|
|
36217
36226
|
}
|
|
36218
36227
|
//#endregion
|
|
36219
36228
|
//#region packages/core/dist/commands/present.js
|
|
@@ -36789,6 +36798,8 @@ async function runAnalyticsRows(input, google, options) {
|
|
|
36789
36798
|
const BING_LIMITS = ["Bing and Google count clicks, impressions and positions their own way. Their numbers sit side by side here and are never added together.", "Bing lists only its top queries and pages, grouped by week. A query or page missing from Bing’s list is unknown, not zero."];
|
|
36790
36799
|
const LOCAL_BING = "Bing data runs on the person’s machine with the CLI: connect a Bing Webmaster API key once with `npx -y seo providers bing connect`, then run `npx -y seo report --site <site> --section bing --json`.";
|
|
36791
36800
|
const NO_BING_HERE = "Bing: not included here, so these totals show Google only. Some sites get most of their search visits from Bing; run `npx -y seo report --site <site> --section bing --json` with the CLI to see them.";
|
|
36801
|
+
const BING_HOME = "https://www.bing.com/webmasters";
|
|
36802
|
+
const noBingData = (site) => `Bing has no data for ${site} yet. Newly added or newly verified sites take a few days to show data in Bing Webmaster Tools.`;
|
|
36792
36803
|
const sitesSeen = /* @__PURE__ */ new WeakMap();
|
|
36793
36804
|
async function bingFor(deps, property) {
|
|
36794
36805
|
if (!deps.bing) return {
|
|
@@ -36808,13 +36819,14 @@ async function bingFor(deps, property) {
|
|
|
36808
36819
|
sitesSeen.set(call, sites);
|
|
36809
36820
|
}
|
|
36810
36821
|
const site = bingSiteFor(await sites, property);
|
|
36811
|
-
|
|
36822
|
+
if (site?.verified) return {
|
|
36812
36823
|
call,
|
|
36813
|
-
site
|
|
36814
|
-
}
|
|
36824
|
+
site: site.url
|
|
36825
|
+
};
|
|
36826
|
+
return {
|
|
36815
36827
|
call: null,
|
|
36816
36828
|
site: null,
|
|
36817
|
-
missing:
|
|
36829
|
+
missing: site ? `${site.url} is in the connected Bing account but is not verified, so Bing gives no data for it. Verify it at ${BING_HOME}.` : `The connected Bing account does not have ${property.replace(/^sc-domain:/, "")}. Add and verify it at ${BING_HOME} (it can import sites from Search Console). New sites take a few days to show data.`
|
|
36818
36830
|
};
|
|
36819
36831
|
}
|
|
36820
36832
|
function days(range) {
|
|
@@ -36823,6 +36835,7 @@ function days(range) {
|
|
|
36823
36835
|
async function bingTotals(source, range, before) {
|
|
36824
36836
|
if (!source.call) return { missing: source.missing };
|
|
36825
36837
|
const traffic = await bingTraffic(source.call, source.site);
|
|
36838
|
+
if (!traffic.rows.length && !traffic.invalidRows) return { missing: noBingData(source.site) };
|
|
36826
36839
|
const sum = (window) => {
|
|
36827
36840
|
const rows = traffic.rows.filter((row) => row.date >= window.startDate && row.date <= window.endDate);
|
|
36828
36841
|
return {
|
|
@@ -36889,24 +36902,26 @@ async function bingSection(source, read, property, range) {
|
|
|
36889
36902
|
dataState: "final",
|
|
36890
36903
|
maxRows: 25e3
|
|
36891
36904
|
});
|
|
36892
|
-
const [queries, pages,
|
|
36905
|
+
const [queries, pages, googleRead] = await Promise.all([
|
|
36893
36906
|
bingQueries(source.call, source.site),
|
|
36894
36907
|
bingPages(source.call, source.site),
|
|
36895
|
-
google("query"),
|
|
36896
|
-
google("page")
|
|
36908
|
+
Promise.all([google("query"), google("page")]).catch((error) => error instanceof Error ? error : new Error(String(error)))
|
|
36897
36909
|
]);
|
|
36910
|
+
const googleError = googleRead instanceof Error ? googleRead : null;
|
|
36911
|
+
const [googleQueries, googlePages] = googleRead instanceof Error ? [] : googleRead;
|
|
36898
36912
|
const beside = (bing, googleRows, name) => {
|
|
36899
36913
|
const norm = (value) => name === "query" ? value.toLowerCase().trim() : value.replace(/#.*$/, "");
|
|
36900
|
-
const byKey = new Map(googleRows
|
|
36914
|
+
const byKey = new Map(googleRows?.map((row) => [norm(row.keys[0] ?? ""), row]));
|
|
36901
36915
|
return bing.map((row) => {
|
|
36902
36916
|
const g = byKey.get(norm(row.key));
|
|
36917
|
+
const known = googleRows ? 0 : null;
|
|
36903
36918
|
return {
|
|
36904
36919
|
[name]: row.key,
|
|
36905
36920
|
bingClicks: row.clicks,
|
|
36906
36921
|
bingImpressions: row.impressions,
|
|
36907
36922
|
bingPosition: row.position,
|
|
36908
|
-
googleClicks: g?.clicks ??
|
|
36909
|
-
googleImpressions: g?.impressions ??
|
|
36923
|
+
googleClicks: g?.clicks ?? known,
|
|
36924
|
+
googleImpressions: g?.impressions ?? known,
|
|
36910
36925
|
googlePosition: g ? Math.round(g.position * 10) / 10 : null
|
|
36911
36926
|
};
|
|
36912
36927
|
});
|
|
@@ -36914,6 +36929,7 @@ async function bingSection(source, read, property, range) {
|
|
|
36914
36929
|
const q = sumWeeks(queries.rows, range);
|
|
36915
36930
|
const p = sumWeeks(pages.rows, range);
|
|
36916
36931
|
const invalid = queries.invalidRows + pages.invalidRows;
|
|
36932
|
+
const empty = !queries.rows.length && !pages.rows.length && !invalid;
|
|
36917
36933
|
return {
|
|
36918
36934
|
range,
|
|
36919
36935
|
bingSite: source.site,
|
|
@@ -36922,9 +36938,11 @@ async function bingSection(source, read, property, range) {
|
|
|
36922
36938
|
bingPages: p.rows.length,
|
|
36923
36939
|
weeks: q.weeks.length
|
|
36924
36940
|
},
|
|
36925
|
-
items: beside(q.rows, googleQueries
|
|
36926
|
-
topPages: beside(p.rows, googlePages
|
|
36941
|
+
items: beside(q.rows, googleQueries?.rows, "query"),
|
|
36942
|
+
topPages: beside(p.rows, googlePages?.rows, "page"),
|
|
36927
36943
|
caveats: [
|
|
36944
|
+
...empty ? [noBingData(source.site)] : [],
|
|
36945
|
+
...googleError ? [`Search Console was not available, so the Google columns are empty (unknown, not zero): ${googleError.message}`] : [],
|
|
36928
36946
|
`Bing weeks counted: ${q.weeks.join(", ") || "none"} (each date names a week). Google numbers cover ${range.startDate} to ${range.endDate} exactly.`,
|
|
36929
36947
|
"Google numbers are for the same query text or page URL. 0 means Google showed no row for it in these dates.",
|
|
36930
36948
|
...BING_LIMITS,
|
|
@@ -36962,6 +36980,7 @@ async function runBingRows(input, deps) {
|
|
|
36962
36980
|
});
|
|
36963
36981
|
}
|
|
36964
36982
|
const notes = [
|
|
36983
|
+
...all.rows.length || all.invalidRows ? [] : [noBingData(source.site)],
|
|
36965
36984
|
`Bing weeks counted: ${now.weeks.join(", ") || "none"}; earlier period ${then.weeks.join(", ") || "none"}. Each date names a week.`,
|
|
36966
36985
|
...BING_LIMITS.slice(1),
|
|
36967
36986
|
...all.invalidRows ? [`${all.invalidRows} Bing rows were not valid and were left out.`] : []
|
|
@@ -37172,6 +37191,11 @@ async function facts(url) {
|
|
|
37172
37191
|
function checkPages(urls) {
|
|
37173
37192
|
return Promise.all(urls.slice(0, MAX_PAGES).map(facts));
|
|
37174
37193
|
}
|
|
37194
|
+
function temporaryRedirectNote(pages) {
|
|
37195
|
+
const temporary = pages.filter((page) => (page.status === 302 || page.status === 307) && page.redirects_to !== null && new URL(page.redirects_to).host === new URL(page.url).host);
|
|
37196
|
+
if (!temporary.length) return null;
|
|
37197
|
+
return `Note: 302 and 307 are temporary redirects; a permanent move usually uses 301 or 308. Temporary redirects here: ${temporary.map((page) => `${page.url} (${page.status} to ${page.redirects_to})`).join("; ")}.`;
|
|
37198
|
+
}
|
|
37175
37199
|
//#endregion
|
|
37176
37200
|
//#region packages/core/dist/commands/report.js
|
|
37177
37201
|
const SECTIONS = {
|
|
@@ -37461,7 +37485,7 @@ function reportUrls(rows, origin) {
|
|
|
37461
37485
|
return found;
|
|
37462
37486
|
}
|
|
37463
37487
|
function pageFactsTable(checked, origin) {
|
|
37464
|
-
|
|
37488
|
+
const rows = table(checked, [
|
|
37465
37489
|
"url",
|
|
37466
37490
|
"status",
|
|
37467
37491
|
"redirects_to",
|
|
@@ -37472,6 +37496,8 @@ function pageFactsTable(checked, origin) {
|
|
|
37472
37496
|
"robots",
|
|
37473
37497
|
...checked.some((page) => page.error) ? ["error"] : []
|
|
37474
37498
|
], origin, 320);
|
|
37499
|
+
const note = temporaryRedirectNote(checked);
|
|
37500
|
+
return note ? `${rows}\n${note}` : rows;
|
|
37475
37501
|
}
|
|
37476
37502
|
const PAGE_SIZE = 50;
|
|
37477
37503
|
async function allSections(input, deps) {
|
|
@@ -37788,11 +37814,48 @@ const RESEARCH_ACTIONS = [
|
|
|
37788
37814
|
];
|
|
37789
37815
|
const RESEARCH_PROVIDERS = ["dataforseo", "ahrefs"];
|
|
37790
37816
|
const RESEARCH_HELP = "keyword_metrics (keywords: volume, difficulty, CPC), ranked_keywords (target: keywords a domain or URL ranks for), serp (keywords, up to 5: the ranking pages), competitors (target: domains that share keywords), backlinks (target: referring domains). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.";
|
|
37817
|
+
const GENERIC_PLATFORMS = [
|
|
37818
|
+
"youtube.com",
|
|
37819
|
+
"facebook.com",
|
|
37820
|
+
"wikipedia.org",
|
|
37821
|
+
"reddit.com",
|
|
37822
|
+
"quora.com",
|
|
37823
|
+
"instagram.com",
|
|
37824
|
+
"linkedin.com",
|
|
37825
|
+
"x.com",
|
|
37826
|
+
"twitter.com",
|
|
37827
|
+
"pinterest.com",
|
|
37828
|
+
"tiktok.com",
|
|
37829
|
+
"amazon.com"
|
|
37830
|
+
];
|
|
37831
|
+
const within = (domain, parent) => domain === parent || domain.endsWith(`.${parent}`);
|
|
37832
|
+
function realCompetitors(rows, target) {
|
|
37833
|
+
const self = bareDomain(target);
|
|
37834
|
+
const domainOf = (row) => bareDomain(String(row.domain ?? row.competitor_domain ?? ""));
|
|
37835
|
+
const isSelf = (domain) => self !== "" && within(domain, self);
|
|
37836
|
+
const isPlatform = (domain) => GENERIC_PLATFORMS.some((name) => within(domain, name));
|
|
37837
|
+
const domains = rows.map(domainOf);
|
|
37838
|
+
const own = domains.filter(isSelf);
|
|
37839
|
+
const platforms = domains.filter((d) => !isSelf(d) && isPlatform(d));
|
|
37840
|
+
const gone = own.length + platforms.length;
|
|
37841
|
+
if (!gone) return {
|
|
37842
|
+
rows,
|
|
37843
|
+
note: null
|
|
37844
|
+
};
|
|
37845
|
+
const reasons = [own.length ? `the target itself (${own.join(", ")})` : "", platforms.length ? `generic platforms that share keywords with most sites (${platforms.join(", ")})` : ""].filter(Boolean);
|
|
37846
|
+
return {
|
|
37847
|
+
rows: rows.filter((row) => {
|
|
37848
|
+
const domain = domainOf(row);
|
|
37849
|
+
return !isSelf(domain) && !isPlatform(domain);
|
|
37850
|
+
}),
|
|
37851
|
+
note: `Left out ${gone} ${gone === 1 ? "row" : "rows"}: ${reasons.join(" and ")}. The fixed platform list is ${GENERIC_PLATFORMS.join(", ")}.`
|
|
37852
|
+
};
|
|
37853
|
+
}
|
|
37791
37854
|
async function runResearch(input, keys, options) {
|
|
37792
37855
|
const country = input.country ?? "us";
|
|
37793
37856
|
const target = input.target ?? "";
|
|
37794
37857
|
const offset = input.offset ?? 0;
|
|
37795
|
-
const
|
|
37858
|
+
const found = await research(input.provider, keys, {
|
|
37796
37859
|
action: input.action,
|
|
37797
37860
|
keywords: input.keywords ?? [],
|
|
37798
37861
|
target,
|
|
@@ -37800,6 +37863,10 @@ async function runResearch(input, keys, options) {
|
|
|
37800
37863
|
language: "en",
|
|
37801
37864
|
limit: Math.min(1e3, Math.max(100, offset + 50))
|
|
37802
37865
|
});
|
|
37866
|
+
const { rows, note } = input.action === "competitors" ? realCompetitors(found, target) : {
|
|
37867
|
+
rows: found,
|
|
37868
|
+
note: null
|
|
37869
|
+
};
|
|
37803
37870
|
const drKey = keys.get("x-ahrefs-key") ?? options.drKey;
|
|
37804
37871
|
const rated = await withDomainRating(rows, drKey);
|
|
37805
37872
|
const targetDr = target ? (await domainRatings([target], drKey)).get(bareDomain(target)) : void 0;
|
|
@@ -37813,6 +37880,7 @@ async function runResearch(input, keys, options) {
|
|
|
37813
37880
|
detail: input.detail ?? "summary",
|
|
37814
37881
|
notes: [
|
|
37815
37882
|
`${input.provider} estimates, in the provider’s own units.`,
|
|
37883
|
+
...note ? [note] : [],
|
|
37816
37884
|
...hasTargetDr ? [`${bareDomain(target)} domain_rating: ${targetDr}.`] : [],
|
|
37817
37885
|
...rated.added || hasTargetDr ? [DR_ATTRIBUTION] : []
|
|
37818
37886
|
],
|
|
@@ -37825,6 +37893,7 @@ async function runResearch(input, keys, options) {
|
|
|
37825
37893
|
country,
|
|
37826
37894
|
targetDomainRating: hasTargetDr ? targetDr : null,
|
|
37827
37895
|
attribution: rated.added || hasTargetDr ? DR_ATTRIBUTION : null,
|
|
37896
|
+
leftOut: note,
|
|
37828
37897
|
total: rated.rows.length,
|
|
37829
37898
|
offset,
|
|
37830
37899
|
rows: rated.rows.slice(offset, offset + 50)
|
|
@@ -37999,12 +38068,13 @@ async function runLostPages(input, deps) {
|
|
|
37999
38068
|
`${lost.length} pages kept ${LOST_SHARE * 100}% or less of at least ${MIN_LOST} clicks${lost.length > CHECKED ? `; the top ${CHECKED} by clicks lost are shown` : ""}.`,
|
|
38000
38069
|
rows.length ? table(rows, Object.keys(rows[0] ?? {}), originOf(input.site)) : "No pages lost their clicks.",
|
|
38001
38070
|
"Live checks are fetched now with no redirects followed. Index status is from URL Inspection; Google allows about 2,000 inspections a day per property.",
|
|
38071
|
+
temporaryRedirectNote(rows),
|
|
38002
38072
|
`Next: ${deps.call("page")} for one URL; ${deps.call("report", {
|
|
38003
38073
|
site: input.site,
|
|
38004
38074
|
section: "content-decay"
|
|
38005
38075
|
})} for pages that lost part of their clicks.`,
|
|
38006
38076
|
FACTS_ONLY
|
|
38007
|
-
].join("\n\n"),
|
|
38077
|
+
].filter(Boolean).join("\n\n"),
|
|
38008
38078
|
data: {
|
|
38009
38079
|
range,
|
|
38010
38080
|
comparisonRange: before,
|
|
@@ -38168,7 +38238,7 @@ const SHARED_TOOLS = [
|
|
|
38168
38238
|
...detail
|
|
38169
38239
|
}),
|
|
38170
38240
|
run: async (surface, input) => runReport$1({
|
|
38171
|
-
site: await surface.resolveSite(input.site),
|
|
38241
|
+
site: input.section === "bing" ? await surface.resolveSite(input.site).catch(() => input.site) : await surface.resolveSite(input.site),
|
|
38172
38242
|
section: input.section,
|
|
38173
38243
|
startDate: input.start_date,
|
|
38174
38244
|
endDate: input.end_date,
|