seo 0.15.0 → 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-Dik5MNDk.js → dist-DOJoZZ9W.js} +1 -1
- package/dist/{dist-pwGymked.js → dist-Dpl5hSMM.js} +71 -71
- package/dist/dist-Dpl5hSMM.js.map +1 -0
- package/dist/{esm-DieyhVZ-.js → esm-Dt4JJZ0P.js} +2 -2
- package/dist/{esm-DieyhVZ-.js.map → esm-Dt4JJZ0P.js.map} +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +246 -85
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +246 -85
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/dist-pwGymked.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as version, t as load } from "./esm-
|
|
2
|
+
import { n as version, t as load } from "./esm-Dt4JJZ0P.js";
|
|
3
3
|
import { SEO_PROVIDER_API_VERSION } from "./provider-sdk.js";
|
|
4
4
|
import crypto, { createHash, randomUUID, sign } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -2709,7 +2709,7 @@ async function readableSites(google, signIn) {
|
|
|
2709
2709
|
permissionLevel: entry.permissionLevel ?? ""
|
|
2710
2710
|
})).sort((a, b) => a.siteUrl.localeCompare(b.siteUrl));
|
|
2711
2711
|
}
|
|
2712
|
-
const hostOf$
|
|
2712
|
+
const hostOf$3 = (value) => {
|
|
2713
2713
|
try {
|
|
2714
2714
|
return new URL(value.includes("://") ? value : `https://${value.replace(/^sc-domain:/, "")}`).hostname.replace(/^www\./, "");
|
|
2715
2715
|
} catch {
|
|
@@ -2727,14 +2727,14 @@ async function runSites(input) {
|
|
|
2727
2727
|
};
|
|
2728
2728
|
})), input.bing ? listBingSites(input.bing).catch((error) => error instanceof Error ? error : new Error(String(error))) : Promise.resolve(null)]);
|
|
2729
2729
|
const bingSites = Array.isArray(bing) ? bing.filter((site) => site.verified) : [];
|
|
2730
|
-
const bingByHost = new Map(bingSites.map((site) => [hostOf$
|
|
2730
|
+
const bingByHost = new Map(bingSites.map((site) => [hostOf$3(site.url), site]));
|
|
2731
2731
|
const properties = google.flatMap((account) => account.properties instanceof Error ? [] : account.properties.map((property) => ({
|
|
2732
2732
|
...property,
|
|
2733
2733
|
...named ? { account: account.email ?? "" } : {},
|
|
2734
|
-
...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$
|
|
2734
|
+
...Array.isArray(bing) ? { bing: bingByHost.get(hostOf$3(property.siteUrl))?.url ?? "" } : {}
|
|
2735
2735
|
})));
|
|
2736
|
-
const googleHosts = new Set(properties.map((property) => hostOf$
|
|
2737
|
-
const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$
|
|
2736
|
+
const googleHosts = new Set(properties.map((property) => hostOf$3(property.siteUrl)));
|
|
2737
|
+
const bingOnly = bingSites.filter((site) => !googleHosts.has(hostOf$3(site.url)));
|
|
2738
2738
|
const failed = google.filter((account) => account.properties instanceof Error);
|
|
2739
2739
|
const analytics = google.flatMap((account) => (account.analytics ?? []).flatMap((summary) => summary.propertySummaries.map((property) => ({
|
|
2740
2740
|
property: property.property.replace("properties/", ""),
|
|
@@ -23563,7 +23563,7 @@ function typesafeDecisionModel(input) {
|
|
|
23563
23563
|
const MAX_CONCURRENCY = 32;
|
|
23564
23564
|
const MIN_CONCURRENCY = 2;
|
|
23565
23565
|
const MAX_ATTEMPTS = 5;
|
|
23566
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23566
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23567
23567
|
var DecisionRunner = class {
|
|
23568
23568
|
model;
|
|
23569
23569
|
limit = MAX_CONCURRENCY;
|
|
@@ -23628,7 +23628,7 @@ var DecisionRunner = class {
|
|
|
23628
23628
|
} finally {
|
|
23629
23629
|
this.release();
|
|
23630
23630
|
}
|
|
23631
|
-
await sleep(delay);
|
|
23631
|
+
await sleep$1(delay);
|
|
23632
23632
|
}
|
|
23633
23633
|
this.failed += 1;
|
|
23634
23634
|
return null;
|
|
@@ -31876,6 +31876,30 @@ async function dataForSeoTask([login, password], path, task) {
|
|
|
31876
31876
|
}
|
|
31877
31877
|
const NO_RESULTS = 40102;
|
|
31878
31878
|
const PARTIAL = 40106;
|
|
31879
|
+
const queued = (task) => ({
|
|
31880
|
+
id: String(task.id ?? ""),
|
|
31881
|
+
status_code: Number(task.status_code ?? 0),
|
|
31882
|
+
status_message: String(task.status_message ?? ""),
|
|
31883
|
+
cost: Number(task.cost ?? 0),
|
|
31884
|
+
data: task.data ?? {},
|
|
31885
|
+
result: task.result?.[0] ?? null
|
|
31886
|
+
});
|
|
31887
|
+
async function dataForSeoPost([login, password], path, tasks) {
|
|
31888
|
+
return ((await readJson(await call("dataforseo", new Request(`https://api.dataforseo.com/${path}`, {
|
|
31889
|
+
method: "POST",
|
|
31890
|
+
headers: {
|
|
31891
|
+
authorization: `Basic ${btoa(`${login}:${password}`)}`,
|
|
31892
|
+
"content-type": "application/json"
|
|
31893
|
+
},
|
|
31894
|
+
body: JSON.stringify(tasks)
|
|
31895
|
+
})), "DataForSEO")).tasks ?? []).map(queued);
|
|
31896
|
+
}
|
|
31897
|
+
const DATAFORSEO_NOT_READY = /* @__PURE__ */ new Set([40601, 40602]);
|
|
31898
|
+
async function dataForSeoGet([login, password], path) {
|
|
31899
|
+
const task = (await readJson(await call("dataforseo", new Request(`https://api.dataforseo.com/${path}`, { headers: { authorization: `Basic ${btoa(`${login}:${password}`)}` } })), "DataForSEO")).tasks?.[0];
|
|
31900
|
+
if (!task) throw new CommandError("PROVIDER_ERROR", `dataforseo: no task at ${path}`);
|
|
31901
|
+
return queued(task);
|
|
31902
|
+
}
|
|
31879
31903
|
async function dataForSeo(keys, input) {
|
|
31880
31904
|
const spent = {
|
|
31881
31905
|
cost: 0,
|
|
@@ -32175,7 +32199,8 @@ const ENGINES = [
|
|
|
32175
32199
|
{
|
|
32176
32200
|
name: "chatgpt",
|
|
32177
32201
|
means: "a ChatGPT search, read by DataForSEO",
|
|
32178
|
-
|
|
32202
|
+
post: "v3/ai_optimization/chat_gpt/llm_scraper/task_post",
|
|
32203
|
+
get: "v3/ai_optimization/chat_gpt/llm_scraper/task_get/advanced/",
|
|
32179
32204
|
languages: "v3/ai_optimization/chat_gpt/llm_scraper/languages",
|
|
32180
32205
|
task: (question, market) => ({
|
|
32181
32206
|
keyword: question,
|
|
@@ -32195,7 +32220,8 @@ const ENGINES = [
|
|
|
32195
32220
|
{
|
|
32196
32221
|
name: "google_ai_mode",
|
|
32197
32222
|
means: "Google AI Mode",
|
|
32198
|
-
|
|
32223
|
+
post: "v3/serp/google/ai_mode/task_post",
|
|
32224
|
+
get: "v3/serp/google/ai_mode/task_get/advanced/",
|
|
32199
32225
|
languages: "v3/serp/google/ai_mode/languages",
|
|
32200
32226
|
task: (question, market) => ({
|
|
32201
32227
|
keyword: question,
|
|
@@ -32206,7 +32232,8 @@ const ENGINES = [
|
|
|
32206
32232
|
{
|
|
32207
32233
|
name: "google_ai_overview",
|
|
32208
32234
|
means: "the AI Overview on a Google search",
|
|
32209
|
-
|
|
32235
|
+
post: "v3/serp/google/organic/task_post",
|
|
32236
|
+
get: "v3/serp/google/organic/task_get/advanced/",
|
|
32210
32237
|
languages: "v3/serp/google/languages",
|
|
32211
32238
|
task: (question, market) => ({
|
|
32212
32239
|
keyword: question,
|
|
@@ -32228,56 +32255,55 @@ function onSite(url, domain) {
|
|
|
32228
32255
|
return false;
|
|
32229
32256
|
}
|
|
32230
32257
|
}
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
result: "unavailable",
|
|
32234
|
-
model: null,
|
|
32235
|
-
cited_url: null,
|
|
32236
|
-
answered_at: null,
|
|
32237
|
-
cost_usd: 0,
|
|
32238
|
-
error: market
|
|
32239
|
-
};
|
|
32258
|
+
const MAX_SOURCES = 8;
|
|
32259
|
+
const hostOf$2 = (url) => {
|
|
32240
32260
|
try {
|
|
32241
|
-
|
|
32242
|
-
|
|
32243
|
-
|
|
32244
|
-
cost_usd: done.cost
|
|
32245
|
-
};
|
|
32246
|
-
const shown = done.result && engine.shown(done.result);
|
|
32247
|
-
if (!shown) return {
|
|
32248
|
-
result: "no_answer",
|
|
32249
|
-
model: null,
|
|
32250
|
-
cited_url: null,
|
|
32251
|
-
...answered
|
|
32252
|
-
};
|
|
32253
|
-
const base = {
|
|
32254
|
-
model: shown.model,
|
|
32255
|
-
...answered
|
|
32256
|
-
};
|
|
32257
|
-
const linked = shown.markdown.match(/https?:\/\/[^\s)\]]+/g) ?? [];
|
|
32258
|
-
const cited = [...shown.sources, ...linked].find((url) => onSite(url, site.domain));
|
|
32259
|
-
if (cited) return {
|
|
32260
|
-
result: "cited",
|
|
32261
|
-
cited_url: cited,
|
|
32262
|
-
...base
|
|
32263
|
-
};
|
|
32264
|
-
const text = prose(shown.markdown);
|
|
32265
|
-
return {
|
|
32266
|
-
result: text.toLowerCase().includes(site.domain) || isBrandQuery(text, site.terms) ? "mentioned" : "neither",
|
|
32267
|
-
cited_url: null,
|
|
32268
|
-
...base
|
|
32269
|
-
};
|
|
32270
|
-
} catch (error) {
|
|
32271
|
-
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
32272
|
-
return {
|
|
32273
|
-
result: "unavailable",
|
|
32274
|
-
model: null,
|
|
32275
|
-
cited_url: null,
|
|
32276
|
-
answered_at: null,
|
|
32277
|
-
cost_usd: error.cost ?? 0,
|
|
32278
|
-
error: error instanceof Error ? error.message.slice(0, 200) : "failed"
|
|
32279
|
-
};
|
|
32261
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
32262
|
+
} catch {
|
|
32263
|
+
return null;
|
|
32280
32264
|
}
|
|
32265
|
+
};
|
|
32266
|
+
const empty = {
|
|
32267
|
+
cited_url: null,
|
|
32268
|
+
sources: [],
|
|
32269
|
+
model: null,
|
|
32270
|
+
answered_at: null
|
|
32271
|
+
};
|
|
32272
|
+
function judge(engine, task, site) {
|
|
32273
|
+
if (task.status_code === 40102) return {
|
|
32274
|
+
result: "no_answer",
|
|
32275
|
+
...empty
|
|
32276
|
+
};
|
|
32277
|
+
if (task.status_code >= 4e4 || !task.result) return {
|
|
32278
|
+
result: "unavailable",
|
|
32279
|
+
...empty,
|
|
32280
|
+
error: `dataforseo: ${task.status_message}`.slice(0, 200)
|
|
32281
|
+
};
|
|
32282
|
+
const answered_at = String(task.result.datetime ?? "").slice(0, 10) || null;
|
|
32283
|
+
const shown = engine.shown(task.result);
|
|
32284
|
+
if (!shown) return {
|
|
32285
|
+
result: "no_answer",
|
|
32286
|
+
...empty,
|
|
32287
|
+
answered_at
|
|
32288
|
+
};
|
|
32289
|
+
const linked = shown.markdown.match(/https?:\/\/[^\s)\]]+/g) ?? [];
|
|
32290
|
+
const all = [...shown.sources, ...linked];
|
|
32291
|
+
const cited = all.find((url) => onSite(url, site.domain)) ?? null;
|
|
32292
|
+
const base = {
|
|
32293
|
+
cited_url: cited,
|
|
32294
|
+
sources: [...new Set(all.filter((url) => !onSite(url, site.domain)).map(hostOf$2).filter((host) => Boolean(host)))].slice(0, MAX_SOURCES),
|
|
32295
|
+
model: shown.model,
|
|
32296
|
+
answered_at
|
|
32297
|
+
};
|
|
32298
|
+
if (cited) return {
|
|
32299
|
+
result: "cited",
|
|
32300
|
+
...base
|
|
32301
|
+
};
|
|
32302
|
+
const text = prose(shown.markdown);
|
|
32303
|
+
return {
|
|
32304
|
+
result: text.toLowerCase().includes(site.domain) || isBrandQuery(text, site.terms) ? "mentioned" : "neither",
|
|
32305
|
+
...base
|
|
32306
|
+
};
|
|
32281
32307
|
}
|
|
32282
32308
|
async function marketsFor(keys, country) {
|
|
32283
32309
|
const code = country.toUpperCase();
|
|
@@ -32295,38 +32321,162 @@ async function marketsFor(keys, country) {
|
|
|
32295
32321
|
}));
|
|
32296
32322
|
}
|
|
32297
32323
|
const usd = (value) => Math.round(value * 1e4) / 1e4;
|
|
32324
|
+
const FIRST_WAIT_MS = 1e4;
|
|
32325
|
+
const ROUND_WAIT_MS = 5e3;
|
|
32326
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
32327
|
+
const TASK_PATTERN = /^([a-z_]+):([0-9a-f-]{20,64})$/;
|
|
32328
|
+
const AI_ANSWER_TASK_PATTERN = TASK_PATTERN;
|
|
32298
32329
|
async function runAiAnswers(input, headers, deps, more) {
|
|
32299
32330
|
const keys = dataForSeoKeys(headers);
|
|
32300
32331
|
const domain = registrableDomain(input.site)?.domain;
|
|
32301
32332
|
if (!domain) throw new CommandError("INVALID_INPUT", `ai_answers needs a site with a domain, not "${input.site}".`);
|
|
32302
|
-
const range = dateWindow({}, QUESTION_DAYS);
|
|
32303
|
-
const questions = ((await aiQueriesReport({
|
|
32304
|
-
site: input.site,
|
|
32305
|
-
...range
|
|
32306
|
-
}, deps.searchAnalytics)).items ?? []).filter((item) => item.askable).slice(0, 10);
|
|
32307
32333
|
const site = {
|
|
32308
32334
|
domain,
|
|
32309
32335
|
terms: deriveBrandTerms({ siteUrl: input.site })
|
|
32310
32336
|
};
|
|
32311
|
-
const
|
|
32312
|
-
|
|
32313
|
-
|
|
32314
|
-
|
|
32315
|
-
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
|
|
32319
|
-
|
|
32320
|
-
|
|
32321
|
-
|
|
32337
|
+
const given = [...new Set((input.questions ?? []).map((q) => q.trim()).filter(Boolean))];
|
|
32338
|
+
if (given.length > 10) throw new CommandError("INVALID_INPUT", `ai_answers takes up to 10 questions, not ${given.length}.`);
|
|
32339
|
+
const tasks = [...new Set(input.tasks ?? [])];
|
|
32340
|
+
const range = dateWindow({}, QUESTION_DAYS);
|
|
32341
|
+
const rows = [];
|
|
32342
|
+
const waiting = [];
|
|
32343
|
+
const blank = (question, impressions) => ({
|
|
32344
|
+
question,
|
|
32345
|
+
impressions,
|
|
32346
|
+
task: null,
|
|
32347
|
+
cost_usd: 0,
|
|
32348
|
+
...empty
|
|
32349
|
+
});
|
|
32350
|
+
if (tasks.length) for (const value of tasks) {
|
|
32351
|
+
const match = TASK_PATTERN.exec(value);
|
|
32352
|
+
const engine = ENGINES.find((item) => item.name === match?.[1]);
|
|
32353
|
+
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(", ")}.`);
|
|
32354
|
+
const row = {
|
|
32355
|
+
...blank("", null),
|
|
32356
|
+
engine: engine.name,
|
|
32357
|
+
result: "pending",
|
|
32358
|
+
task: value
|
|
32359
|
+
};
|
|
32360
|
+
rows.push(row);
|
|
32361
|
+
waiting.push({
|
|
32362
|
+
row,
|
|
32363
|
+
engine,
|
|
32364
|
+
id: match[2]
|
|
32365
|
+
});
|
|
32366
|
+
}
|
|
32367
|
+
else {
|
|
32368
|
+
let questions;
|
|
32369
|
+
if (given.length) questions = given.map((query) => ({
|
|
32370
|
+
query,
|
|
32371
|
+
impressions: null
|
|
32372
|
+
}));
|
|
32373
|
+
else questions = ((await aiQueriesReport({
|
|
32374
|
+
site: input.site,
|
|
32375
|
+
...range
|
|
32376
|
+
}, deps.searchAnalytics)).items ?? []).filter((item) => item.askable).slice(0, 10);
|
|
32377
|
+
const markets = questions.length ? await marketsFor(keys, input.country) : /* @__PURE__ */ new Map();
|
|
32378
|
+
await Promise.all(ENGINES.map(async (engine) => {
|
|
32379
|
+
const engineRows = questions.map((question) => ({
|
|
32380
|
+
...blank(question.query, question.impressions),
|
|
32381
|
+
engine: engine.name,
|
|
32382
|
+
result: "unavailable"
|
|
32383
|
+
}));
|
|
32384
|
+
rows.push(...engineRows);
|
|
32385
|
+
if (!questions.length) return;
|
|
32386
|
+
const market = markets.get(engine.name) ?? "no market";
|
|
32387
|
+
if (typeof market === "string") {
|
|
32388
|
+
for (const row of engineRows) row.error = market;
|
|
32389
|
+
return;
|
|
32390
|
+
}
|
|
32391
|
+
let queuedTasks;
|
|
32392
|
+
try {
|
|
32393
|
+
queuedTasks = await dataForSeoPost(keys, engine.post, questions.map((question) => ({
|
|
32394
|
+
...engine.task(question.query, market),
|
|
32395
|
+
priority: 2
|
|
32396
|
+
})));
|
|
32397
|
+
} catch (error) {
|
|
32398
|
+
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
32399
|
+
for (const row of engineRows) row.error = error instanceof Error ? error.message.slice(0, 200) : "failed";
|
|
32400
|
+
return;
|
|
32401
|
+
}
|
|
32402
|
+
engineRows.forEach((row, index) => {
|
|
32403
|
+
const task = queuedTasks[index];
|
|
32404
|
+
row.cost_usd = task?.cost ?? 0;
|
|
32405
|
+
if (!task || task.status_code >= 4e4 || !task.id) {
|
|
32406
|
+
row.error = `dataforseo: ${task?.status_message ?? "no task"}`;
|
|
32407
|
+
return;
|
|
32408
|
+
}
|
|
32409
|
+
row.result = "pending";
|
|
32410
|
+
row.task = `${engine.name}:${task.id}`;
|
|
32411
|
+
waiting.push({
|
|
32412
|
+
row,
|
|
32413
|
+
engine,
|
|
32414
|
+
id: task.id
|
|
32415
|
+
});
|
|
32416
|
+
});
|
|
32417
|
+
}));
|
|
32418
|
+
}
|
|
32419
|
+
const deadline = Date.now() + (input.waitSeconds ?? 40) * 1e3;
|
|
32420
|
+
let pending = waiting;
|
|
32421
|
+
let first = !tasks.length;
|
|
32422
|
+
while (pending.length) {
|
|
32423
|
+
const pause = first ? FIRST_WAIT_MS : ROUND_WAIT_MS;
|
|
32424
|
+
first = false;
|
|
32425
|
+
if (!tasks.length || pending !== waiting) {
|
|
32426
|
+
if (Date.now() + pause > deadline) break;
|
|
32427
|
+
await sleep(pause);
|
|
32428
|
+
}
|
|
32429
|
+
const still = [];
|
|
32430
|
+
await Promise.all(pending.map(async (item) => {
|
|
32431
|
+
try {
|
|
32432
|
+
const task = await dataForSeoGet(keys, `${item.engine.get}${item.id}`);
|
|
32433
|
+
if (!item.row.question) item.row.question = String(task.data.keyword ?? "");
|
|
32434
|
+
if (DATAFORSEO_NOT_READY.has(task.status_code)) {
|
|
32435
|
+
still.push(item);
|
|
32436
|
+
return;
|
|
32437
|
+
}
|
|
32438
|
+
Object.assign(item.row, judge(item.engine, task, site), { task: null });
|
|
32439
|
+
} catch (error) {
|
|
32440
|
+
if (error instanceof CommandError && error.code === "NO_KEY") throw error;
|
|
32441
|
+
still.push(item);
|
|
32442
|
+
}
|
|
32443
|
+
}));
|
|
32444
|
+
pending = still;
|
|
32445
|
+
if (tasks.length && Date.now() >= deadline) break;
|
|
32446
|
+
}
|
|
32447
|
+
rows.sort((a, b) => a.question.localeCompare(b.question) || ENGINES.findIndex((e) => e.name === a.engine) - ENGINES.findIndex((e) => e.name === b.engine));
|
|
32448
|
+
const cost = usd(rows.reduce((sum, row) => sum + row.cost_usd, 0));
|
|
32449
|
+
const count = (result) => rows.filter((row) => row.result === result).length;
|
|
32450
|
+
const left = rows.filter((row) => row.task).map((row) => row.task);
|
|
32451
|
+
const cites = /* @__PURE__ */ new Map();
|
|
32452
|
+
for (const row of rows) for (const host of row.sources) cites.set(host, (cites.get(host) ?? 0) + 1);
|
|
32453
|
+
const topCited = [...cites].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
32454
|
+
const questionCount = new Set(rows.map((row) => row.question)).size;
|
|
32455
|
+
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 ? [
|
|
32456
|
+
`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}.`,
|
|
32457
|
+
...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).`] : [],
|
|
32458
|
+
...topCited.length ? [`Other sites these answers cite: ${topCited.map(([host, n]) => `${host} (${n})`).join(", ")}.`] : [],
|
|
32322
32459
|
"Each row is one sample of an answer that changes from run to run, not a ranking.",
|
|
32323
|
-
`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.`,
|
|
32460
|
+
`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.`,
|
|
32324
32461
|
`Engines: ${ENGINES.map((engine) => `${engine.name} is ${engine.means}`).join("; ")}.`
|
|
32325
32462
|
] : []];
|
|
32463
|
+
const shown = rows.map((row) => ({
|
|
32464
|
+
question: row.question,
|
|
32465
|
+
impressions: row.impressions,
|
|
32466
|
+
engine: row.engine,
|
|
32467
|
+
result: row.result,
|
|
32468
|
+
cited_url: row.cited_url,
|
|
32469
|
+
sources: row.sources.join(", "),
|
|
32470
|
+
model: row.model,
|
|
32471
|
+
answered_at: row.answered_at,
|
|
32472
|
+
cost_usd: row.cost_usd,
|
|
32473
|
+
task: row.task,
|
|
32474
|
+
error: row.error ?? null
|
|
32475
|
+
}));
|
|
32326
32476
|
return {
|
|
32327
32477
|
text: rowsText({
|
|
32328
32478
|
title: `AI answers for ${input.site}, ${input.country}.`,
|
|
32329
|
-
rows:
|
|
32479
|
+
rows: shown,
|
|
32330
32480
|
offset: input.offset,
|
|
32331
32481
|
limit: 50,
|
|
32332
32482
|
detail: input.detail,
|
|
@@ -32338,15 +32488,23 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32338
32488
|
action: "ai_answers",
|
|
32339
32489
|
site: input.site,
|
|
32340
32490
|
country: input.country,
|
|
32341
|
-
questionsFrom: {
|
|
32491
|
+
questionsFrom: tasks.length ? { section: "tasks" } : given.length ? { section: "given" } : {
|
|
32342
32492
|
section: "ai-queries",
|
|
32343
32493
|
...range
|
|
32344
32494
|
},
|
|
32345
|
-
questions:
|
|
32495
|
+
questions: [...new Set(rows.map((row) => row.question))],
|
|
32346
32496
|
costUsd: cost,
|
|
32347
|
-
|
|
32497
|
+
pendingTasks: left,
|
|
32498
|
+
citedSites: topCited.map(([host, answers]) => ({
|
|
32499
|
+
host,
|
|
32500
|
+
answers
|
|
32501
|
+
})),
|
|
32502
|
+
total: rows.length,
|
|
32348
32503
|
offset: input.offset,
|
|
32349
|
-
rows:
|
|
32504
|
+
rows: rows.slice(input.offset, input.offset + 50).map(({ error, ...row }) => error ? {
|
|
32505
|
+
...row,
|
|
32506
|
+
error
|
|
32507
|
+
} : row)
|
|
32350
32508
|
}
|
|
32351
32509
|
};
|
|
32352
32510
|
}
|
|
@@ -34928,7 +35086,7 @@ function nextFor(section, call) {
|
|
|
34928
35086
|
"content-decay": `Next: ${call("index")} for pages that lost most of their clicks; ${call("page")} to see which queries moved.`,
|
|
34929
35087
|
cannibalization: `Next: ${call("page")} on each competing URL to see Google’s chosen canonical.`,
|
|
34930
35088
|
"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.`,
|
|
34931
|
-
"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.
|
|
35089
|
+
"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.`,
|
|
34932
35090
|
bing: `Next: ${call("data", { source: "bing" })} for every Bing row; ${call("page")} for one page.`
|
|
34933
35091
|
}[section];
|
|
34934
35092
|
}
|
|
@@ -35432,7 +35590,7 @@ const RESEARCH_ACTIONS = [
|
|
|
35432
35590
|
"ai_answers"
|
|
35433
35591
|
];
|
|
35434
35592
|
const RESEARCH_PROVIDERS = ["dataforseo", "ahrefs"];
|
|
35435
|
-
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.
|
|
35593
|
+
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.`;
|
|
35436
35594
|
const GENERIC_PLATFORMS = [
|
|
35437
35595
|
"youtube.com",
|
|
35438
35596
|
"facebook.com",
|
|
@@ -36351,6 +36509,7 @@ const SHARED_TOOLS = [
|
|
|
36351
36509
|
action: z.enum(RESEARCH_ACTIONS),
|
|
36352
36510
|
keywords: z.array(z.string().min(1)).max(100).default([]),
|
|
36353
36511
|
target: z.string().default("").describe("Domain or URL."),
|
|
36512
|
+
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."),
|
|
36354
36513
|
country: z.string().regex(/^[a-z]{2}$/).default("us").describe("Two-letter country code."),
|
|
36355
36514
|
...detail
|
|
36356
36515
|
}),
|
|
@@ -36359,10 +36518,12 @@ const SHARED_TOOLS = [
|
|
|
36359
36518
|
if (input.action === "ai_answers") {
|
|
36360
36519
|
if (input.provider !== "dataforseo") throw new CommandError("INVALID_INPUT", "ai_answers uses dataforseo.");
|
|
36361
36520
|
return runAiAnswers({
|
|
36362
|
-
site: await surface.resolveSite(input.target),
|
|
36521
|
+
site: input.keywords.length || input.tasks.length ? input.target : await surface.resolveSite(input.target),
|
|
36363
36522
|
country: input.country,
|
|
36364
36523
|
detail: input.detail,
|
|
36365
|
-
offset: input.offset
|
|
36524
|
+
offset: input.offset,
|
|
36525
|
+
questions: input.keywords,
|
|
36526
|
+
tasks: input.tasks
|
|
36366
36527
|
}, await surface.keys(), surface.deps, more);
|
|
36367
36528
|
}
|
|
36368
36529
|
return runResearch(input, await surface.keys(), {
|