seo 0.3.0-beta.2 → 0.3.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/PRIVACY.md +62 -25
- package/README.md +49 -18
- package/dist/cli.js +136 -130
- package/dist/cli.js.map +1 -1
- package/dist/{dist-D41t9hLG.js → dist-C9P8dY80.js} +1 -1
- package/dist/{dist-STeaS3Hh.js → dist-wSAqXmM1.js} +58 -58
- package/dist/dist-wSAqXmM1.js.map +1 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.js +74 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +473 -38
- package/dist/mcp.js.map +1 -1
- package/dist/{package-1f8VcMU8.js → package-7H_jIpAz.js} +2 -2
- package/dist/{package-1f8VcMU8.js.map → package-7H_jIpAz.js.map} +1 -1
- package/evals/research.json +2 -2
- package/package.json +10 -2
- package/skills/seo/SKILL.md +21 -11
- package/dist/dist-STeaS3Hh.js.map +0 -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-7H_jIpAz.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";
|
|
@@ -23897,12 +23897,12 @@ const KEYRING_SERVICE = "seo";
|
|
|
23897
23897
|
const PRIVATE_FILE_MODE = 384;
|
|
23898
23898
|
const PROVIDER_SECRET_NAMES = {
|
|
23899
23899
|
ahrefsApiKey: "ahrefs-api-key",
|
|
23900
|
+
bingApiKey: "bing-api-key",
|
|
23900
23901
|
clickySiteKeys: "clicky-sitekeys",
|
|
23901
23902
|
dataForSeoCredentials: "dataforseo-credentials",
|
|
23902
23903
|
providerExtensionCredentials: "provider-extension-credentials"
|
|
23903
23904
|
};
|
|
23904
23905
|
const REMOVED_PROVIDER_SECRET_NAMES = [
|
|
23905
|
-
"bing-api-key",
|
|
23906
23906
|
"indexnow-keys",
|
|
23907
23907
|
"semrush-api-key",
|
|
23908
23908
|
"serpbase-api-key"
|
|
@@ -39117,6 +39117,118 @@ function analyticsConnection(client) {
|
|
|
39117
39117
|
};
|
|
39118
39118
|
}
|
|
39119
39119
|
//#endregion
|
|
39120
|
+
//#region packages/core/dist/bing/api.js
|
|
39121
|
+
const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
39122
|
+
const MAX_BYTES$1 = 8e6;
|
|
39123
|
+
function bingCall(auth, options) {
|
|
39124
|
+
return async (method, params = {}) => {
|
|
39125
|
+
const url = new URL(`${BASE}/${method}`);
|
|
39126
|
+
for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
|
|
39127
|
+
const headers = { accept: "application/json" };
|
|
39128
|
+
if ("apiKey" in auth) url.searchParams.set("apikey", auth.apiKey);
|
|
39129
|
+
else headers.authorization = `Bearer ${await auth.accessToken()}`;
|
|
39130
|
+
let response;
|
|
39131
|
+
try {
|
|
39132
|
+
response = await fetch(url, {
|
|
39133
|
+
headers,
|
|
39134
|
+
signal: AbortSignal.timeout(3e4)
|
|
39135
|
+
});
|
|
39136
|
+
} catch (error) {
|
|
39137
|
+
throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster did not respond: ${error instanceof Error ? error.message : String(error)}`);
|
|
39138
|
+
}
|
|
39139
|
+
if (!response.ok) {
|
|
39140
|
+
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
39141
|
+
if (response.status === 401 || /invalidapikey/i.test(detail)) throw new SeoError("AUTH_REQUIRED", `Bing Webmaster rejected the sign-in. ${options.signIn}`);
|
|
39142
|
+
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.");
|
|
39143
|
+
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.");
|
|
39144
|
+
if (response.status === 429 || /throttle/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is limiting requests. Try again in a few minutes.");
|
|
39145
|
+
throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
|
|
39146
|
+
}
|
|
39147
|
+
return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$1 })).d;
|
|
39148
|
+
};
|
|
39149
|
+
}
|
|
39150
|
+
function day(value) {
|
|
39151
|
+
if (typeof value !== "string") return null;
|
|
39152
|
+
const legacy = /^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/.exec(value);
|
|
39153
|
+
const date = legacy ? new Date(Number(legacy[1])) : new Date(value);
|
|
39154
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
|
|
39155
|
+
}
|
|
39156
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
39157
|
+
function list(data, label) {
|
|
39158
|
+
if (!Array.isArray(data)) throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster returned an invalid ${label}.`);
|
|
39159
|
+
return data.filter((item) => Boolean(item) && typeof item === "object");
|
|
39160
|
+
}
|
|
39161
|
+
async function listBingSites(call) {
|
|
39162
|
+
return list(await call("GetUserSites"), "site list").filter((row) => typeof row.Url === "string").map((row) => ({
|
|
39163
|
+
url: row.Url,
|
|
39164
|
+
verified: row.IsVerified === true
|
|
39165
|
+
})).sort((a, b) => a.url.localeCompare(b.url, "en"));
|
|
39166
|
+
}
|
|
39167
|
+
async function bingTraffic(call, site) {
|
|
39168
|
+
const rows = [];
|
|
39169
|
+
let invalidRows = 0;
|
|
39170
|
+
for (const row of list(await call("GetRankAndTrafficStats", { siteUrl: site }), "traffic list")) {
|
|
39171
|
+
const date = day(row.Date);
|
|
39172
|
+
const clicks = count(row.Clicks);
|
|
39173
|
+
const impressions = count(row.Impressions);
|
|
39174
|
+
if (!date || clicks === null || impressions === null) {
|
|
39175
|
+
invalidRows += 1;
|
|
39176
|
+
continue;
|
|
39177
|
+
}
|
|
39178
|
+
rows.push({
|
|
39179
|
+
date,
|
|
39180
|
+
clicks,
|
|
39181
|
+
impressions
|
|
39182
|
+
});
|
|
39183
|
+
}
|
|
39184
|
+
rows.sort((a, b) => a.date.localeCompare(b.date, "en"));
|
|
39185
|
+
return {
|
|
39186
|
+
rows,
|
|
39187
|
+
invalidRows
|
|
39188
|
+
};
|
|
39189
|
+
}
|
|
39190
|
+
async function keyed(call, method, params) {
|
|
39191
|
+
const rows = [];
|
|
39192
|
+
let invalidRows = 0;
|
|
39193
|
+
for (const row of list(await call(method, params), "row list")) {
|
|
39194
|
+
const date = day(row.Date);
|
|
39195
|
+
const key = typeof row.Query === "string" ? row.Query.trim() : "";
|
|
39196
|
+
const clicks = count(row.Clicks);
|
|
39197
|
+
const impressions = count(row.Impressions);
|
|
39198
|
+
const position = count(row.AvgImpressionPosition);
|
|
39199
|
+
if (!date || !key || clicks === null || impressions === null) {
|
|
39200
|
+
invalidRows += 1;
|
|
39201
|
+
continue;
|
|
39202
|
+
}
|
|
39203
|
+
rows.push({
|
|
39204
|
+
date,
|
|
39205
|
+
key,
|
|
39206
|
+
clicks,
|
|
39207
|
+
impressions,
|
|
39208
|
+
position
|
|
39209
|
+
});
|
|
39210
|
+
}
|
|
39211
|
+
rows.sort((a, b) => a.date.localeCompare(b.date, "en") || a.key.localeCompare(b.key, "en"));
|
|
39212
|
+
return {
|
|
39213
|
+
rows,
|
|
39214
|
+
invalidRows
|
|
39215
|
+
};
|
|
39216
|
+
}
|
|
39217
|
+
const bingQueries = (call, site) => keyed(call, "GetQueryStats", { siteUrl: site });
|
|
39218
|
+
const bingPages = (call, site) => keyed(call, "GetPageStats", { siteUrl: site });
|
|
39219
|
+
function bingSiteFor(sites, target) {
|
|
39220
|
+
const host = (value) => {
|
|
39221
|
+
try {
|
|
39222
|
+
return new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/, "")}`).hostname.replace(/^www\./, "");
|
|
39223
|
+
} catch {
|
|
39224
|
+
return null;
|
|
39225
|
+
}
|
|
39226
|
+
};
|
|
39227
|
+
const wanted = host(target);
|
|
39228
|
+
if (!wanted) return null;
|
|
39229
|
+
return sites.filter((site) => site.verified && host(site.url) === wanted).sort((a, b) => Number(b.url.startsWith("https://")) - Number(a.url.startsWith("https://")) || a.url.localeCompare(b.url, "en"))[0]?.url ?? null;
|
|
39230
|
+
}
|
|
39231
|
+
//#endregion
|
|
39120
39232
|
//#region packages/core/dist/commands/present.js
|
|
39121
39233
|
const LIST_KEYS = [
|
|
39122
39234
|
"items",
|
|
@@ -39616,6 +39728,15 @@ async function runRows(input, deps) {
|
|
|
39616
39728
|
}
|
|
39617
39729
|
//#endregion
|
|
39618
39730
|
//#region packages/core/dist/commands/analytics.js
|
|
39731
|
+
async function analyticsSignIn(accounts, property, signIn) {
|
|
39732
|
+
const only = accounts.length === 1 ? accounts[0] : void 0;
|
|
39733
|
+
if (only) return only.google;
|
|
39734
|
+
const account = (await Promise.all(accounts.map(async (account) => {
|
|
39735
|
+
return (await listAnalyticsAccounts(account.google, { signIn }).catch(() => [])).some((summary) => summary.propertySummaries.some((item) => item.property === `properties/${property}`)) ? account : null;
|
|
39736
|
+
}))).find(Boolean);
|
|
39737
|
+
if (account) return account.google;
|
|
39738
|
+
throw new CommandError("NO_ACCESS", `No connected Google account can read Google Analytics property ${property}. Call seo_sites to list the properties.`);
|
|
39739
|
+
}
|
|
39619
39740
|
async function runAnalyticsRows(input, google, options) {
|
|
39620
39741
|
const range = dateWindow(input, 28);
|
|
39621
39742
|
const offset = input.offset ?? 0;
|
|
@@ -39669,6 +39790,215 @@ async function runAnalyticsRows(input, google, options) {
|
|
|
39669
39790
|
};
|
|
39670
39791
|
}
|
|
39671
39792
|
//#endregion
|
|
39793
|
+
//#region packages/core/dist/commands/bing.js
|
|
39794
|
+
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."];
|
|
39795
|
+
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`.";
|
|
39796
|
+
const sitesSeen = /* @__PURE__ */ new WeakMap();
|
|
39797
|
+
async function bingFor(deps, property) {
|
|
39798
|
+
if (!deps.bing) return {
|
|
39799
|
+
call: null,
|
|
39800
|
+
site: null,
|
|
39801
|
+
missing: LOCAL_BING
|
|
39802
|
+
};
|
|
39803
|
+
const call = await deps.bing();
|
|
39804
|
+
if (!call) return {
|
|
39805
|
+
call: null,
|
|
39806
|
+
site: null,
|
|
39807
|
+
missing: `Bing is not connected. ${deps.bingConnect ?? ""}`.trim()
|
|
39808
|
+
};
|
|
39809
|
+
let sites = sitesSeen.get(call);
|
|
39810
|
+
if (!sites) {
|
|
39811
|
+
sites = listBingSites(call);
|
|
39812
|
+
sitesSeen.set(call, sites);
|
|
39813
|
+
}
|
|
39814
|
+
const site = bingSiteFor(await sites, property);
|
|
39815
|
+
return site ? {
|
|
39816
|
+
call,
|
|
39817
|
+
site
|
|
39818
|
+
} : {
|
|
39819
|
+
call: null,
|
|
39820
|
+
site: null,
|
|
39821
|
+
missing: `The connected Bing account has no verified site for ${property.replace(/^sc-domain:/, "")}. Add it in Bing Webmaster Tools; it can import sites from Search Console.`
|
|
39822
|
+
};
|
|
39823
|
+
}
|
|
39824
|
+
function days(range) {
|
|
39825
|
+
return Math.round((Date.parse(range.endDate) - Date.parse(range.startDate)) / 864e5) + 1;
|
|
39826
|
+
}
|
|
39827
|
+
async function bingTotals(source, range, before) {
|
|
39828
|
+
if (!source.call) return { missing: source.missing };
|
|
39829
|
+
const traffic = await bingTraffic(source.call, source.site);
|
|
39830
|
+
const sum = (window) => {
|
|
39831
|
+
const rows = traffic.rows.filter((row) => row.date >= window.startDate && row.date <= window.endDate);
|
|
39832
|
+
return {
|
|
39833
|
+
clicks: rows.reduce((total, row) => total + row.clicks, 0),
|
|
39834
|
+
impressions: rows.reduce((total, row) => total + row.impressions, 0),
|
|
39835
|
+
days: new Set(rows.map((row) => row.date)).size,
|
|
39836
|
+
expectedDays: days(window)
|
|
39837
|
+
};
|
|
39838
|
+
};
|
|
39839
|
+
return {
|
|
39840
|
+
site: source.site,
|
|
39841
|
+
current: sum(range),
|
|
39842
|
+
previous: sum(before),
|
|
39843
|
+
invalidRows: traffic.invalidRows
|
|
39844
|
+
};
|
|
39845
|
+
}
|
|
39846
|
+
const change = (now, before) => {
|
|
39847
|
+
const value = pct(now, before);
|
|
39848
|
+
return value === null ? "n/a" : `${value > 0 ? "+" : ""}${value}%`;
|
|
39849
|
+
};
|
|
39850
|
+
function enginesText(google, bing) {
|
|
39851
|
+
if ("missing" in bing) return `Bing: ${bing.missing}`;
|
|
39852
|
+
const { current: now, previous: then } = bing;
|
|
39853
|
+
const lines = [`Search engines, same dates. Google (Search Console): clicks ${google.clicks} (was ${google.previousClicks}, ${change(google.clicks, google.previousClicks)}), impressions ${google.impressions}. Bing (${bing.site}): clicks ${now.clicks} (was ${then.clicks}, ${change(now.clicks, then.clicks)}), impressions ${now.impressions} (was ${then.impressions}).`];
|
|
39854
|
+
const both = google.clicks + now.clicks;
|
|
39855
|
+
if (both > 0) lines.push(`Share of these clicks: Google ${Math.round(google.clicks / both * 100)}%, Bing ${Math.round(now.clicks / both * 100)}%.`);
|
|
39856
|
+
if (now.days < now.expectedDays || then.days < then.expectedDays) lines.push(`Bing has data for ${now.days} of ${now.expectedDays} days (earlier period ${then.days} of ${then.expectedDays}), so its totals are partial.`);
|
|
39857
|
+
return lines.join(" ");
|
|
39858
|
+
}
|
|
39859
|
+
function sumWeeks(rows, window) {
|
|
39860
|
+
const inside = rows.filter((row) => row.date >= window.startDate && row.date <= window.endDate);
|
|
39861
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
39862
|
+
for (const row of inside) {
|
|
39863
|
+
const found = byKey.get(row.key) ?? {
|
|
39864
|
+
key: row.key,
|
|
39865
|
+
clicks: 0,
|
|
39866
|
+
impressions: 0,
|
|
39867
|
+
position: null,
|
|
39868
|
+
weighted: 0,
|
|
39869
|
+
seen: 0
|
|
39870
|
+
};
|
|
39871
|
+
found.clicks += row.clicks;
|
|
39872
|
+
found.impressions += row.impressions;
|
|
39873
|
+
if (row.position !== null && row.impressions > 0) {
|
|
39874
|
+
found.weighted += row.position * row.impressions;
|
|
39875
|
+
found.seen += row.impressions;
|
|
39876
|
+
}
|
|
39877
|
+
byKey.set(row.key, found);
|
|
39878
|
+
}
|
|
39879
|
+
return {
|
|
39880
|
+
rows: [...byKey.values()].map(({ weighted, seen, ...row }) => ({
|
|
39881
|
+
...row,
|
|
39882
|
+
position: seen > 0 ? Math.round(weighted / seen * 10) / 10 : null
|
|
39883
|
+
})).sort((a, b) => b.clicks - a.clicks || b.impressions - a.impressions || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)),
|
|
39884
|
+
weeks: [...new Set(inside.map((row) => row.date))].sort()
|
|
39885
|
+
};
|
|
39886
|
+
}
|
|
39887
|
+
async function bingSection(source, read, property, range) {
|
|
39888
|
+
if (!source.call) throw new CommandError("NOT_CONNECTED", source.missing);
|
|
39889
|
+
const google = (dimension) => read(property, {
|
|
39890
|
+
...range,
|
|
39891
|
+
dimensions: [dimension],
|
|
39892
|
+
type: "web",
|
|
39893
|
+
dataState: "final",
|
|
39894
|
+
maxRows: 25e3
|
|
39895
|
+
});
|
|
39896
|
+
const [queries, pages, googleQueries, googlePages] = await Promise.all([
|
|
39897
|
+
bingQueries(source.call, source.site),
|
|
39898
|
+
bingPages(source.call, source.site),
|
|
39899
|
+
google("query"),
|
|
39900
|
+
google("page")
|
|
39901
|
+
]);
|
|
39902
|
+
const beside = (bing, googleRows, name) => {
|
|
39903
|
+
const norm = (value) => name === "query" ? value.toLowerCase().trim() : value.replace(/#.*$/, "");
|
|
39904
|
+
const byKey = new Map(googleRows.map((row) => [norm(row.keys[0] ?? ""), row]));
|
|
39905
|
+
return bing.map((row) => {
|
|
39906
|
+
const g = byKey.get(norm(row.key));
|
|
39907
|
+
return {
|
|
39908
|
+
[name]: row.key,
|
|
39909
|
+
bingClicks: row.clicks,
|
|
39910
|
+
bingImpressions: row.impressions,
|
|
39911
|
+
bingPosition: row.position,
|
|
39912
|
+
googleClicks: g?.clicks ?? 0,
|
|
39913
|
+
googleImpressions: g?.impressions ?? 0,
|
|
39914
|
+
googlePosition: g ? Math.round(g.position * 10) / 10 : null
|
|
39915
|
+
};
|
|
39916
|
+
});
|
|
39917
|
+
};
|
|
39918
|
+
const q = sumWeeks(queries.rows, range);
|
|
39919
|
+
const p = sumWeeks(pages.rows, range);
|
|
39920
|
+
const invalid = queries.invalidRows + pages.invalidRows;
|
|
39921
|
+
return {
|
|
39922
|
+
range,
|
|
39923
|
+
bingSite: source.site,
|
|
39924
|
+
summary: {
|
|
39925
|
+
bingQueries: q.rows.length,
|
|
39926
|
+
bingPages: p.rows.length,
|
|
39927
|
+
weeks: q.weeks.length
|
|
39928
|
+
},
|
|
39929
|
+
items: beside(q.rows, googleQueries.rows, "query"),
|
|
39930
|
+
topPages: beside(p.rows, googlePages.rows, "page"),
|
|
39931
|
+
caveats: [
|
|
39932
|
+
`Bing weeks counted: ${q.weeks.join(", ") || "none"} (each date names a week). Google numbers cover ${range.startDate} to ${range.endDate} exactly.`,
|
|
39933
|
+
"Google numbers are for the same query text or page URL. 0 means Google showed no row for it in these dates.",
|
|
39934
|
+
...BING_LIMITS,
|
|
39935
|
+
...invalid ? [`${invalid} Bing rows were not valid and were left out.`] : []
|
|
39936
|
+
]
|
|
39937
|
+
};
|
|
39938
|
+
}
|
|
39939
|
+
async function runBingRows(input, deps) {
|
|
39940
|
+
const source = await bingFor(deps, input.site);
|
|
39941
|
+
if (!source.call) throw new CommandError("NOT_CONNECTED", source.missing);
|
|
39942
|
+
const range = dateWindow(input, 28);
|
|
39943
|
+
const previous = earlierRange(range);
|
|
39944
|
+
const all = await (input.dimension === "page" ? bingPages : bingQueries)(source.call, source.site);
|
|
39945
|
+
const now = sumWeeks(all.rows, range);
|
|
39946
|
+
const then = sumWeeks(all.rows, previous);
|
|
39947
|
+
const old = new Map(then.rows.map((row) => [row.key, row]));
|
|
39948
|
+
const seen = new Set(now.rows.map((row) => row.key));
|
|
39949
|
+
const rows = now.rows.map((row) => ({
|
|
39950
|
+
[input.dimension]: row.key,
|
|
39951
|
+
clicks: row.clicks,
|
|
39952
|
+
impressions: row.impressions,
|
|
39953
|
+
position: row.position,
|
|
39954
|
+
previous_clicks: old.get(row.key)?.clicks ?? 0,
|
|
39955
|
+
clicks_change: row.clicks - (old.get(row.key)?.clicks ?? 0)
|
|
39956
|
+
}));
|
|
39957
|
+
for (const row of then.rows) {
|
|
39958
|
+
if (seen.has(row.key)) continue;
|
|
39959
|
+
rows.push({
|
|
39960
|
+
[input.dimension]: row.key,
|
|
39961
|
+
clicks: 0,
|
|
39962
|
+
impressions: 0,
|
|
39963
|
+
position: null,
|
|
39964
|
+
previous_clicks: row.clicks,
|
|
39965
|
+
clicks_change: -row.clicks
|
|
39966
|
+
});
|
|
39967
|
+
}
|
|
39968
|
+
const notes = [
|
|
39969
|
+
`Bing weeks counted: ${now.weeks.join(", ") || "none"}; earlier period ${then.weeks.join(", ") || "none"}. Each date names a week.`,
|
|
39970
|
+
...BING_LIMITS.slice(1),
|
|
39971
|
+
...all.invalidRows ? [`${all.invalidRows} Bing rows were not valid and were left out.`] : []
|
|
39972
|
+
];
|
|
39973
|
+
return {
|
|
39974
|
+
text: `${rowsText({
|
|
39975
|
+
title: `Bing Webmaster ${source.site}, ${range.startDate} to ${range.endDate}, by ${input.dimension}.`,
|
|
39976
|
+
rows,
|
|
39977
|
+
offset: input.offset ?? 0,
|
|
39978
|
+
limit: 50,
|
|
39979
|
+
detail: input.detail ?? "summary",
|
|
39980
|
+
notes,
|
|
39981
|
+
origin: originOf(source.site),
|
|
39982
|
+
more: (offset) => deps.call("data", {
|
|
39983
|
+
site: input.site,
|
|
39984
|
+
source: "bing",
|
|
39985
|
+
dimensions: input.dimension,
|
|
39986
|
+
detail: "rows",
|
|
39987
|
+
offset
|
|
39988
|
+
})
|
|
39989
|
+
})}\n\n${FACTS_ONLY.replace("Search Console", "Bing Webmaster Tools")}`,
|
|
39990
|
+
data: {
|
|
39991
|
+
bingSite: source.site,
|
|
39992
|
+
range,
|
|
39993
|
+
comparisonRange: previous,
|
|
39994
|
+
weeks: now.weeks,
|
|
39995
|
+
total: rows.length,
|
|
39996
|
+
offset: input.offset ?? 0,
|
|
39997
|
+
rows: rows.slice(input.offset ?? 0, (input.offset ?? 0) + 50)
|
|
39998
|
+
}
|
|
39999
|
+
};
|
|
40000
|
+
}
|
|
40001
|
+
//#endregion
|
|
39672
40002
|
//#region packages/core/dist/commands/domain-rating.js
|
|
39673
40003
|
const DR_ATTRIBUTION = "domain_rating: Domain Rating by Ahrefs (https://ahrefs.com/).";
|
|
39674
40004
|
const ENDPOINT = "https://api.ahrefs.com/v3/public/domain-rating-free";
|
|
@@ -39856,7 +40186,8 @@ const SECTIONS = {
|
|
|
39856
40186
|
"second-page": "Pages with an average position of 11 to 20 and their queries.",
|
|
39857
40187
|
"content-decay": "Query and page pairs that lost clicks against the previous period.",
|
|
39858
40188
|
cannibalization: "Queries where more than one page of the site gets impressions, with each page’s share.",
|
|
39859
|
-
"traffic-drop": "Daily clicks and impressions checked for sudden changes in the last days, with Google updates whose dates overlap. Overlap is timing only, not cause."
|
|
40189
|
+
"traffic-drop": "Daily clicks and impressions checked for sudden changes in the last days, with Google updates whose dates overlap. Overlap is timing only, not cause.",
|
|
40190
|
+
bing: "Bing’s top queries from Bing Webmaster Tools with Google’s clicks, impressions and position for the same query beside them, and the same for pages."
|
|
39860
40191
|
};
|
|
39861
40192
|
const SECTION_IDS = Object.keys(SECTIONS);
|
|
39862
40193
|
const PAGE_CHECKS = /* @__PURE__ */ new Set([
|
|
@@ -39875,7 +40206,8 @@ const DEFAULT_DAYS = {
|
|
|
39875
40206
|
"second-page": 28,
|
|
39876
40207
|
"content-decay": 28,
|
|
39877
40208
|
cannibalization: 28,
|
|
39878
|
-
"traffic-drop": 90
|
|
40209
|
+
"traffic-drop": 90,
|
|
40210
|
+
bing: 28
|
|
39879
40211
|
};
|
|
39880
40212
|
function nextFor(section, call) {
|
|
39881
40213
|
const section_ = (id) => `section "${id}"`;
|
|
@@ -39887,7 +40219,8 @@ function nextFor(section, call) {
|
|
|
39887
40219
|
"second-page": `Next: ${call("page")} for one page’s queries and live facts.`,
|
|
39888
40220
|
"content-decay": `Next: ${call("index")} for pages that lost most of their clicks; ${call("page")} to see which queries moved.`,
|
|
39889
40221
|
cannibalization: `Next: ${call("page")} on each competing URL to see Google’s chosen canonical.`,
|
|
39890
|
-
"traffic-drop": `Next: ${call("compare")} with the date the drop started, for the pages and queries that moved
|
|
40222
|
+
"traffic-drop": `Next: ${call("compare")} with the date the drop started, for the pages and queries that moved.`,
|
|
40223
|
+
bing: `Next: ${call("data", { source: "bing" })} for every Bing row; ${call("page")} for one page.`
|
|
39891
40224
|
}[section];
|
|
39892
40225
|
}
|
|
39893
40226
|
async function siteOverview(read, site, range, before, pageContains) {
|
|
@@ -40029,7 +40362,22 @@ function overviewText(report, heading, origin, rowsPerList) {
|
|
|
40029
40362
|
`Limits:\n${report.caveats.map((line) => `- ${line}`).join("\n")}`
|
|
40030
40363
|
].join("\n\n");
|
|
40031
40364
|
}
|
|
40032
|
-
|
|
40365
|
+
function once(make) {
|
|
40366
|
+
let made;
|
|
40367
|
+
return () => {
|
|
40368
|
+
made ??= make();
|
|
40369
|
+
return made;
|
|
40370
|
+
};
|
|
40371
|
+
}
|
|
40372
|
+
async function enginesLine(overview, bing) {
|
|
40373
|
+
const summary = overview.summary;
|
|
40374
|
+
try {
|
|
40375
|
+
return enginesText(summary, await bingTotals(await bing(), overview.range, overview.comparisonRange));
|
|
40376
|
+
} catch (error) {
|
|
40377
|
+
return `Bing: could not be read. ${error instanceof Error ? error.message : String(error)}`;
|
|
40378
|
+
}
|
|
40379
|
+
}
|
|
40380
|
+
async function build(read, site, section, range, now, bing) {
|
|
40033
40381
|
const dependencies = {
|
|
40034
40382
|
searchAnalytics: read,
|
|
40035
40383
|
now
|
|
@@ -40088,7 +40436,8 @@ async function build(read, site, section, range, now) {
|
|
|
40088
40436
|
end: first.comparisonEnd
|
|
40089
40437
|
})
|
|
40090
40438
|
};
|
|
40091
|
-
}
|
|
40439
|
+
},
|
|
40440
|
+
bing: async () => bingSection(await bing(), read, site, range)
|
|
40092
40441
|
}[section];
|
|
40093
40442
|
return factsOnly(await run());
|
|
40094
40443
|
}
|
|
@@ -40132,13 +40481,16 @@ async function allSections(input, deps) {
|
|
|
40132
40481
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
40133
40482
|
const read = sharedReader(deps.searchAnalytics);
|
|
40134
40483
|
const origin = originOf(input.site);
|
|
40135
|
-
const
|
|
40484
|
+
const bing = once(() => bingFor(deps, input.site));
|
|
40485
|
+
const ids = SECTION_IDS.filter((id) => id !== "bing" || deps.bing);
|
|
40486
|
+
const [overview, ...rest] = await Promise.all(ids.map((id) => build(read, input.site, id, dateWindow(input, DEFAULT_DAYS[id]), now, bing).catch((error) => error instanceof Error ? error : new Error(String(error)))));
|
|
40136
40487
|
if (!overview || overview instanceof Error) throw overview ?? /* @__PURE__ */ new Error("The overview did not run.");
|
|
40137
40488
|
const parts = [overviewText(overview, `Report on ${input.site}`, origin, 5)];
|
|
40489
|
+
if (deps.bing) parts.push(await enginesLine(overview, bing));
|
|
40138
40490
|
const brand = rest.find((run) => !(run instanceof Error) && brandNote(run) !== null);
|
|
40139
40491
|
if (brand) parts.push(brandNote(brand));
|
|
40140
40492
|
const sections = {};
|
|
40141
|
-
|
|
40493
|
+
ids.slice(1).forEach((id, index) => {
|
|
40142
40494
|
const run = rest[index];
|
|
40143
40495
|
if (!run || run instanceof Error) {
|
|
40144
40496
|
const message = run?.message ?? "did not run";
|
|
@@ -40158,7 +40510,7 @@ async function allSections(input, deps) {
|
|
|
40158
40510
|
parts.push(`More: ${deps.call("report", {
|
|
40159
40511
|
site: input.site,
|
|
40160
40512
|
section: "<section>"
|
|
40161
|
-
})} for its rows (${
|
|
40513
|
+
})} for its rows (${ids.join(", ")}). Page sections also check their top 20 URLs live and merge same-site redirects.`);
|
|
40162
40514
|
return {
|
|
40163
40515
|
text: `${parts.join("\n\n")}\n\n${FACTS_ONLY}`,
|
|
40164
40516
|
data: {
|
|
@@ -40174,10 +40526,11 @@ async function oneSection(input, deps) {
|
|
|
40174
40526
|
const detail = input.detail ?? "summary";
|
|
40175
40527
|
const range = dateWindow(input, DEFAULT_DAYS[section]);
|
|
40176
40528
|
const origin = originOf(site);
|
|
40177
|
-
|
|
40529
|
+
const bing = once(() => bingFor(deps, site));
|
|
40530
|
+
let report = await build(sharedReader(deps.searchAnalytics), site, section, range, now, bing);
|
|
40178
40531
|
const next = nextFor(section, deps.call);
|
|
40179
40532
|
if (section === "overview" && detail !== "full") return {
|
|
40180
|
-
text: `${overviewText(report, `overview of ${site}`, origin, 10)}\n\
|
|
40533
|
+
text: `${overviewText(report, `overview of ${site}`, origin, 10)}\n\n${deps.bing ? `${await enginesLine(report, bing)}\n\n` : ""}More: ${deps.call("report", {
|
|
40181
40534
|
site,
|
|
40182
40535
|
section,
|
|
40183
40536
|
detail: "full"
|
|
@@ -40196,7 +40549,7 @@ async function oneSection(input, deps) {
|
|
|
40196
40549
|
redirects.set(page.url, page.redirects_to.replace(/#.*$/, ""));
|
|
40197
40550
|
merged.push(`${page.url} → ${page.redirects_to}`);
|
|
40198
40551
|
}
|
|
40199
|
-
if (redirects.size) report = await build(sharedReader(deps.searchAnalytics, redirects), site, section, range, now);
|
|
40552
|
+
if (redirects.size) report = await build(sharedReader(deps.searchAnalytics, redirects), site, section, range, now, bing);
|
|
40200
40553
|
report.pageChecks = checked;
|
|
40201
40554
|
}
|
|
40202
40555
|
const notes = [];
|
|
@@ -40224,8 +40577,9 @@ async function oneSection(input, deps) {
|
|
|
40224
40577
|
offset
|
|
40225
40578
|
})
|
|
40226
40579
|
});
|
|
40580
|
+
const facts = section === "bing" ? FACTS_ONLY.replace("Search Console", "Search Console and Bing") : FACTS_ONLY;
|
|
40227
40581
|
return {
|
|
40228
|
-
text: `${shown.text}\n\n${next}\n\n${
|
|
40582
|
+
text: `${shown.text}\n\n${next}\n\n${facts}`,
|
|
40229
40583
|
data: detail === "full" ? report : shown.data
|
|
40230
40584
|
};
|
|
40231
40585
|
}
|
|
@@ -40671,29 +41025,67 @@ async function readableSites(google, signIn) {
|
|
|
40671
41025
|
permissionLevel: entry.permissionLevel ?? ""
|
|
40672
41026
|
})).sort((a, b) => a.siteUrl.localeCompare(b.siteUrl));
|
|
40673
41027
|
}
|
|
40674
|
-
|
|
40675
|
-
|
|
40676
|
-
|
|
41028
|
+
const hostOf = (value) => {
|
|
41029
|
+
try {
|
|
41030
|
+
return new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/, "")}`).hostname.replace(/^www\./, "");
|
|
41031
|
+
} catch {
|
|
41032
|
+
return value;
|
|
41033
|
+
}
|
|
41034
|
+
};
|
|
41035
|
+
async function runSites(input) {
|
|
41036
|
+
const named = input.accounts.length > 1;
|
|
41037
|
+
const [google, bing] = await Promise.all([Promise.all(input.accounts.map(async (account) => {
|
|
41038
|
+
const [properties, analytics] = await Promise.all([readableSites(account.google, input.signIn).catch((error) => error instanceof Error ? error : new Error(String(error))), listAnalyticsAccounts(account.google, { signIn: input.signIn }).catch(() => null)]);
|
|
41039
|
+
return {
|
|
41040
|
+
email: account.email,
|
|
41041
|
+
properties,
|
|
41042
|
+
analytics
|
|
41043
|
+
};
|
|
41044
|
+
})), input.bing ? listBingSites(input.bing).catch((error) => error instanceof Error ? error : new Error(String(error))) : Promise.resolve(null)]);
|
|
41045
|
+
const bingSites = Array.isArray(bing) ? bing.filter((site) => site.verified) : [];
|
|
41046
|
+
const bingByHost = new Map(bingSites.map((site) => [hostOf(site.url), site]));
|
|
41047
|
+
const properties = google.flatMap((account) => account.properties instanceof Error ? [] : account.properties.map((property) => ({
|
|
41048
|
+
...property,
|
|
41049
|
+
...named ? { account: account.email ?? "" } : {},
|
|
41050
|
+
...Array.isArray(bing) ? { bing: bingByHost.get(hostOf(property.siteUrl))?.url ?? "" } : {}
|
|
41051
|
+
})));
|
|
41052
|
+
const googleHosts = new Set(properties.map((property) => hostOf(property.siteUrl)));
|
|
41053
|
+
const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf(site.url)));
|
|
41054
|
+
const failed = google.filter((account) => account.properties instanceof Error);
|
|
41055
|
+
const analytics = google.flatMap((account) => (account.analytics ?? []).flatMap((summary) => summary.propertySummaries.map((property) => ({
|
|
40677
41056
|
property: property.property.replace("properties/", ""),
|
|
40678
41057
|
name: property.displayName ?? "",
|
|
40679
|
-
account:
|
|
40680
|
-
|
|
40681
|
-
|
|
41058
|
+
account: summary.displayName ?? "",
|
|
41059
|
+
...named ? { signIn: account.email ?? "" } : {}
|
|
41060
|
+
}))));
|
|
41061
|
+
const research = [["dataforseo", "x-dataforseo-login"], ["ahrefs", "x-ahrefs-key"]].filter(([, name]) => input.keys.get(name ?? "")).map(([provider]) => provider);
|
|
41062
|
+
const columns = [
|
|
41063
|
+
"siteUrl",
|
|
41064
|
+
"permissionLevel",
|
|
41065
|
+
...named ? ["account"] : [],
|
|
41066
|
+
...Array.isArray(bing) ? ["bing"] : []
|
|
41067
|
+
];
|
|
40682
41068
|
return {
|
|
40683
41069
|
text: [
|
|
41070
|
+
named ? `Google accounts: ${input.accounts.map((account) => account.email).join(", ")}.` : null,
|
|
40684
41071
|
`Search Console properties (${properties.length}):`,
|
|
40685
|
-
properties.length ? table(properties,
|
|
40686
|
-
|
|
41072
|
+
properties.length ? table(properties, columns, null) : "None. These Google sign-ins have no Search Console access, so the reports cannot run.",
|
|
41073
|
+
...failed.map((account) => `${account.email ?? "Google"}: could not list properties. ${account.properties.message}`),
|
|
41074
|
+
analytics.length ? `Google Analytics properties (${analytics.length}):\n${table(analytics, [
|
|
40687
41075
|
"property",
|
|
40688
41076
|
"name",
|
|
40689
|
-
"account"
|
|
40690
|
-
|
|
41077
|
+
"account",
|
|
41078
|
+
...named ? ["signIn"] : []
|
|
41079
|
+
], null)}` : "Google Analytics: no properties readable with these sign-ins.",
|
|
41080
|
+
input.bing === void 0 ? null : bing === null ? "Bing Webmaster: not connected." : bing instanceof Error ? `Bing Webmaster: could not list sites. ${bing.message}` : bingOnly.length ? `Bing Webmaster sites with no Search Console property here (${bingOnly.length}): ${bingOnly.map((site) => site.url).join(", ")}.` : `Bing Webmaster: ${bingSites.length} verified sites; the bing column shows each one.`,
|
|
40691
41081
|
`Research keys: ${research.length ? research.join(", ") : "none"}.`,
|
|
41082
|
+
`Add a Google account${input.bing === void 0 ? "" : ", Bing"} or a key: ${input.connect}`,
|
|
40692
41083
|
"Next: seo_report with a site."
|
|
40693
|
-
].join("\n\n"),
|
|
41084
|
+
].filter(Boolean).join("\n\n"),
|
|
40694
41085
|
data: {
|
|
40695
41086
|
searchConsole: properties,
|
|
40696
41087
|
googleAnalytics: analytics,
|
|
41088
|
+
bing: Array.isArray(bing) ? bingSites : null,
|
|
40697
41089
|
research
|
|
40698
41090
|
}
|
|
40699
41091
|
};
|
|
@@ -40743,7 +41135,11 @@ const detail = {
|
|
|
40743
41135
|
]).default("summary").describe("summary first. rows: the next page of rows. full: every field as JSON."),
|
|
40744
41136
|
offset: z.number().int().min(0).default(0).describe("rows only: the first row to return.")
|
|
40745
41137
|
};
|
|
40746
|
-
const DATA_SOURCES = [
|
|
41138
|
+
const DATA_SOURCES = [
|
|
41139
|
+
"search_console",
|
|
41140
|
+
"google_analytics",
|
|
41141
|
+
"bing"
|
|
41142
|
+
];
|
|
40747
41143
|
const LOCAL_ANALYTICS = "Clicky and other analytics run on the person’s machine with the CLI and a provider package: `npx -y seo providers install @seoskill/clicky-provider`, connect it once with `seo providers connect clicky`, then `npx -y seo analytics clicky report --json`. `seo providers list` shows installed providers.";
|
|
40748
41144
|
function tool(spec) {
|
|
40749
41145
|
return spec;
|
|
@@ -40753,9 +41149,15 @@ const SHARED_TOOLS = [
|
|
|
40753
41149
|
name: "seo_sites",
|
|
40754
41150
|
command: "data",
|
|
40755
41151
|
title: "List sites",
|
|
40756
|
-
description: "Lists the Search Console and Google Analytics properties
|
|
41152
|
+
description: "Lists the Search Console and Google Analytics properties the connected Google accounts can read, the Bing Webmaster site for each, and which research keys are set. Call it first.",
|
|
40757
41153
|
input: z.object({}),
|
|
40758
|
-
run: async (surface) => runSites(
|
|
41154
|
+
run: async (surface) => runSites({
|
|
41155
|
+
accounts: await surface.accounts(),
|
|
41156
|
+
keys: await surface.keys(),
|
|
41157
|
+
signIn: surface.signIn,
|
|
41158
|
+
bing: surface.deps.bing ? await surface.deps.bing() : void 0,
|
|
41159
|
+
connect: surface.connect
|
|
41160
|
+
})
|
|
40759
41161
|
}),
|
|
40760
41162
|
tool({
|
|
40761
41163
|
name: "seo_report",
|
|
@@ -40849,24 +41251,32 @@ const SHARED_TOOLS = [
|
|
|
40849
41251
|
name: "seo_data",
|
|
40850
41252
|
command: "data",
|
|
40851
41253
|
title: "Raw rows",
|
|
40852
|
-
description: `Raw rows for questions the report does not answer. search_console (default): rows for any dimensions, each with the previous period and the change; page and query narrow to rows whose URL or query contains that text. google_analytics: landing pages for visits from search, with Google Analytics’ own metric names. ${LOCAL_ANALYTICS}`,
|
|
41254
|
+
description: `Raw rows for questions the report does not answer. search_console (default): rows for any dimensions, each with the previous period and the change; page and query narrow to rows whose URL or query contains that text. google_analytics: landing pages for visits from search, with Google Analytics’ own metric names. bing: Bing Webmaster query or page rows (first dimension) for the same site, by week. ${LOCAL_ANALYTICS}`,
|
|
40853
41255
|
input: z.object({
|
|
40854
41256
|
source: z.enum(DATA_SOURCES).default("search_console"),
|
|
40855
|
-
site: z.string().min(1).describe("search_console: the property or site. google_analytics: numeric property id from seo_sites."),
|
|
40856
|
-
dimensions: z.array(z.enum(DIMENSIONS)).max(4).default(["query"]).describe("search_console
|
|
41257
|
+
site: z.string().min(1).describe("search_console and bing: the property or site. google_analytics: numeric property id from seo_sites."),
|
|
41258
|
+
dimensions: z.array(z.enum(DIMENSIONS)).max(4).default(["query"]).describe("search_console; bing uses the first, query or page."),
|
|
40857
41259
|
page: z.string().min(1).optional().describe("search_console only: URL text, such as /blog/."),
|
|
40858
41260
|
query: z.string().min(1).optional().describe("search_console only: query text."),
|
|
40859
41261
|
...dates,
|
|
40860
41262
|
...detail
|
|
40861
41263
|
}),
|
|
40862
41264
|
run: async (surface, input) => {
|
|
41265
|
+
if (input.source === "bing") return runBingRows({
|
|
41266
|
+
site: input.site,
|
|
41267
|
+
dimension: input.dimensions[0] === "page" ? "page" : "query",
|
|
41268
|
+
startDate: input.start_date,
|
|
41269
|
+
endDate: input.end_date,
|
|
41270
|
+
detail: input.detail,
|
|
41271
|
+
offset: input.offset
|
|
41272
|
+
}, surface.deps);
|
|
40863
41273
|
if (input.source === "google_analytics") return runAnalyticsRows({
|
|
40864
41274
|
property: input.site,
|
|
40865
41275
|
startDate: input.start_date,
|
|
40866
41276
|
endDate: input.end_date,
|
|
40867
41277
|
detail: input.detail,
|
|
40868
41278
|
offset: input.offset
|
|
40869
|
-
}, await surface.
|
|
41279
|
+
}, await analyticsSignIn(await surface.accounts(), input.site, surface.signIn), {
|
|
40870
41280
|
signIn: surface.signIn,
|
|
40871
41281
|
call: (offset) => `seo_data with source "google_analytics", site "${input.site}", detail "rows", offset ${offset}`
|
|
40872
41282
|
});
|
|
@@ -40895,8 +41305,8 @@ async function runSharedTool(tool, surface, input) {
|
|
|
40895
41305
|
if (!parsed.success) throw new CommandError("INVALID_INPUT", parsed.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "));
|
|
40896
41306
|
return tool.run(surface, parsed.data);
|
|
40897
41307
|
}
|
|
40898
|
-
const MCP_INSTRUCTIONS = `SEO Skill reads the person's own Search Console, Google Analytics and optional keyword and analytics accounts, and returns facts with their dates and limits. It gives no advice.
|
|
40899
|
-
1. seo_sites: the properties
|
|
41308
|
+
const MCP_INSTRUCTIONS = `SEO Skill reads the person's own Search Console, Google Analytics, Bing Webmaster Tools and optional keyword and analytics accounts, and returns facts with their dates and limits. It gives no advice.
|
|
41309
|
+
1. seo_sites: the properties the connected accounts can read.
|
|
40900
41310
|
2. seo_report: the main report. Run it without a section first; it shows the overview and every section's count and top rows. Then ask for the one section you need.
|
|
40901
41311
|
3. seo_page for one URL, seo_compare for before and after a date, seo_index for pages that lost their clicks, seo_research for keyword, competitor and backlink data from the person's own DataForSEO or Ahrefs key, seo_data for raw rows.
|
|
40902
41312
|
Show the person the facts that matter, for example in a table or a small dashboard, and decide with them what to change. Do not edit pages without their approval.`;
|
|
@@ -40942,13 +41352,26 @@ function createApiKeyCredentialStore(input) {
|
|
|
40942
41352
|
//#region packages/core/dist/providers/ahrefs/credentials.js
|
|
40943
41353
|
const AHREFS_API_KEY_ENV = "SEO_AHREFS_API_KEY";
|
|
40944
41354
|
const AHREFS_API_KEY_SECRET = PROVIDER_SECRET_NAMES.ahrefsApiKey;
|
|
40945
|
-
const store = createApiKeyCredentialStore({
|
|
41355
|
+
const store$1 = createApiKeyCredentialStore({
|
|
40946
41356
|
provider: "ahrefs",
|
|
40947
41357
|
environmentVariable: AHREFS_API_KEY_ENV,
|
|
40948
41358
|
secretName: AHREFS_API_KEY_SECRET,
|
|
40949
41359
|
invalidMessage: "Ahrefs needs a valid API v3 key."
|
|
40950
41360
|
});
|
|
40951
41361
|
async function readAhrefsApiKey(input = {}) {
|
|
41362
|
+
return store$1.read(input);
|
|
41363
|
+
}
|
|
41364
|
+
//#endregion
|
|
41365
|
+
//#region packages/core/dist/providers/bing/credentials.js
|
|
41366
|
+
const BING_API_KEY_ENV = "SEO_BING_API_KEY";
|
|
41367
|
+
const BING_API_KEY_SECRET = PROVIDER_SECRET_NAMES.bingApiKey;
|
|
41368
|
+
const store = createApiKeyCredentialStore({
|
|
41369
|
+
provider: "bing",
|
|
41370
|
+
environmentVariable: BING_API_KEY_ENV,
|
|
41371
|
+
secretName: BING_API_KEY_SECRET,
|
|
41372
|
+
invalidMessage: "Bing Webmaster needs a valid API key."
|
|
41373
|
+
});
|
|
41374
|
+
async function readBingApiKey(input = {}) {
|
|
40952
41375
|
return store.read(input);
|
|
40953
41376
|
}
|
|
40954
41377
|
//#endregion
|
|
@@ -41083,9 +41506,17 @@ function nodeCommandDeps(options) {
|
|
|
41083
41506
|
return null;
|
|
41084
41507
|
}
|
|
41085
41508
|
},
|
|
41086
|
-
call: options.call
|
|
41509
|
+
call: options.call,
|
|
41510
|
+
bing: localBing,
|
|
41511
|
+
bingConnect: LOCAL_BING_CONNECT
|
|
41087
41512
|
};
|
|
41088
41513
|
}
|
|
41514
|
+
const LOCAL_BING_CONNECT = "Run `seo providers bing connect` with an API key from Bing Webmaster Tools (Settings, API access).";
|
|
41515
|
+
let bing;
|
|
41516
|
+
function localBing() {
|
|
41517
|
+
bing ??= readBingApiKey().catch(() => void 0).then((key) => key ? bingCall({ apiKey: key.apiKey }, { signIn: LOCAL_BING_CONNECT }) : null);
|
|
41518
|
+
return bing;
|
|
41519
|
+
}
|
|
41089
41520
|
async function localResearchKeys() {
|
|
41090
41521
|
const [dataForSeo, ahrefs] = await Promise.all([readDataForSeoCredentials().catch(() => void 0), readAhrefsApiKey().catch(() => void 0)]);
|
|
41091
41522
|
const keys = new Headers();
|
|
@@ -45513,8 +45944,12 @@ function registerSharedTools(server, options = {}) {
|
|
|
45513
45944
|
resolveSite: (site) => resolveLocalSite(site),
|
|
45514
45945
|
keys: localResearchKeys,
|
|
45515
45946
|
drKey: null,
|
|
45516
|
-
|
|
45517
|
-
|
|
45947
|
+
accounts: async () => [{
|
|
45948
|
+
email: null,
|
|
45949
|
+
google: await localGoogleFetch()
|
|
45950
|
+
}],
|
|
45951
|
+
signIn: LOCAL_SIGN_IN.signIn,
|
|
45952
|
+
connect: "run `seo auth login` for Google, `seo providers bing connect` for Bing, and `seo providers dataforseo connect` or `seo providers ahrefs connect` for keys."
|
|
45518
45953
|
};
|
|
45519
45954
|
for (const shared of SHARED_TOOLS) server.registerTool(shared.name, {
|
|
45520
45955
|
title: shared.title,
|