seo 0.15.1 → 0.15.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 +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/{dist-BnpsvtXl.js → dist-DOJoZZ9W.js} +1 -1
- package/dist/{dist-DoPZlCJq.js → dist-Dpl5hSMM.js} +71 -71
- package/dist/{dist-DoPZlCJq.js.map → dist-Dpl5hSMM.js.map} +1 -1
- package/dist/{esm-D12ZCG_1.js → esm-Dt4JJZ0P.js} +2 -2
- package/dist/{esm-D12ZCG_1.js.map → esm-Dt4JJZ0P.js.map} +1 -1
- package/dist/index.js +245 -92
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +245 -92
- package/dist/mcp.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 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-Dt4JJZ0P.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";
|
|
@@ -14823,7 +14823,7 @@ async function readableSites(google, signIn) {
|
|
|
14823
14823
|
permissionLevel: entry.permissionLevel ?? ""
|
|
14824
14824
|
})).sort((a, b) => a.siteUrl.localeCompare(b.siteUrl));
|
|
14825
14825
|
}
|
|
14826
|
-
const hostOf$
|
|
14826
|
+
const hostOf$3 = (value) => {
|
|
14827
14827
|
try {
|
|
14828
14828
|
return new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/, "")}`).hostname.replace(/^www\./, "");
|
|
14829
14829
|
} catch {
|
|
@@ -14841,14 +14841,14 @@ async function runSites(input) {
|
|
|
14841
14841
|
};
|
|
14842
14842
|
})), input.bing ? listBingSites(input.bing).catch((error) => error instanceof Error ? error : new Error(String(error))) : Promise.resolve(null)]);
|
|
14843
14843
|
const bingSites = Array.isArray(bing) ? bing.filter((site) => site.verified) : [];
|
|
14844
|
-
const bingByHost = new Map(bingSites.map((site) => [hostOf$
|
|
14844
|
+
const bingByHost = new Map(bingSites.map((site) => [hostOf$3(site.url), site]));
|
|
14845
14845
|
const properties = google.flatMap((account) => account.properties instanceof Error ? [] : account.properties.map((property) => ({
|
|
14846
14846
|
...property,
|
|
14847
14847
|
...named ? { account: account.email ?? "" } : {},
|
|
14848
|
-
...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$
|
|
14848
|
+
...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$3(property.siteUrl))?.url ?? "" } : {}
|
|
14849
14849
|
})));
|
|
14850
|
-
const googleHosts = new Set(properties.map((property) => hostOf$
|
|
14851
|
-
const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$
|
|
14850
|
+
const googleHosts = new Set(properties.map((property) => hostOf$3(property.siteUrl)));
|
|
14851
|
+
const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$3(site.url)));
|
|
14852
14852
|
const failed = google.filter((account) => account.properties instanceof Error);
|
|
14853
14853
|
const analytics = google.flatMap((account) => (account.analytics ?? []).flatMap((summary) => summary.propertySummaries.map((property) => ({
|
|
14854
14854
|
property: property.property.replace("properties/", ""),
|
|
@@ -33849,7 +33849,7 @@ function typesafeDecisionModel(input) {
|
|
|
33849
33849
|
const MAX_CONCURRENCY = 32;
|
|
33850
33850
|
const MIN_CONCURRENCY = 2;
|
|
33851
33851
|
const MAX_ATTEMPTS = 5;
|
|
33852
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33852
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33853
33853
|
var DecisionRunner = class {
|
|
33854
33854
|
model;
|
|
33855
33855
|
limit = MAX_CONCURRENCY;
|
|
@@ -33914,7 +33914,7 @@ var DecisionRunner = class {
|
|
|
33914
33914
|
} finally {
|
|
33915
33915
|
this.release();
|
|
33916
33916
|
}
|
|
33917
|
-
await sleep(delay);
|
|
33917
|
+
await sleep$1(delay);
|
|
33918
33918
|
}
|
|
33919
33919
|
this.failed += 1;
|
|
33920
33920
|
return null;
|
|
@@ -41955,6 +41955,30 @@ async function dataForSeoTask([login, password], path, task) {
|
|
|
41955
41955
|
}
|
|
41956
41956
|
const NO_RESULTS = 40102;
|
|
41957
41957
|
const PARTIAL = 40106;
|
|
41958
|
+
const queued = (task) => ({
|
|
41959
|
+
id: String(task.id ?? ""),
|
|
41960
|
+
status_code: Number(task.status_code ?? 0),
|
|
41961
|
+
status_message: String(task.status_message ?? ""),
|
|
41962
|
+
cost: Number(task.cost ?? 0),
|
|
41963
|
+
data: task.data ?? {},
|
|
41964
|
+
result: task.result?.[0] ?? null
|
|
41965
|
+
});
|
|
41966
|
+
async function dataForSeoPost([login, password], path, tasks) {
|
|
41967
|
+
return ((await readJson(await call("dataforseo", new Request(`https://api.dataforseo.com/${path}`, {
|
|
41968
|
+
method: "POST",
|
|
41969
|
+
headers: {
|
|
41970
|
+
authorization: `Basic ${btoa(`${login}:${password}`)}`,
|
|
41971
|
+
"content-type": "application/json"
|
|
41972
|
+
},
|
|
41973
|
+
body: JSON.stringify(tasks)
|
|
41974
|
+
})), "DataForSEO")).tasks ?? []).map(queued);
|
|
41975
|
+
}
|
|
41976
|
+
const DATAFORSEO_NOT_READY = /* @__PURE__ */ new Set([40601, 40602]);
|
|
41977
|
+
async function dataForSeoGet([login, password], path) {
|
|
41978
|
+
const task = (await readJson(await call("dataforseo", new Request(`https://api.dataforseo.com/${path}`, { headers: { authorization: `Basic ${btoa(`${login}:${password}`)}` } })), "DataForSEO")).tasks?.[0];
|
|
41979
|
+
if (!task) throw new CommandError("PROVIDER_ERROR", `dataforseo: no task at ${path}`);
|
|
41980
|
+
return queued(task);
|
|
41981
|
+
}
|
|
41958
41982
|
async function dataForSeo(keys, input) {
|
|
41959
41983
|
const spent = {
|
|
41960
41984
|
cost: 0,
|
|
@@ -42254,7 +42278,8 @@ const ENGINES = [
|
|
|
42254
42278
|
{
|
|
42255
42279
|
name: "chatgpt",
|
|
42256
42280
|
means: "a ChatGPT search, read by DataForSEO",
|
|
42257
|
-
|
|
42281
|
+
post: "v3/ai_optimization/chat_gpt/llm_scraper/task_post",
|
|
42282
|
+
get: "v3/ai_optimization/chat_gpt/llm_scraper/task_get/advanced/",
|
|
42258
42283
|
languages: "v3/ai_optimization/chat_gpt/llm_scraper/languages",
|
|
42259
42284
|
task: (question, market) => ({
|
|
42260
42285
|
keyword: question,
|
|
@@ -42274,7 +42299,8 @@ const ENGINES = [
|
|
|
42274
42299
|
{
|
|
42275
42300
|
name: "google_ai_mode",
|
|
42276
42301
|
means: "Google AI Mode",
|
|
42277
|
-
|
|
42302
|
+
post: "v3/serp/google/ai_mode/task_post",
|
|
42303
|
+
get: "v3/serp/google/ai_mode/task_get/advanced/",
|
|
42278
42304
|
languages: "v3/serp/google/ai_mode/languages",
|
|
42279
42305
|
task: (question, market) => ({
|
|
42280
42306
|
keyword: question,
|
|
@@ -42285,7 +42311,8 @@ const ENGINES = [
|
|
|
42285
42311
|
{
|
|
42286
42312
|
name: "google_ai_overview",
|
|
42287
42313
|
means: "the AI Overview on a Google search",
|
|
42288
|
-
|
|
42314
|
+
post: "v3/serp/google/organic/task_post",
|
|
42315
|
+
get: "v3/serp/google/organic/task_get/advanced/",
|
|
42289
42316
|
languages: "v3/serp/google/languages",
|
|
42290
42317
|
task: (question, market) => ({
|
|
42291
42318
|
keyword: question,
|
|
@@ -42307,56 +42334,55 @@ function onSite(url, domain) {
|
|
|
42307
42334
|
return false;
|
|
42308
42335
|
}
|
|
42309
42336
|
}
|
|
42310
|
-
|
|
42311
|
-
|
|
42312
|
-
result: "unavailable",
|
|
42313
|
-
model: null,
|
|
42314
|
-
cited_url: null,
|
|
42315
|
-
answered_at: null,
|
|
42316
|
-
cost_usd: 0,
|
|
42317
|
-
error: market
|
|
42318
|
-
};
|
|
42337
|
+
const MAX_SOURCES = 8;
|
|
42338
|
+
const hostOf$2 = (url) => {
|
|
42319
42339
|
try {
|
|
42320
|
-
|
|
42321
|
-
|
|
42322
|
-
|
|
42323
|
-
cost_usd: done.cost
|
|
42324
|
-
};
|
|
42325
|
-
const shown = done.result && engine.shown(done.result);
|
|
42326
|
-
if (!shown) return {
|
|
42327
|
-
result: "no_answer",
|
|
42328
|
-
model: null,
|
|
42329
|
-
cited_url: null,
|
|
42330
|
-
...answered
|
|
42331
|
-
};
|
|
42332
|
-
const base = {
|
|
42333
|
-
model: shown.model,
|
|
42334
|
-
...answered
|
|
42335
|
-
};
|
|
42336
|
-
const linked = shown.markdown.match(/https?:\/\/[^\s)\]]+/g) ?? [];
|
|
42337
|
-
const cited = [...shown.sources, ...linked].find((url) => onSite(url, site.domain));
|
|
42338
|
-
if (cited) return {
|
|
42339
|
-
result: "cited",
|
|
42340
|
-
cited_url: cited,
|
|
42341
|
-
...base
|
|
42342
|
-
};
|
|
42343
|
-
const text = prose(shown.markdown);
|
|
42344
|
-
return {
|
|
42345
|
-
result: text.toLowerCase().includes(site.domain) || isBrandQuery(text, site.terms) ? "mentioned" : "neither",
|
|
42346
|
-
cited_url: null,
|
|
42347
|
-
...base
|
|
42348
|
-
};
|
|
42349
|
-
} catch (error) {
|
|
42350
|
-
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
42351
|
-
return {
|
|
42352
|
-
result: "unavailable",
|
|
42353
|
-
model: null,
|
|
42354
|
-
cited_url: null,
|
|
42355
|
-
answered_at: null,
|
|
42356
|
-
cost_usd: error.cost ?? 0,
|
|
42357
|
-
error: error instanceof Error ? error.message.slice(0, 200) : "failed"
|
|
42358
|
-
};
|
|
42340
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
42341
|
+
} catch {
|
|
42342
|
+
return null;
|
|
42359
42343
|
}
|
|
42344
|
+
};
|
|
42345
|
+
const empty = {
|
|
42346
|
+
cited_url: null,
|
|
42347
|
+
sources: [],
|
|
42348
|
+
model: null,
|
|
42349
|
+
answered_at: null
|
|
42350
|
+
};
|
|
42351
|
+
function judge(engine, task, site) {
|
|
42352
|
+
if (task.status_code === 40102) return {
|
|
42353
|
+
result: "no_answer",
|
|
42354
|
+
...empty
|
|
42355
|
+
};
|
|
42356
|
+
if (task.status_code >= 4e4 || !task.result) return {
|
|
42357
|
+
result: "unavailable",
|
|
42358
|
+
...empty,
|
|
42359
|
+
error: `dataforseo: ${task.status_message}`.slice(0, 200)
|
|
42360
|
+
};
|
|
42361
|
+
const answered_at = String(task.result.datetime ?? "").slice(0, 10) || null;
|
|
42362
|
+
const shown = engine.shown(task.result);
|
|
42363
|
+
if (!shown) return {
|
|
42364
|
+
result: "no_answer",
|
|
42365
|
+
...empty,
|
|
42366
|
+
answered_at
|
|
42367
|
+
};
|
|
42368
|
+
const linked = shown.markdown.match(/https?:\/\/[^\s)\]]+/g) ?? [];
|
|
42369
|
+
const all = [...shown.sources, ...linked];
|
|
42370
|
+
const cited = all.find((url) => onSite(url, site.domain)) ?? null;
|
|
42371
|
+
const base = {
|
|
42372
|
+
cited_url: cited,
|
|
42373
|
+
sources: [...new Set(all.filter((url) => !onSite(url, site.domain)).map(hostOf$2).filter((host) => Boolean(host)))].slice(0, MAX_SOURCES),
|
|
42374
|
+
model: shown.model,
|
|
42375
|
+
answered_at
|
|
42376
|
+
};
|
|
42377
|
+
if (cited) return {
|
|
42378
|
+
result: "cited",
|
|
42379
|
+
...base
|
|
42380
|
+
};
|
|
42381
|
+
const text = prose(shown.markdown);
|
|
42382
|
+
return {
|
|
42383
|
+
result: text.toLowerCase().includes(site.domain) || isBrandQuery(text, site.terms) ? "mentioned" : "neither",
|
|
42384
|
+
...base
|
|
42385
|
+
};
|
|
42360
42386
|
}
|
|
42361
42387
|
async function marketsFor(keys, country) {
|
|
42362
42388
|
const code = country.toUpperCase();
|
|
@@ -42374,45 +42400,162 @@ async function marketsFor(keys, country) {
|
|
|
42374
42400
|
}));
|
|
42375
42401
|
}
|
|
42376
42402
|
const usd = (value) => Math.round(value * 1e4) / 1e4;
|
|
42403
|
+
const FIRST_WAIT_MS = 1e4;
|
|
42404
|
+
const ROUND_WAIT_MS = 5e3;
|
|
42405
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
42406
|
+
const TASK_PATTERN = /^([a-z_]+):([0-9a-f-]{20,64})$/;
|
|
42407
|
+
const AI_ANSWER_TASK_PATTERN = TASK_PATTERN;
|
|
42377
42408
|
async function runAiAnswers(input, headers, deps, more) {
|
|
42378
42409
|
const keys = dataForSeoKeys(headers);
|
|
42379
42410
|
const domain = registrableDomain(input.site)?.domain;
|
|
42380
42411
|
if (!domain) throw new CommandError("INVALID_INPUT", `ai_answers needs a site with a domain, not "${input.site}".`);
|
|
42381
|
-
const given = [...new Set((input.questions ?? []).map((q) => q.trim()).filter(Boolean))];
|
|
42382
|
-
if (given.length > 10) throw new CommandError("INVALID_INPUT", `ai_answers takes up to 10 questions, not ${given.length}.`);
|
|
42383
|
-
const range = dateWindow({}, QUESTION_DAYS);
|
|
42384
|
-
let questions;
|
|
42385
|
-
if (given.length) questions = given.map((query) => ({
|
|
42386
|
-
query,
|
|
42387
|
-
impressions: null
|
|
42388
|
-
}));
|
|
42389
|
-
else questions = ((await aiQueriesReport({
|
|
42390
|
-
site: input.site,
|
|
42391
|
-
...range
|
|
42392
|
-
}, deps.searchAnalytics)).items ?? []).filter((item) => item.askable).slice(0, 10);
|
|
42393
42412
|
const site = {
|
|
42394
42413
|
domain,
|
|
42395
42414
|
terms: deriveBrandTerms({ siteUrl: input.site })
|
|
42396
42415
|
};
|
|
42397
|
-
const
|
|
42398
|
-
|
|
42399
|
-
|
|
42400
|
-
|
|
42401
|
-
|
|
42402
|
-
|
|
42403
|
-
|
|
42404
|
-
|
|
42405
|
-
|
|
42406
|
-
|
|
42407
|
-
|
|
42416
|
+
const given = [...new Set((input.questions ?? []).map((q) => q.trim()).filter(Boolean))];
|
|
42417
|
+
if (given.length > 10) throw new CommandError("INVALID_INPUT", `ai_answers takes up to 10 questions, not ${given.length}.`);
|
|
42418
|
+
const tasks = [...new Set(input.tasks ?? [])];
|
|
42419
|
+
const range = dateWindow({}, QUESTION_DAYS);
|
|
42420
|
+
const rows = [];
|
|
42421
|
+
const waiting = [];
|
|
42422
|
+
const blank = (question, impressions) => ({
|
|
42423
|
+
question,
|
|
42424
|
+
impressions,
|
|
42425
|
+
task: null,
|
|
42426
|
+
cost_usd: 0,
|
|
42427
|
+
...empty
|
|
42428
|
+
});
|
|
42429
|
+
if (tasks.length) for (const value of tasks) {
|
|
42430
|
+
const match = TASK_PATTERN.exec(value);
|
|
42431
|
+
const engine = ENGINES.find((item) => item.name === match?.[1]);
|
|
42432
|
+
if (!match || !engine) throw new CommandError("INVALID_INPUT", `"${value}" is not an ai_answers task. Tasks look like <engine>:<id>, with the engine one of ${ENGINES.map((item) => item.name).join(", ")}.`);
|
|
42433
|
+
const row = {
|
|
42434
|
+
...blank("", null),
|
|
42435
|
+
engine: engine.name,
|
|
42436
|
+
result: "pending",
|
|
42437
|
+
task: value
|
|
42438
|
+
};
|
|
42439
|
+
rows.push(row);
|
|
42440
|
+
waiting.push({
|
|
42441
|
+
row,
|
|
42442
|
+
engine,
|
|
42443
|
+
id: match[2]
|
|
42444
|
+
});
|
|
42445
|
+
}
|
|
42446
|
+
else {
|
|
42447
|
+
let questions;
|
|
42448
|
+
if (given.length) questions = given.map((query) => ({
|
|
42449
|
+
query,
|
|
42450
|
+
impressions: null
|
|
42451
|
+
}));
|
|
42452
|
+
else questions = ((await aiQueriesReport({
|
|
42453
|
+
site: input.site,
|
|
42454
|
+
...range
|
|
42455
|
+
}, deps.searchAnalytics)).items ?? []).filter((item) => item.askable).slice(0, 10);
|
|
42456
|
+
const markets = questions.length ? await marketsFor(keys, input.country) : /* @__PURE__ */ new Map();
|
|
42457
|
+
await Promise.all(ENGINES.map(async (engine) => {
|
|
42458
|
+
const engineRows = questions.map((question) => ({
|
|
42459
|
+
...blank(question.query, question.impressions),
|
|
42460
|
+
engine: engine.name,
|
|
42461
|
+
result: "unavailable"
|
|
42462
|
+
}));
|
|
42463
|
+
rows.push(...engineRows);
|
|
42464
|
+
if (!questions.length) return;
|
|
42465
|
+
const market = markets.get(engine.name) ?? "no market";
|
|
42466
|
+
if (typeof market === "string") {
|
|
42467
|
+
for (const row of engineRows) row.error = market;
|
|
42468
|
+
return;
|
|
42469
|
+
}
|
|
42470
|
+
let queuedTasks;
|
|
42471
|
+
try {
|
|
42472
|
+
queuedTasks = await dataForSeoPost(keys, engine.post, questions.map((question) => ({
|
|
42473
|
+
...engine.task(question.query, market),
|
|
42474
|
+
priority: 2
|
|
42475
|
+
})));
|
|
42476
|
+
} catch (error) {
|
|
42477
|
+
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
42478
|
+
for (const row of engineRows) row.error = error instanceof Error ? error.message.slice(0, 200) : "failed";
|
|
42479
|
+
return;
|
|
42480
|
+
}
|
|
42481
|
+
engineRows.forEach((row, index) => {
|
|
42482
|
+
const task = queuedTasks[index];
|
|
42483
|
+
row.cost_usd = task?.cost ?? 0;
|
|
42484
|
+
if (!task || task.status_code >= 4e4 || !task.id) {
|
|
42485
|
+
row.error = `dataforseo: ${task?.status_message ?? "no task"}`;
|
|
42486
|
+
return;
|
|
42487
|
+
}
|
|
42488
|
+
row.result = "pending";
|
|
42489
|
+
row.task = `${engine.name}:${task.id}`;
|
|
42490
|
+
waiting.push({
|
|
42491
|
+
row,
|
|
42492
|
+
engine,
|
|
42493
|
+
id: task.id
|
|
42494
|
+
});
|
|
42495
|
+
});
|
|
42496
|
+
}));
|
|
42497
|
+
}
|
|
42498
|
+
const deadline = Date.now() + (input.waitSeconds ?? 40) * 1e3;
|
|
42499
|
+
let pending = waiting;
|
|
42500
|
+
let first = !tasks.length;
|
|
42501
|
+
while (pending.length) {
|
|
42502
|
+
const pause = first ? FIRST_WAIT_MS : ROUND_WAIT_MS;
|
|
42503
|
+
first = false;
|
|
42504
|
+
if (!tasks.length || pending !== waiting) {
|
|
42505
|
+
if (Date.now() + pause > deadline) break;
|
|
42506
|
+
await sleep(pause);
|
|
42507
|
+
}
|
|
42508
|
+
const still = [];
|
|
42509
|
+
await Promise.all(pending.map(async (item) => {
|
|
42510
|
+
try {
|
|
42511
|
+
const task = await dataForSeoGet(keys, `${item.engine.get}${item.id}`);
|
|
42512
|
+
if (!item.row.question) item.row.question = String(task.data.keyword ?? "");
|
|
42513
|
+
if (DATAFORSEO_NOT_READY.has(task.status_code)) {
|
|
42514
|
+
still.push(item);
|
|
42515
|
+
return;
|
|
42516
|
+
}
|
|
42517
|
+
Object.assign(item.row, judge(item.engine, task, site), { task: null });
|
|
42518
|
+
} catch (error) {
|
|
42519
|
+
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
42520
|
+
still.push(item);
|
|
42521
|
+
}
|
|
42522
|
+
}));
|
|
42523
|
+
pending = still;
|
|
42524
|
+
if (tasks.length && Date.now() >= deadline) break;
|
|
42525
|
+
}
|
|
42526
|
+
rows.sort((a, b) => a.question.localeCompare(b.question) || ENGINES.findIndex((e) => e.name === a.engine) - ENGINES.findIndex((e) => e.name === b.engine));
|
|
42527
|
+
const cost = usd(rows.reduce((sum, row) => sum + row.cost_usd, 0));
|
|
42528
|
+
const count = (result) => rows.filter((row) => row.result === result).length;
|
|
42529
|
+
const left = rows.filter((row) => row.task).map((row) => row.task);
|
|
42530
|
+
const cites = /* @__PURE__ */ new Map();
|
|
42531
|
+
for (const row of rows) for (const host of row.sources) cites.set(host, (cites.get(host) ?? 0) + 1);
|
|
42532
|
+
const topCited = [...cites].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
42533
|
+
const questionCount = new Set(rows.map((row) => row.question)).size;
|
|
42534
|
+
const notes = [tasks.length ? `${tasks.length === 1 ? "One answer" : `${tasks.length} answers`} queued by an earlier run, read back at no cost.` : given.length ? `The ${given.length} questions you gave, asked as written. Impressions are blank because they don't come from Search Console.` : questionCount ? `The top ${questionCount} askable questions in Search Console’s AI-shaped queries for ${range.startDate} to ${range.endDate}, by impressions. No calls were made for other questions.` : `No askable questions in Search Console’s AI-shaped queries for ${range.startDate} to ${range.endDate}, so no answers were checked and nothing was spent.`, ...rows.length ? [
|
|
42535
|
+
`Cited ${count("cited")}, mentioned ${count("mentioned")}, neither ${count("neither")}, no answer shown ${count("no_answer")}, unavailable ${count("unavailable")}, not ready ${count("pending")}. DataForSEO cost: $${cost}.`,
|
|
42536
|
+
...left.length ? [`${left.length === 1 ? "One answer wasn't" : `${left.length} answers weren't`} ready yet. They're paid for: ask ai_answers again with the same target and tasks ${JSON.stringify(left)} in a minute to read them at no cost (DataForSEO keeps them for 30 days).`] : [],
|
|
42537
|
+
...topCited.length ? [`Other sites these answers cite: ${topCited.map(([host, n]) => `${host} (${n})`).join(", ")}.`] : [],
|
|
42408
42538
|
"Each row is one sample of an answer that changes from run to run, not a ranking.",
|
|
42409
|
-
`cited: the answer lists or links a page on ${domain} or a subdomain. mentioned: no such source, but the answer text names ${[domain, ...site.terms].join(" or ")}, a text match. no_answer: the engine showed no AI answer for the question. model: the model DataForSEO reports for the answer, when it reports one.`,
|
|
42539
|
+
`cited: the answer lists or links a page on ${domain} or a subdomain. mentioned: no such source, but the answer text names ${[domain, ...site.terms].join(" or ")}, a text match. no_answer: the engine showed no AI answer for the question. sources: the other sites the answer cites, up to ${MAX_SOURCES}. model: the model DataForSEO reports for the answer, when it reports one.`,
|
|
42410
42540
|
`Engines: ${ENGINES.map((engine) => `${engine.name} is ${engine.means}`).join("; ")}.`
|
|
42411
42541
|
] : []];
|
|
42542
|
+
const shown = rows.map((row) => ({
|
|
42543
|
+
question: row.question,
|
|
42544
|
+
impressions: row.impressions,
|
|
42545
|
+
engine: row.engine,
|
|
42546
|
+
result: row.result,
|
|
42547
|
+
cited_url: row.cited_url,
|
|
42548
|
+
sources: row.sources.join(", "),
|
|
42549
|
+
model: row.model,
|
|
42550
|
+
answered_at: row.answered_at,
|
|
42551
|
+
cost_usd: row.cost_usd,
|
|
42552
|
+
task: row.task,
|
|
42553
|
+
error: row.error ?? null
|
|
42554
|
+
}));
|
|
42412
42555
|
return {
|
|
42413
42556
|
text: rowsText({
|
|
42414
42557
|
title: `AI answers for ${input.site}, ${input.country}.`,
|
|
42415
|
-
rows:
|
|
42558
|
+
rows: shown,
|
|
42416
42559
|
offset: input.offset,
|
|
42417
42560
|
limit: 50,
|
|
42418
42561
|
detail: input.detail,
|
|
@@ -42424,15 +42567,23 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
42424
42567
|
action: "ai_answers",
|
|
42425
42568
|
site: input.site,
|
|
42426
42569
|
country: input.country,
|
|
42427
|
-
questionsFrom: given.length ? { section: "given" } : {
|
|
42570
|
+
questionsFrom: tasks.length ? { section: "tasks" } : given.length ? { section: "given" } : {
|
|
42428
42571
|
section: "ai-queries",
|
|
42429
42572
|
...range
|
|
42430
42573
|
},
|
|
42431
|
-
questions:
|
|
42574
|
+
questions: [...new Set(rows.map((row) => row.question))],
|
|
42432
42575
|
costUsd: cost,
|
|
42433
|
-
|
|
42576
|
+
pendingTasks: left,
|
|
42577
|
+
citedSites: topCited.map(([host, answers]) => ({
|
|
42578
|
+
host,
|
|
42579
|
+
answers
|
|
42580
|
+
})),
|
|
42581
|
+
total: rows.length,
|
|
42434
42582
|
offset: input.offset,
|
|
42435
|
-
rows:
|
|
42583
|
+
rows: rows.slice(input.offset, input.offset + 50).map(({ error, ...row }) => error ? {
|
|
42584
|
+
...row,
|
|
42585
|
+
error
|
|
42586
|
+
} : row)
|
|
42436
42587
|
}
|
|
42437
42588
|
};
|
|
42438
42589
|
}
|
|
@@ -45067,7 +45218,7 @@ function nextFor(section, call) {
|
|
|
45067
45218
|
"content-decay": `Next: ${call("index")} for pages that lost most of their clicks; ${call("page")} to see which queries moved.`,
|
|
45068
45219
|
cannibalization: `Next: ${call("page")} on each competing URL to see Google’s chosen canonical.`,
|
|
45069
45220
|
"traffic-drop": `Next: ${call("compare")} with the date the drop started, for the pages and queries that moved; ${call("index")} for pages that lost almost all their clicks and their index status.`,
|
|
45070
|
-
"ai-queries": `Next: ${call("page")} for a page that gets many of these questions, to see whether it answers them; ${call("research")} ai_answers with the site as target checks whether AI answers cite it for the top 10 askable questions here (about $0.
|
|
45221
|
+
"ai-queries": `Next: ${call("page")} for a page that gets many of these questions, to see whether it answers them; ${call("research")} ai_answers with the site as target checks whether AI answers cite it for the top 10 askable questions here (about $0.07 of DataForSEO credit), so show the person those questions and ask first.`,
|
|
45071
45222
|
bing: `Next: ${call("data", { source: "bing" })} for every Bing row; ${call("page")} for one page.`
|
|
45072
45223
|
}[section];
|
|
45073
45224
|
}
|
|
@@ -45571,7 +45722,7 @@ const RESEARCH_ACTIONS = [
|
|
|
45571
45722
|
"ai_answers"
|
|
45572
45723
|
];
|
|
45573
45724
|
const RESEARCH_PROVIDERS = ["dataforseo", "ahrefs"];
|
|
45574
|
-
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), ai_answers (DataForSEO only; target: the site. Without keywords it takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself. With up to 10 questions in keywords it asks those instead, and the target can be any domain, such as a competitor's. It asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s
|
|
45725
|
+
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), ai_answers (DataForSEO only; target: the site. Without keywords it takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself. With up to 10 questions in keywords it asks those instead, and the target can be any domain, such as a competitor's. Each answer row also lists the other sites it cites, and the notes rank them. Answers come from DataForSEO's queue: answers not ready after about 40 seconds are returned as tasks, already paid for; call again with the same target and those tasks to read them at no cost. It asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s queue prices are about $0.0072 a question across the three engines, so about $0.07 a run: tell the person before running it). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.`;
|
|
45575
45726
|
const GENERIC_PLATFORMS = [
|
|
45576
45727
|
"youtube.com",
|
|
45577
45728
|
"facebook.com",
|
|
@@ -46519,6 +46670,7 @@ const SHARED_TOOLS = [
|
|
|
46519
46670
|
action: z.enum(RESEARCH_ACTIONS),
|
|
46520
46671
|
keywords: z.array(z.string().min(1)).max(100).default([]),
|
|
46521
46672
|
target: z.string().default("").describe("Domain or URL."),
|
|
46673
|
+
tasks: z.array(z.string().regex(AI_ANSWER_TASK_PATTERN)).max(30).default([]).describe("ai_answers only: the tasks an earlier run returned as not ready, to read back at no cost."),
|
|
46522
46674
|
country: z.string().regex(/^[a-z]{2}$/).default("us").describe("Two-letter country code."),
|
|
46523
46675
|
...detail
|
|
46524
46676
|
}),
|
|
@@ -46527,11 +46679,12 @@ const SHARED_TOOLS = [
|
|
|
46527
46679
|
if (input.action === "ai_answers") {
|
|
46528
46680
|
if (input.provider !== "dataforseo") throw new CommandError("INVALID_INPUT", "ai_answers uses dataforseo.");
|
|
46529
46681
|
return runAiAnswers({
|
|
46530
|
-
site: input.keywords.length ? input.target : await surface.resolveSite(input.target),
|
|
46682
|
+
site: input.keywords.length || input.tasks.length ? input.target : await surface.resolveSite(input.target),
|
|
46531
46683
|
country: input.country,
|
|
46532
46684
|
detail: input.detail,
|
|
46533
46685
|
offset: input.offset,
|
|
46534
|
-
questions: input.keywords
|
|
46686
|
+
questions: input.keywords,
|
|
46687
|
+
tasks: input.tasks
|
|
46535
46688
|
}, await surface.keys(), surface.deps, more);
|
|
46536
46689
|
}
|
|
46537
46690
|
return runResearch(input, await surface.keys(), {
|