seo 0.13.1 → 0.13.2
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 +2 -2
- package/dist/{dist-rmavn5h6.js → dist-7TxFytBp.js} +54 -54
- package/dist/dist-7TxFytBp.js.map +1 -0
- package/dist/{dist-3A3Vzp7r.js → dist-BBszRLUG.js} +1 -1
- package/dist/{esm-BIXGSxFJ.js → esm-Dmtr8qrP.js} +2 -2
- package/dist/{esm-BIXGSxFJ.js.map → esm-Dmtr8qrP.js.map} +1 -1
- package/dist/index.d.ts +45 -45
- package/dist/index.js +79 -49
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +79 -49
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/dist-rmavn5h6.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 version, r as __commonJSMin, t as load } from "./esm-
|
|
2
|
+
import { i as __toESM, n as version, r as __commonJSMin, t as load } from "./esm-Dmtr8qrP.js";
|
|
3
3
|
import { createHash, randomUUID, sign } from "node:crypto";
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import { arch, homedir, platform, tmpdir, totalmem } from "node:os";
|
|
@@ -39546,25 +39546,64 @@ function need(headers, provider) {
|
|
|
39546
39546
|
if (values.some((value) => !value)) throw new CommandError("NO_KEY", `No ${provider} key. Hosted MCP: save it at https://seoskill.dev/account#keys. CLI and local MCP: run \`seo login\` to use the account's keys, or \`seo providers ${provider} connect\`.`);
|
|
39547
39547
|
return values;
|
|
39548
39548
|
}
|
|
39549
|
+
const TASK_TIMEOUT_MS = 6e4;
|
|
39550
|
+
const READ_TIMEOUT_MS = 3e4;
|
|
39551
|
+
const RETRY_DELAY_MS = 250;
|
|
39552
|
+
const retryStatus = (status) => status === 429 || status >= 500;
|
|
39549
39553
|
async function call(provider, request) {
|
|
39550
|
-
const
|
|
39551
|
-
|
|
39552
|
-
|
|
39553
|
-
|
|
39554
|
+
const read = request.method === "GET";
|
|
39555
|
+
const timeoutMs = read ? READ_TIMEOUT_MS : TASK_TIMEOUT_MS;
|
|
39556
|
+
for (let attempt = 1;; attempt += 1) {
|
|
39557
|
+
const last = attempt === 2;
|
|
39558
|
+
let response;
|
|
39559
|
+
try {
|
|
39560
|
+
response = await fetch(request.clone(), { signal: AbortSignal.timeout(timeoutMs) });
|
|
39561
|
+
} catch (error) {
|
|
39562
|
+
if (read && !last) {
|
|
39563
|
+
await wait(RETRY_DELAY_MS);
|
|
39564
|
+
continue;
|
|
39565
|
+
}
|
|
39566
|
+
throw new CommandError("PROVIDER_ERROR", error.name === "TimeoutError" ? `${provider} did not answer within ${timeoutMs / 1e3} seconds.` : `${provider} could not be reached: ${error.message}`);
|
|
39567
|
+
}
|
|
39568
|
+
if (response.ok) return response;
|
|
39569
|
+
if (retryStatus(response.status) && !last) {
|
|
39570
|
+
await response.body?.cancel();
|
|
39571
|
+
await wait(RETRY_DELAY_MS);
|
|
39572
|
+
continue;
|
|
39573
|
+
}
|
|
39574
|
+
const text = (await response.text()).slice(0, 300);
|
|
39575
|
+
throw new CommandError(response.status === 401 || response.status === 403 ? "NO_KEY" : response.status === 429 ? "RATE_LIMITED" : "PROVIDER_ERROR", `${provider} answered ${response.status}. ${text}`);
|
|
39576
|
+
}
|
|
39554
39577
|
}
|
|
39578
|
+
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
39555
39579
|
function requireInput(input) {
|
|
39556
39580
|
const needsKeywords = input.action === "keyword_metrics" || input.action === "serp";
|
|
39557
39581
|
if (needsKeywords && input.keywords.length === 0) throw new CommandError("INVALID_INPUT", `${input.action} needs keywords.`);
|
|
39558
39582
|
if (!needsKeywords && !input.target) throw new CommandError("INVALID_INPUT", `${input.action} needs a target domain or URL.`);
|
|
39559
39583
|
}
|
|
39584
|
+
async function eachKeyword(keywords, missed, rowsFor) {
|
|
39585
|
+
const rows = [];
|
|
39586
|
+
let failure;
|
|
39587
|
+
for (const keyword of keywords) try {
|
|
39588
|
+
rows.push(...await rowsFor(keyword));
|
|
39589
|
+
} catch (error) {
|
|
39590
|
+
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
39591
|
+
failure ??= error;
|
|
39592
|
+
missed.push(keyword);
|
|
39593
|
+
}
|
|
39594
|
+
if (missed.length === keywords.length) throw failure;
|
|
39595
|
+
return rows;
|
|
39596
|
+
}
|
|
39560
39597
|
async function research(provider, headers, input) {
|
|
39561
39598
|
requireInput(input);
|
|
39562
39599
|
const keys = need(headers, provider);
|
|
39563
39600
|
if (provider === "dataforseo") return dataForSeo(keys, input);
|
|
39601
|
+
const missed = [];
|
|
39564
39602
|
return {
|
|
39565
|
-
rows: await ahrefs(keys[0] ?? "", input),
|
|
39603
|
+
rows: await ahrefs(keys[0] ?? "", input, missed),
|
|
39566
39604
|
costUsd: null,
|
|
39567
|
-
partial: false
|
|
39605
|
+
partial: false,
|
|
39606
|
+
missed
|
|
39568
39607
|
};
|
|
39569
39608
|
}
|
|
39570
39609
|
function countryName(code) {
|
|
@@ -39630,12 +39669,14 @@ const PARTIAL = 40106;
|
|
|
39630
39669
|
async function dataForSeo(keys, input) {
|
|
39631
39670
|
const spent = {
|
|
39632
39671
|
cost: 0,
|
|
39633
|
-
partial: false
|
|
39672
|
+
partial: false,
|
|
39673
|
+
missed: []
|
|
39634
39674
|
};
|
|
39635
39675
|
return {
|
|
39636
39676
|
rows: await dataForSeoRows(keys, input, spent),
|
|
39637
39677
|
costUsd: Math.round(spent.cost * 1e4) / 1e4,
|
|
39638
|
-
partial: spent.partial
|
|
39678
|
+
partial: spent.partial,
|
|
39679
|
+
missed: spent.missed
|
|
39639
39680
|
};
|
|
39640
39681
|
}
|
|
39641
39682
|
async function dataForSeoRows(keys, input, spent) {
|
|
@@ -39677,28 +39718,20 @@ async function dataForSeoRows(keys, input, spent) {
|
|
|
39677
39718
|
keyword_difficulty: "keyword_data.keyword_properties.keyword_difficulty",
|
|
39678
39719
|
main_intent: "keyword_data.search_intent_info.main_intent"
|
|
39679
39720
|
}));
|
|
39680
|
-
if (input.action === "serp") {
|
|
39681
|
-
|
|
39682
|
-
|
|
39683
|
-
|
|
39684
|
-
|
|
39685
|
-
|
|
39686
|
-
|
|
39687
|
-
|
|
39688
|
-
|
|
39689
|
-
|
|
39690
|
-
|
|
39691
|
-
|
|
39692
|
-
|
|
39693
|
-
|
|
39694
|
-
domain: "domain",
|
|
39695
|
-
title: "title"
|
|
39696
|
-
})
|
|
39697
|
-
});
|
|
39698
|
-
}
|
|
39699
|
-
}
|
|
39700
|
-
return rows;
|
|
39701
|
-
}
|
|
39721
|
+
if (input.action === "serp") return eachKeyword(input.keywords.slice(0, 5), spent.missed, async (keyword) => {
|
|
39722
|
+
return (await post("v3/serp/google/organic/live/advanced", {
|
|
39723
|
+
keyword,
|
|
39724
|
+
depth: Math.min(input.limit, 100)
|
|
39725
|
+
})).filter((item) => item.type === "organic").map((item) => ({
|
|
39726
|
+
keyword,
|
|
39727
|
+
...pick(item, {
|
|
39728
|
+
position: "rank_absolute",
|
|
39729
|
+
url: "url",
|
|
39730
|
+
domain: "domain",
|
|
39731
|
+
title: "title"
|
|
39732
|
+
})
|
|
39733
|
+
}));
|
|
39734
|
+
});
|
|
39702
39735
|
if (input.action === "backlinks") return (await post("v3/backlinks/referring_domains/live", {
|
|
39703
39736
|
target: bareTarget(input.target),
|
|
39704
39737
|
limit: input.limit,
|
|
@@ -39727,7 +39760,7 @@ function isPage(target) {
|
|
|
39727
39760
|
function bareTarget(target) {
|
|
39728
39761
|
return target.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
39729
39762
|
}
|
|
39730
|
-
async function ahrefs(key, input) {
|
|
39763
|
+
async function ahrefs(key, input, missed) {
|
|
39731
39764
|
const get = async (path, params) => {
|
|
39732
39765
|
const url = new URL(`https://api.ahrefs.com/v3/${path}`);
|
|
39733
39766
|
for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
|
|
@@ -39751,22 +39784,17 @@ async function ahrefs(key, input) {
|
|
|
39751
39784
|
limit: String(input.limit),
|
|
39752
39785
|
order_by: "sum_traffic:desc"
|
|
39753
39786
|
})).keywords ?? [];
|
|
39754
|
-
if (input.action === "serp") {
|
|
39755
|
-
|
|
39756
|
-
|
|
39757
|
-
|
|
39758
|
-
|
|
39759
|
-
|
|
39760
|
-
|
|
39761
|
-
|
|
39762
|
-
|
|
39763
|
-
|
|
39764
|
-
|
|
39765
|
-
...row
|
|
39766
|
-
});
|
|
39767
|
-
}
|
|
39768
|
-
return rows;
|
|
39769
|
-
}
|
|
39787
|
+
if (input.action === "serp") return eachKeyword(input.keywords.slice(0, 5), missed, async (keyword) => {
|
|
39788
|
+
return ((await get("serp-overview/serp-overview", {
|
|
39789
|
+
select: "position,type,url,title,domain_rating,traffic,keywords",
|
|
39790
|
+
keyword,
|
|
39791
|
+
country: input.country,
|
|
39792
|
+
top_positions: String(Math.min(input.limit, 100))
|
|
39793
|
+
})).positions ?? []).map((row) => ({
|
|
39794
|
+
keyword,
|
|
39795
|
+
...row
|
|
39796
|
+
}));
|
|
39797
|
+
});
|
|
39770
39798
|
if (input.action === "backlinks") return (await get("site-explorer/refdomains", {
|
|
39771
39799
|
select: "domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen",
|
|
39772
39800
|
target: input.target,
|
|
@@ -42488,7 +42516,7 @@ async function runResearch(input, keys, options) {
|
|
|
42488
42516
|
const offset = input.offset ?? 0;
|
|
42489
42517
|
const language = input.provider === "dataforseo" ? dataForSeoLanguage(country) : null;
|
|
42490
42518
|
if (input.action === "ai_answers") throw new CommandError("INVALID_INPUT", "ai_answers runs on its own.");
|
|
42491
|
-
const { rows: found, costUsd, partial } = await research(input.provider, keys, {
|
|
42519
|
+
const { rows: found, costUsd, partial, missed } = await research(input.provider, keys, {
|
|
42492
42520
|
action: input.action,
|
|
42493
42521
|
keywords: input.keywords ?? [],
|
|
42494
42522
|
target,
|
|
@@ -42514,6 +42542,7 @@ async function runResearch(input, keys, options) {
|
|
|
42514
42542
|
`${input.provider} estimates, in the provider’s own units.`,
|
|
42515
42543
|
...costUsd !== null ? [`DataForSEO cost: $${costUsd}.`] : [],
|
|
42516
42544
|
...partial ? ["Partial: DataForSEO could not fetch some pages of results, so rows may be missing."] : [],
|
|
42545
|
+
...missed.length ? [`Partial: ${input.provider} failed for ${missed.join(", ")}, so those keywords have no rows. Ask for them again.`] : [],
|
|
42517
42546
|
...note ? [note] : [],
|
|
42518
42547
|
...hasTargetDr ? [`${bareDomain(target)} domain_rating: ${targetDr}.`] : [],
|
|
42519
42548
|
...rated.added || hasTargetDr ? [DR_ATTRIBUTION] : []
|
|
@@ -42531,6 +42560,7 @@ async function runResearch(input, keys, options) {
|
|
|
42531
42560
|
leftOut: note,
|
|
42532
42561
|
costUsd,
|
|
42533
42562
|
partial,
|
|
42563
|
+
missedKeywords: missed,
|
|
42534
42564
|
total: rated.rows.length,
|
|
42535
42565
|
offset,
|
|
42536
42566
|
rows: rated.rows.slice(offset, offset + 50)
|