seo 0.8.2 → 0.8.3

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/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 version, r as __commonJSMin, t as load } from "./esm-DeY7lZeN.js";
2
+ import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-BZhSLbcQ.js";
3
3
  import { createHash, randomUUID, sign } from "node:crypto";
4
4
  import { ZodOptional, z } from "zod";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -38604,7 +38604,7 @@ async function totals$1(found, access, range, before, pages) {
38604
38604
  }, { signIn: access.signIn });
38605
38605
  const onPages = pages ? [filter("landingPage", pages, "CONTAINS")] : [];
38606
38606
  const fromAi = [...onPages, filter("sessionSource", AI_SOURCE_PATTERN, "FULL_REGEXP")];
38607
- const [channels, google, aiSources, aiPages] = await Promise.all([
38607
+ const [channels, google, aiSources, aiPages, engines] = await Promise.all([
38608
38608
  read(["sessionDefaultChannelGroup"], onPages),
38609
38609
  read([], [
38610
38610
  ...onPages,
@@ -38612,7 +38612,8 @@ async function totals$1(found, access, range, before, pages) {
38612
38612
  filter("sessionSource", "google")
38613
38613
  ]),
38614
38614
  read(["sessionSource"], fromAi),
38615
- read(["landingPage"], fromAi)
38615
+ read(["landingPage"], fromAi),
38616
+ read(["sessionSource"], [...onPages, filter("sessionDefaultChannelGroup", "Organic Search")])
38616
38617
  ]);
38617
38618
  const sum = () => ({
38618
38619
  google: {
@@ -38679,6 +38680,7 @@ async function totals$1(found, access, range, before, pages) {
38679
38680
  })).sort((a, b) => b.sessions - a.sessions || b.previousSessions - a.previousSessions || String(a[key]).localeCompare(String(b[key])));
38680
38681
  };
38681
38682
  const bySource = byName(aiSources, "source");
38683
+ const byEngine = byName(engines, "source");
38682
38684
  const aiNow = bySource.reduce((total, row) => total + row.sessions, 0);
38683
38685
  const aiBefore = bySource.reduce((total, row) => total + row.previousSessions, 0);
38684
38686
  const data = { metadata: {
@@ -38687,13 +38689,15 @@ async function totals$1(found, access, range, before, pages) {
38687
38689
  channels,
38688
38690
  google,
38689
38691
  aiSources,
38690
- aiPages
38692
+ aiPages,
38693
+ engines
38691
38694
  ].some((data) => data.metadata?.subjectToThresholding),
38692
38695
  dataLossFromOtherRow: [
38693
38696
  channels,
38694
38697
  google,
38695
38698
  aiSources,
38696
- aiPages
38699
+ aiPages,
38700
+ engines
38697
38701
  ].some((data) => data.metadata?.dataLossFromOtherRow)
38698
38702
  } };
38699
38703
  const notes = [];
@@ -38722,6 +38726,7 @@ async function totals$1(found, access, range, before, pages) {
38722
38726
  previousSessions: periods.before.all.sessions,
38723
38727
  sessionsChangePct: pct(periods.now.all.sessions, periods.before.all.sessions)
38724
38728
  },
38729
+ organicSearchByEngine: byEngine.slice(0, 10),
38725
38730
  aiAssistants: {
38726
38731
  sessions: aiNow,
38727
38732
  previousSessions: aiBefore,
@@ -38734,6 +38739,11 @@ async function totals$1(found, access, range, before, pages) {
38734
38739
  };
38735
38740
  }
38736
38741
  const change$1 = (value) => value === null ? "n/a" : `${value > 0 ? "+" : ""}${value}%`;
38742
+ function engineText(rows) {
38743
+ const shown = rows.filter((row) => row.sessions || row.previousSessions);
38744
+ if (shown.length < 2) return "";
38745
+ return `Organic Search sessions by search engine: ${shown.slice(0, AI_ROWS).map((row) => `${row.source} ${row.sessions} (was ${row.previousSessions})`).join(", ")}.`;
38746
+ }
38737
38747
  function aiText(ai) {
38738
38748
  const list = (rows, key) => rows.slice(0, AI_ROWS).map((row) => `${row[key]} ${row.sessions} (was ${row.previousSessions})`).join(", ");
38739
38749
  if (!ai.sessions && !ai.previousSessions) return "Visits from AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot and others): none in either period.";
@@ -38762,10 +38772,11 @@ async function analyticsBeside(access, site, range, before, pages) {
38762
38772
  return {
38763
38773
  text: [
38764
38774
  `Google Analytics, property ${data.propertyName} (${data.property}), matched by its web stream ${data.webStream}${pages ? `, landing pages whose path contains "${pages}"` : ""}: Organic Search sessions from Google ${google.sessions} (was ${google.previousSessions}, ${change$1(google.sessionsChangePct)}), from every search engine ${organic.sessions} (was ${organic.previousSessions}, ${change$1(organic.sessionsChangePct)})${keyEvents}, sessions from all channels ${all.sessions} (was ${all.previousSessions}, ${change$1(all.sessionsChangePct)}).`,
38775
+ engineText(data.organicSearchByEngine),
38765
38776
  "Sessions are Google Analytics’ own count, not Search Console clicks.",
38766
38777
  ...data.notes,
38767
38778
  ...others.length ? [`Other properties with a matching web stream, with fewer sessions: ${others.join(", ")}.`] : []
38768
- ].join(" ").concat("\n\n", aiText(data.aiAssistants)),
38779
+ ].filter(Boolean).join(" ").concat("\n\n", aiText(data.aiAssistants)),
38769
38780
  data: {
38770
38781
  ...data,
38771
38782
  ...others.length ? { otherMatches: others } : {}
@@ -39000,6 +39011,102 @@ async function runBingRows(input, deps) {
39000
39011
  }
39001
39012
  };
39002
39013
  }
39014
+ function moved(rows, range, before, name, limit) {
39015
+ const now = sumWeeks(rows, range);
39016
+ const then = sumWeeks(rows, before);
39017
+ const old = new Map(then.rows.map((row) => [row.key, row]));
39018
+ const seen = new Set(now.rows.map((row) => row.key));
39019
+ const all = now.rows.map((row) => {
39020
+ const earlier = old.get(row.key);
39021
+ return {
39022
+ [name]: row.key,
39023
+ clicks: row.clicks,
39024
+ previousClicks: earlier ? earlier.clicks : null,
39025
+ clicksChange: earlier ? row.clicks - earlier.clicks : null,
39026
+ impressions: row.impressions,
39027
+ position: row.position
39028
+ };
39029
+ });
39030
+ const by = (sign) => all.filter((row) => row.clicksChange !== null && sign * row.clicksChange > 0).sort((a, b) => sign * (b.clicksChange - a.clicksChange) || String(a[name]).localeCompare(String(b[name]))).slice(0, limit);
39031
+ return {
39032
+ top: all.slice(0, limit),
39033
+ biggestLosses: by(-1),
39034
+ biggestGains: by(1),
39035
+ leftTopList: then.rows.filter((row) => !seen.has(row.key)).slice(0, limit).map((row) => ({
39036
+ [name]: row.key,
39037
+ previousClicks: row.clicks
39038
+ })),
39039
+ weeks: now.weeks,
39040
+ previousWeeks: then.weeks
39041
+ };
39042
+ }
39043
+ async function bingOverview(source, range, before) {
39044
+ if (!source.call) throw new CommandError("NOT_CONNECTED", source.missing);
39045
+ const [totals, queries, pages] = await Promise.all([
39046
+ bingTotals(source, range, before),
39047
+ bingQueries(source.call, source.site),
39048
+ bingPages(source.call, source.site)
39049
+ ]);
39050
+ if ("missing" in totals) throw new CommandError("NOT_CONNECTED", totals.missing);
39051
+ const { current: now, previous: then } = totals;
39052
+ const q = moved(queries.rows, range, before, "query", 15);
39053
+ const p = moved(pages.rows, range, before, "page", 15);
39054
+ return {
39055
+ bingSite: source.site,
39056
+ range,
39057
+ comparisonRange: before,
39058
+ summary: {
39059
+ clicks: now.clicks,
39060
+ previousClicks: then.clicks,
39061
+ clicksChangePct: pct(now.clicks, then.clicks),
39062
+ impressions: now.impressions,
39063
+ previousImpressions: then.impressions,
39064
+ impressionsChangePct: pct(now.impressions, then.impressions),
39065
+ days: now.days,
39066
+ previousDays: then.days
39067
+ },
39068
+ pages: p,
39069
+ queries: q,
39070
+ caveats: [
39071
+ ...now.days < now.expectedDays || then.days < then.expectedDays ? [`Bing has data for ${now.days} of ${now.expectedDays} days (earlier period ${then.days} of ${then.expectedDays}), so its totals are partial.`] : [],
39072
+ `Totals are Bing's daily site totals. Page and query rows are Bing's weekly top rows: weeks ${q.weeks.join(", ") || "none"} against ${q.previousWeeks.join(", ") || "none"}.`,
39073
+ ...BING_LIMITS.slice(1)
39074
+ ]
39075
+ };
39076
+ }
39077
+ function bingOverviewText(report, heading, origin, rowsPerList) {
39078
+ const range = report.range;
39079
+ const before = report.comparisonRange;
39080
+ const t = report.summary;
39081
+ const columns = (name) => [
39082
+ name,
39083
+ "clicks",
39084
+ "previousClicks",
39085
+ "clicksChange",
39086
+ "impressions",
39087
+ "position"
39088
+ ];
39089
+ const part = (title, rows, name) => {
39090
+ const list = rows ?? [];
39091
+ return list.length ? `${title}:\n${table(list.slice(0, rowsPerList), columns(name), origin)}` : "";
39092
+ };
39093
+ const group = (key, name) => {
39094
+ const lists = report[key];
39095
+ return [
39096
+ part(`Biggest Bing ${key === "pages" ? "page" : "query"} click losses`, lists.biggestLosses, name),
39097
+ part(`Biggest Bing ${key === "pages" ? "page" : "query"} click gains`, lists.biggestGains, name),
39098
+ lists.leftTopList?.length ? `Left Bing's top ${key} list (now unknown, not zero): ${lists.leftTopList.slice(0, rowsPerList).map((row) => `${row[name]} (had ${row.previousClicks} clicks)`).join(", ")}.` : "",
39099
+ rowsPerList >= 10 ? part(`Top Bing ${key}`, lists.top, name) : ""
39100
+ ].filter(Boolean).join("\n\n");
39101
+ };
39102
+ return [
39103
+ `${heading}, ${range.startDate} to ${range.endDate} against ${before.startDate} to ${before.endDate}.`,
39104
+ t.days || t.previousDays ? `Bing totals: clicks ${t.clicks} (was ${t.previousClicks}, ${change(t.clicks, t.previousClicks)}), impressions ${t.impressions} (was ${t.previousImpressions}, ${change(t.impressions, t.previousImpressions)}).` : "Bing totals: unknown. Bing has no daily data for these dates.",
39105
+ group("pages", "page"),
39106
+ group("queries", "query"),
39107
+ `Limits:\n${report.caveats.map((line) => `- ${line}`).join("\n")}`
39108
+ ].filter(Boolean).join("\n\n");
39109
+ }
39003
39110
  //#endregion
39004
39111
  //#region packages/core/dist/commands/clicky.js
39005
39112
  const CLICKY_KEY_PREFIX = "x-clicky-";
@@ -39732,6 +39839,24 @@ async function enginesLine(overview, bing) {
39732
39839
  return `Bing: could not be read. ${error instanceof Error ? error.message : String(error)}`;
39733
39840
  }
39734
39841
  }
39842
+ async function bingLead(overview, bing, origin, rowsPerList) {
39843
+ const range = overview.range;
39844
+ const before = overview.comparisonRange;
39845
+ const google = overview.summary.clicks;
39846
+ try {
39847
+ const source = await bing();
39848
+ if (!source.call) return null;
39849
+ const totals = await bingTotals(source, range, before);
39850
+ if ("missing" in totals || totals.current.clicks <= google) return null;
39851
+ const report = await bingOverview(source, range, before);
39852
+ return {
39853
+ text: `## Bing\n${bingOverviewText(report, `Bing sends more clicks than Google here, so here is Bing's overview of ${totals.site}`, origin, rowsPerList)}`,
39854
+ data: report
39855
+ };
39856
+ } catch {
39857
+ return null;
39858
+ }
39859
+ }
39735
39860
  async function build(read, site, section, range, now, bing) {
39736
39861
  const dependencies = {
39737
39862
  searchAnalytics: read,
@@ -39851,6 +39976,8 @@ async function allSections(input, deps) {
39851
39976
  if (!overview || overview instanceof Error) throw overview ?? /* @__PURE__ */ new Error("The overview did not run.");
39852
39977
  const parts = [overviewText(overview, `Report on ${input.site}`, origin, 5)];
39853
39978
  parts.push(deps.bing ? await enginesLine(overview, bing) : NO_BING_HERE);
39979
+ const bingFirst = deps.bing ? await bingLead(overview, bing, origin, 5) : null;
39980
+ if (bingFirst) parts.push(bingFirst.text);
39854
39981
  const ga = await analytics;
39855
39982
  if (ga) parts.push(ga.text);
39856
39983
  const brand = rest.find((run) => !(run instanceof Error) && brandNote(run) !== null);
@@ -39883,6 +40010,7 @@ async function allSections(input, deps) {
39883
40010
  site: input.site,
39884
40011
  overview,
39885
40012
  ...ga ? { analytics: ga.data } : {},
40013
+ ...bingFirst ? { bingOverview: bingFirst.data } : {},
39886
40014
  sections
39887
40015
  }
39888
40016
  };
@@ -39899,10 +40027,13 @@ async function oneSection(input, deps) {
39899
40027
  if (section === "overview") {
39900
40028
  const ga = await analyticsBeside(deps.analytics, site, range, earlierRange(range));
39901
40029
  if (ga) report.analytics = ga.data;
40030
+ const bingFirst = deps.bing ? await bingLead(report, bing, origin, 10) : null;
40031
+ if (bingFirst) report.bingOverview = bingFirst.data;
39902
40032
  if (detail !== "full") return {
39903
40033
  text: [
39904
40034
  overviewText(report, `overview of ${site}`, origin, 10),
39905
40035
  deps.bing ? await enginesLine(report, bing) : NO_BING_HERE,
40036
+ bingFirst?.text,
39906
40037
  ga?.text,
39907
40038
  `More: ${deps.call("report", {
39908
40039
  site,
@@ -39967,6 +40098,27 @@ function runReport$1(input, deps) {
39967
40098
  section: input.section
39968
40099
  }, deps) : allSections(input, deps);
39969
40100
  }
40101
+ async function runBingReport(input, deps) {
40102
+ const range = dateWindow(input, DEFAULT_DAYS.overview);
40103
+ const before = earlierRange(range);
40104
+ const source = await bingFor(deps, input.site);
40105
+ if (!source.call) throw new CommandError("NOT_CONNECTED", source.missing);
40106
+ const [report, ga] = await Promise.all([bingOverview(source, range, before), analyticsBeside(deps.analytics, source.site, range, before)]);
40107
+ if (ga) report.analytics = ga.data;
40108
+ return {
40109
+ text: [
40110
+ bingOverviewText(report, `Bing report on ${source.site}. No Search Console property covers this site, so this report uses Bing`, originOf(source.site), 10),
40111
+ ga?.text,
40112
+ `More: ${deps.call("data", {
40113
+ source: "bing",
40114
+ site: input.site,
40115
+ dimensions: "page"
40116
+ })} for every Bing page, or the same with query.`,
40117
+ FACTS_ONLY.replace("Search Console", "Bing Webmaster Tools")
40118
+ ].filter(Boolean).join("\n\n"),
40119
+ data: report
40120
+ };
40121
+ }
39970
40122
  //#endregion
39971
40123
  //#region packages/core/dist/commands/research-providers.js
39972
40124
  function need(headers, provider) {
@@ -40607,14 +40759,25 @@ const SHARED_TOOLS = [
40607
40759
  ...dates,
40608
40760
  ...detail
40609
40761
  }),
40610
- run: async (surface, input) => runReport$1({
40611
- site: input.section === "bing" ? await surface.resolveSite(input.site).catch(() => input.site) : await surface.resolveSite(input.site),
40612
- section: input.section,
40613
- startDate: input.start_date,
40614
- endDate: input.end_date,
40615
- detail: input.detail,
40616
- offset: input.offset
40617
- }, surface.deps)
40762
+ run: async (surface, input) => {
40763
+ const resolved = await surface.resolveSite(input.site).catch((error) => error instanceof Error ? error : new Error(String(error)));
40764
+ if (resolved instanceof Error && !input.section && surface.deps.bing) return runBingReport({
40765
+ site: input.site,
40766
+ startDate: input.start_date,
40767
+ endDate: input.end_date
40768
+ }, surface.deps).catch(() => {
40769
+ throw resolved;
40770
+ });
40771
+ if (resolved instanceof Error && input.section !== "bing") throw resolved;
40772
+ return runReport$1({
40773
+ site: resolved instanceof Error ? input.site : resolved,
40774
+ section: input.section,
40775
+ startDate: input.start_date,
40776
+ endDate: input.end_date,
40777
+ detail: input.detail,
40778
+ offset: input.offset
40779
+ }, surface.deps);
40780
+ }
40618
40781
  }),
40619
40782
  tool({
40620
40783
  name: "seo_page",