@rankcli/agent-runtime 0.0.22 → 0.0.24
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/{analyzer-GMURJADU.mjs → analyzer-MCBRQHB7.mjs} +1 -1
- package/dist/index.d.mts +53 -9
- package/dist/index.d.ts +53 -9
- package/dist/index.js +143 -105
- package/dist/index.mjs +140 -105
- package/package.json +1 -1
- package/dist/{chunk-PJLNXOLN.mjs → chunk-NATXE6B3.mjs} +2 -2
package/dist/index.js
CHANGED
|
@@ -71,7 +71,6 @@ async function httpGet(url, config = {}) {
|
|
|
71
71
|
signal: controller.signal,
|
|
72
72
|
redirect: maxRedirects > 0 ? "follow" : "manual"
|
|
73
73
|
});
|
|
74
|
-
clearTimeout(timeoutId);
|
|
75
74
|
const contentType = response.headers.get("content-type") || "";
|
|
76
75
|
let data;
|
|
77
76
|
if (contentType.includes("application/json")) {
|
|
@@ -79,6 +78,7 @@ async function httpGet(url, config = {}) {
|
|
|
79
78
|
} else {
|
|
80
79
|
data = await response.text();
|
|
81
80
|
}
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
82
|
if (!validateStatus(response.status)) {
|
|
83
83
|
const error = new Error(`Request failed with status ${response.status}`);
|
|
84
84
|
error.response = {
|
|
@@ -170,7 +170,6 @@ async function httpPost(url, data, config = {}) {
|
|
|
170
170
|
body: data ? JSON.stringify(data) : void 0,
|
|
171
171
|
signal: controller.signal
|
|
172
172
|
});
|
|
173
|
-
clearTimeout(timeoutId);
|
|
174
173
|
const contentType = response.headers.get("content-type") || "";
|
|
175
174
|
let responseData;
|
|
176
175
|
if (contentType.includes("application/json")) {
|
|
@@ -178,6 +177,7 @@ async function httpPost(url, data, config = {}) {
|
|
|
178
177
|
} else {
|
|
179
178
|
responseData = await response.text();
|
|
180
179
|
}
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
181
|
if (!validateStatus(response.status)) {
|
|
182
182
|
const error = new Error(`Request failed with status ${response.status}`);
|
|
183
183
|
error.response = {
|
|
@@ -3515,14 +3515,17 @@ __export(index_exports, {
|
|
|
3515
3515
|
injectGSCVerification: () => injectGSCVerification,
|
|
3516
3516
|
interpolatePrompt: () => interpolatePrompt,
|
|
3517
3517
|
isGitRepo: () => isGitRepo,
|
|
3518
|
+
keywordLLM: () => keywordLLM,
|
|
3518
3519
|
listFiles: () => listFiles,
|
|
3519
3520
|
loadAgent: () => loadAgent,
|
|
3520
3521
|
loadAgentByName: () => loadAgentByName,
|
|
3521
3522
|
mergePages: () => mergePages,
|
|
3523
|
+
openAIKeywordLLM: () => openAIKeywordLLM,
|
|
3522
3524
|
optimizeForAI: () => optimizeForAI,
|
|
3523
3525
|
optimizeReadme: () => optimizeReadme,
|
|
3524
3526
|
parseGEOResponse: () => parseGEOResponse,
|
|
3525
3527
|
parseGSCResponse: () => parseGSCResponse,
|
|
3528
|
+
parseJsonReply: () => parseJsonReply,
|
|
3526
3529
|
parseSitemap: () => parseSitemap,
|
|
3527
3530
|
prioritizeKeywords: () => prioritizeKeywords,
|
|
3528
3531
|
processWizardResponse: () => processWizardResponse,
|
|
@@ -8443,7 +8446,7 @@ async function checkRobotsTxt(baseUrl) {
|
|
|
8443
8446
|
}
|
|
8444
8447
|
async function checkSitemap(baseUrl, robotsData) {
|
|
8445
8448
|
const issues = [];
|
|
8446
|
-
const sitemapUrls = robotsData.sitemapUrls.length > 0 ? robotsData.sitemapUrls : [
|
|
8449
|
+
const sitemapUrls = robotsData.sitemapUrls.length > 0 ? robotsData.sitemapUrls.slice(0, 3) : [
|
|
8447
8450
|
new URL("/sitemap.xml", baseUrl).href,
|
|
8448
8451
|
new URL("/sitemap_index.xml", baseUrl).href
|
|
8449
8452
|
];
|
|
@@ -17532,9 +17535,22 @@ async function runFullAudit(options) {
|
|
|
17532
17535
|
console.error(`
|
|
17533
17536
|
\u{1F50D} Running comprehensive SEO audit on ${url}...
|
|
17534
17537
|
`);
|
|
17538
|
+
const withTimeout = async (name, promise, timeoutMs) => {
|
|
17539
|
+
let timer;
|
|
17540
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
17541
|
+
timer = setTimeout(() => reject(new Error(`${name} timed out`)), timeoutMs);
|
|
17542
|
+
});
|
|
17543
|
+
try {
|
|
17544
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
17545
|
+
} finally {
|
|
17546
|
+
clearTimeout(timer);
|
|
17547
|
+
}
|
|
17548
|
+
};
|
|
17535
17549
|
console.error("\u{1F4CB} Phase 1: Crawlability checks + page fetch (parallel)...");
|
|
17536
17550
|
const [crawlabilityResult, fetchResult] = await Promise.all([
|
|
17537
|
-
|
|
17551
|
+
// robots.txt, sitemaps and the redirect chain run one after another; a site that is slow
|
|
17552
|
+
// on each could hold the page for minutes, so the whole group gets 45s.
|
|
17553
|
+
withTimeout("Crawlability checks", runCrawlabilityChecks(url), 45e3).catch((err) => {
|
|
17538
17554
|
console.error("Crawlability check failed:", err);
|
|
17539
17555
|
return [];
|
|
17540
17556
|
}),
|
|
@@ -17685,17 +17701,6 @@ async function runFullAudit(options) {
|
|
|
17685
17701
|
}
|
|
17686
17702
|
}
|
|
17687
17703
|
console.error("\u{1F517} Phase 3: Running async checks (parallel)...");
|
|
17688
|
-
const withTimeout = async (name, promise, timeoutMs) => {
|
|
17689
|
-
let timer;
|
|
17690
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
17691
|
-
timer = setTimeout(() => reject(new Error(`${name} timed out`)), timeoutMs);
|
|
17692
|
-
});
|
|
17693
|
-
try {
|
|
17694
|
-
return await Promise.race([promise, timeoutPromise]);
|
|
17695
|
-
} finally {
|
|
17696
|
-
clearTimeout(timer);
|
|
17697
|
-
}
|
|
17698
|
-
};
|
|
17699
17704
|
const safeAsync = async (name, fn, timeoutMs = 1e4) => {
|
|
17700
17705
|
try {
|
|
17701
17706
|
const result = await withTimeout(name, fn(), timeoutMs);
|
|
@@ -20109,6 +20114,50 @@ async function extractSeedKeywords(url) {
|
|
|
20109
20114
|
}
|
|
20110
20115
|
}
|
|
20111
20116
|
|
|
20117
|
+
// src/keywords/llm.ts
|
|
20118
|
+
var import_openai = __toESM(require("openai"));
|
|
20119
|
+
function openAIKeywordLLM(apiKey, opts = {}) {
|
|
20120
|
+
const openai = new import_openai.default({ apiKey });
|
|
20121
|
+
const openAIModel = opts.model;
|
|
20122
|
+
return {
|
|
20123
|
+
async complete(prompt, opts2 = {}) {
|
|
20124
|
+
const response = await openai.chat.completions.create({
|
|
20125
|
+
model: opts2.best ? "gpt-4o" : openAIModel ?? "gpt-4o-mini",
|
|
20126
|
+
messages: [
|
|
20127
|
+
...opts2.system ? [{ role: "system", content: opts2.system }] : [],
|
|
20128
|
+
{ role: "user", content: prompt }
|
|
20129
|
+
],
|
|
20130
|
+
response_format: { type: "json_object" },
|
|
20131
|
+
temperature: opts2.temperature ?? 0.3,
|
|
20132
|
+
...opts2.maxTokens ? { max_tokens: opts2.maxTokens } : {}
|
|
20133
|
+
});
|
|
20134
|
+
return response.choices[0]?.message?.content || "";
|
|
20135
|
+
},
|
|
20136
|
+
async embed(texts) {
|
|
20137
|
+
const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: texts });
|
|
20138
|
+
return response.data.map((item) => item.embedding);
|
|
20139
|
+
}
|
|
20140
|
+
};
|
|
20141
|
+
}
|
|
20142
|
+
function keywordLLM(options) {
|
|
20143
|
+
return options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey) : void 0);
|
|
20144
|
+
}
|
|
20145
|
+
function parseJsonReply(text) {
|
|
20146
|
+
const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
|
|
20147
|
+
const start = cleaned.indexOf("{");
|
|
20148
|
+
const end = cleaned.lastIndexOf("}");
|
|
20149
|
+
if (start === -1 || end <= start) return null;
|
|
20150
|
+
const candidate = cleaned.slice(start, end + 1);
|
|
20151
|
+
for (const attempt of [candidate, candidate.replace(/,\s*([}\]])/g, "$1")]) {
|
|
20152
|
+
try {
|
|
20153
|
+
const parsed = JSON.parse(attempt);
|
|
20154
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
20155
|
+
} catch {
|
|
20156
|
+
}
|
|
20157
|
+
}
|
|
20158
|
+
return null;
|
|
20159
|
+
}
|
|
20160
|
+
|
|
20112
20161
|
// src/keywords/site-crawler.ts
|
|
20113
20162
|
var cheerio40 = __toESM(require("cheerio"));
|
|
20114
20163
|
init_http();
|
|
@@ -20319,7 +20368,11 @@ function extractKeyPhrases(crawlResult) {
|
|
|
20319
20368
|
}
|
|
20320
20369
|
|
|
20321
20370
|
// src/keywords/ai-summarizer.ts
|
|
20322
|
-
|
|
20371
|
+
function summarizerLLM(options) {
|
|
20372
|
+
const llm = options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey, { model: options.model }) : void 0);
|
|
20373
|
+
if (!llm) throw new Error("An AI provider is required to summarize the site");
|
|
20374
|
+
return llm;
|
|
20375
|
+
}
|
|
20323
20376
|
var SUMMARIZER_PROMPT = `You are an expert SEO analyst. Analyze the following website content and extract key information for keyword research.
|
|
20324
20377
|
|
|
20325
20378
|
WEBSITE CONTENT:
|
|
@@ -20362,37 +20415,26 @@ IMPORTANT:
|
|
|
20362
20415
|
|
|
20363
20416
|
Respond ONLY with the JSON object, no other text.`;
|
|
20364
20417
|
async function summarizeSite(crawlResult, options) {
|
|
20365
|
-
const
|
|
20366
|
-
const
|
|
20418
|
+
const llm = summarizerLLM(options);
|
|
20419
|
+
const maxContent = llm.maxContentChars ?? 15e3;
|
|
20367
20420
|
let content = crawlResult.aggregatedContent;
|
|
20368
|
-
if (content.length >
|
|
20369
|
-
content = content.substring(0,
|
|
20421
|
+
if (content.length > maxContent) {
|
|
20422
|
+
content = content.substring(0, maxContent) + "\n...[truncated]";
|
|
20370
20423
|
}
|
|
20371
20424
|
const headings = crawlResult.uniqueHeadings.slice(0, 30).join("\n");
|
|
20372
20425
|
const prompt = SUMMARIZER_PROMPT.replace("{content}", content).replace("{headings}", headings).replace("{productPages}", String(crawlResult.detectedPageTypes.product)).replace("{pricingPages}", String(crawlResult.detectedPageTypes.pricing)).replace("{blogPages}", String(crawlResult.detectedPageTypes.blog)).replace("{featurePages}", String(crawlResult.detectedPageTypes.feature));
|
|
20373
20426
|
console.log("\u{1F916} Analyzing site content with AI...");
|
|
20374
20427
|
try {
|
|
20375
|
-
const
|
|
20376
|
-
|
|
20377
|
-
messages: [
|
|
20378
|
-
{
|
|
20379
|
-
role: "system",
|
|
20380
|
-
content: "You are an expert SEO analyst. Always respond with valid JSON only."
|
|
20381
|
-
},
|
|
20382
|
-
{
|
|
20383
|
-
role: "user",
|
|
20384
|
-
content: prompt
|
|
20385
|
-
}
|
|
20386
|
-
],
|
|
20428
|
+
const responseText = await llm.complete(prompt, {
|
|
20429
|
+
system: "You are an expert SEO analyst. Always respond with valid JSON only.",
|
|
20387
20430
|
temperature: 0.3,
|
|
20388
|
-
|
|
20431
|
+
maxTokens: options.maxTokens || 2e3
|
|
20389
20432
|
});
|
|
20390
|
-
const
|
|
20391
|
-
|
|
20392
|
-
if (!jsonMatch) {
|
|
20433
|
+
const parsed = parseJsonReply(responseText);
|
|
20434
|
+
if (!parsed) {
|
|
20393
20435
|
throw new Error("No JSON found in response");
|
|
20394
20436
|
}
|
|
20395
|
-
const summary =
|
|
20437
|
+
const summary = parsed;
|
|
20396
20438
|
return {
|
|
20397
20439
|
productName: summary.productName || "Unknown Product",
|
|
20398
20440
|
productDescription: summary.productDescription || "No description available",
|
|
@@ -20454,7 +20496,7 @@ function createFallbackSummary(crawlResult) {
|
|
|
20454
20496
|
};
|
|
20455
20497
|
}
|
|
20456
20498
|
async function enhanceSummaryWithCompetitors(summary, competitors, options) {
|
|
20457
|
-
const
|
|
20499
|
+
const llm = summarizerLLM(options);
|
|
20458
20500
|
const prompt = `Given this product summary:
|
|
20459
20501
|
Product: ${summary.productName}
|
|
20460
20502
|
Description: ${summary.productDescription}
|
|
@@ -20473,19 +20515,13 @@ Analyze the competitive positioning. If this product focuses on a niche segment
|
|
|
20473
20515
|
"updatedUncertainties": ["uncertainty1", "uncertainty2"]
|
|
20474
20516
|
}`;
|
|
20475
20517
|
try {
|
|
20476
|
-
const
|
|
20477
|
-
|
|
20478
|
-
messages: [
|
|
20479
|
-
{ role: "system", content: "Respond with valid JSON only." },
|
|
20480
|
-
{ role: "user", content: prompt }
|
|
20481
|
-
],
|
|
20518
|
+
const responseText = await llm.complete(prompt, {
|
|
20519
|
+
system: "Respond with valid JSON only.",
|
|
20482
20520
|
temperature: 0.3,
|
|
20483
|
-
|
|
20521
|
+
maxTokens: 1e3
|
|
20484
20522
|
});
|
|
20485
|
-
const
|
|
20486
|
-
|
|
20487
|
-
if (jsonMatch) {
|
|
20488
|
-
const analysis = JSON.parse(jsonMatch[0]);
|
|
20523
|
+
const analysis = parseJsonReply(responseText);
|
|
20524
|
+
if (analysis) {
|
|
20489
20525
|
return {
|
|
20490
20526
|
...summary,
|
|
20491
20527
|
competitivePositioning: analysis.competitivePositioning || "",
|
|
@@ -20572,7 +20608,6 @@ function generateUncertaintyQuestions(summary) {
|
|
|
20572
20608
|
}
|
|
20573
20609
|
|
|
20574
20610
|
// src/keywords/nlp-analysis.ts
|
|
20575
|
-
var import_openai2 = __toESM(require("openai"));
|
|
20576
20611
|
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
20577
20612
|
"a",
|
|
20578
20613
|
"an",
|
|
@@ -20857,19 +20892,18 @@ function createTermVectors(keywords, vocabulary) {
|
|
|
20857
20892
|
return vector;
|
|
20858
20893
|
});
|
|
20859
20894
|
}
|
|
20895
|
+
async function embedBatch(embedder, batch) {
|
|
20896
|
+
if (typeof embedder === "function") return embedder(batch);
|
|
20897
|
+
const response = await embedder.embeddings.create({ model: "text-embedding-3-small", input: batch });
|
|
20898
|
+
return response.data.map((item) => item.embedding);
|
|
20899
|
+
}
|
|
20860
20900
|
async function getEmbeddings(texts, openai) {
|
|
20861
20901
|
const embeddings = [];
|
|
20862
20902
|
const batchSize = 100;
|
|
20863
20903
|
for (let i = 0; i < texts.length; i += batchSize) {
|
|
20864
20904
|
const batch = texts.slice(i, i + batchSize);
|
|
20865
20905
|
try {
|
|
20866
|
-
|
|
20867
|
-
model: "text-embedding-3-small",
|
|
20868
|
-
input: batch
|
|
20869
|
-
});
|
|
20870
|
-
for (const item of response.data) {
|
|
20871
|
-
embeddings.push(item.embedding);
|
|
20872
|
-
}
|
|
20906
|
+
embeddings.push(...await embedBatch(openai, batch));
|
|
20873
20907
|
} catch (error) {
|
|
20874
20908
|
console.warn("Embedding API failed, using fallback BOW vectors");
|
|
20875
20909
|
for (const text of batch) {
|
|
@@ -21024,12 +21058,12 @@ async function runNLPAnalysis(content, options = {}) {
|
|
|
21024
21058
|
const entityPhrases = extractEntityPhrases(combinedText);
|
|
21025
21059
|
let clusters = [];
|
|
21026
21060
|
const keywordsToCluster = tfidfKeywords.slice(0, 50).map((k) => k.term);
|
|
21027
|
-
|
|
21061
|
+
const embed = keywordLLM(options)?.embed;
|
|
21062
|
+
if (embed && keywordsToCluster.length > 0) {
|
|
21028
21063
|
try {
|
|
21029
|
-
const openai = new import_openai2.default({ apiKey: options.openaiApiKey });
|
|
21030
21064
|
clusters = await clusterKeywordsByEmbedding(
|
|
21031
21065
|
keywordsToCluster,
|
|
21032
|
-
|
|
21066
|
+
(texts) => embed(texts),
|
|
21033
21067
|
options.numClusters
|
|
21034
21068
|
);
|
|
21035
21069
|
} catch (error) {
|
|
@@ -21372,9 +21406,13 @@ async function searchHackerNews(query, maxResults = 10) {
|
|
|
21372
21406
|
}
|
|
21373
21407
|
|
|
21374
21408
|
// src/keywords/llm-judge.ts
|
|
21375
|
-
|
|
21409
|
+
function judgeLLM(options) {
|
|
21410
|
+
const llm = keywordLLM(options);
|
|
21411
|
+
if (!llm) throw new Error("An AI provider is required to judge tool ideas");
|
|
21412
|
+
return llm;
|
|
21413
|
+
}
|
|
21376
21414
|
async function evaluateToolFeasibility(toolIdea, siteSummary, competitiveData, options) {
|
|
21377
|
-
const
|
|
21415
|
+
const llm = judgeLLM(options);
|
|
21378
21416
|
const minFeasibility = options.minFeasibilityScore ?? 3;
|
|
21379
21417
|
const minSpecificity = options.minSpecificityScore ?? 3;
|
|
21380
21418
|
const minProductFit = options.minProductFitScore ?? 2;
|
|
@@ -21450,13 +21488,10 @@ Respond in JSON:
|
|
|
21450
21488
|
"blockers": ["<issues that prevent building>"],
|
|
21451
21489
|
"improvements": ["<suggestions to make it more specific/buildable>"]
|
|
21452
21490
|
}`;
|
|
21453
|
-
const
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
|
|
21457
|
-
temperature: 0.3
|
|
21458
|
-
});
|
|
21459
|
-
const result = JSON.parse(response.choices[0].message.content || "{}");
|
|
21491
|
+
const result = parseJsonReply(await llm.complete(prompt, { temperature: 0.3 })) ?? {};
|
|
21492
|
+
for (const key of ["feasibility", "specificity", "productFit", "marketOpportunity"]) {
|
|
21493
|
+
result[key] = Number(result[key]) || 0;
|
|
21494
|
+
}
|
|
21460
21495
|
const overallScore = result.feasibility * 0.3 + result.specificity * 0.3 + result.productFit * 0.2 + result.marketOpportunity * 0.2;
|
|
21461
21496
|
const passes = result.feasibility >= minFeasibility && result.specificity >= minSpecificity && result.productFit >= minProductFit;
|
|
21462
21497
|
return {
|
|
@@ -21472,7 +21507,7 @@ Respond in JSON:
|
|
|
21472
21507
|
};
|
|
21473
21508
|
}
|
|
21474
21509
|
async function enhanceToolIdea(toolIdea, siteSummary, competitiveData, options) {
|
|
21475
|
-
const
|
|
21510
|
+
const llm = judgeLLM(options);
|
|
21476
21511
|
const feasibilityScore = await evaluateToolFeasibility(
|
|
21477
21512
|
toolIdea,
|
|
21478
21513
|
siteSummary,
|
|
@@ -21517,13 +21552,7 @@ ${competitiveData?.openSourceOptions.slice(0, 3).map((t) => `- OSS: ${t.name} ($
|
|
|
21517
21552
|
"canUseExistingOSS": <true if an OSS library does most of the work>,
|
|
21518
21553
|
"ossLibrary": "<name of OSS library to use, or null>"
|
|
21519
21554
|
}`;
|
|
21520
|
-
const
|
|
21521
|
-
model: "gpt-4o",
|
|
21522
|
-
messages: [{ role: "user", content: prompt }],
|
|
21523
|
-
response_format: { type: "json_object" },
|
|
21524
|
-
temperature: 0.5
|
|
21525
|
-
});
|
|
21526
|
-
const enhanced = JSON.parse(response.choices[0].message.content || "{}");
|
|
21555
|
+
const enhanced = parseJsonReply(await llm.complete(prompt, { temperature: 0.5, best: true })) ?? {};
|
|
21527
21556
|
const priority = Math.round(
|
|
21528
21557
|
feasibilityScore.overallScore * 20 + toolIdea.volume / 1e3 * 10 + (100 - toolIdea.difficulty) / 100 * 20
|
|
21529
21558
|
);
|
|
@@ -21574,7 +21603,7 @@ async function evaluateAndEnhanceToolIdeas(toolIdeas, siteSummary, getCompetitiv
|
|
|
21574
21603
|
|
|
21575
21604
|
// src/keywords/ai-keyword-engine.ts
|
|
21576
21605
|
async function runAIKeywordResearch(options) {
|
|
21577
|
-
const { url, tier,
|
|
21606
|
+
const { url, tier, maxPages = 20 } = options;
|
|
21578
21607
|
console.log(`
|
|
21579
21608
|
\u{1F52C} Starting AI Keyword Research (${tier} tier)...
|
|
21580
21609
|
`);
|
|
@@ -21582,10 +21611,11 @@ async function runAIKeywordResearch(options) {
|
|
|
21582
21611
|
if (tier === "free") {
|
|
21583
21612
|
return runFreeKeywordResearch(crawlResult, options);
|
|
21584
21613
|
} else {
|
|
21585
|
-
|
|
21586
|
-
|
|
21614
|
+
const llm = keywordLLM(options);
|
|
21615
|
+
if (!llm) {
|
|
21616
|
+
throw new Error("An AI provider (or an OpenAI API key) is required for paid tier keyword research");
|
|
21587
21617
|
}
|
|
21588
|
-
return runPaidKeywordResearch(crawlResult, options);
|
|
21618
|
+
return runPaidKeywordResearch(crawlResult, options, llm);
|
|
21589
21619
|
}
|
|
21590
21620
|
}
|
|
21591
21621
|
async function runFreeKeywordResearch(crawlResult, options) {
|
|
@@ -21639,13 +21669,11 @@ async function runFreeKeywordResearch(crawlResult, options) {
|
|
|
21639
21669
|
]
|
|
21640
21670
|
};
|
|
21641
21671
|
}
|
|
21642
|
-
async function runPaidKeywordResearch(crawlResult, options) {
|
|
21643
|
-
const {
|
|
21672
|
+
async function runPaidKeywordResearch(crawlResult, options, llm) {
|
|
21673
|
+
const { userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
|
|
21644
21674
|
const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false;
|
|
21645
21675
|
console.log("\u{1F680} Running full AI analysis...");
|
|
21646
|
-
let siteSummary = await summarizeSite(crawlResult, {
|
|
21647
|
-
openaiApiKey
|
|
21648
|
-
});
|
|
21676
|
+
let siteSummary = await summarizeSite(crawlResult, { llm });
|
|
21649
21677
|
if (userContext) {
|
|
21650
21678
|
siteSummary = applyUserContext(siteSummary, userContext);
|
|
21651
21679
|
}
|
|
@@ -21654,7 +21682,7 @@ async function runPaidKeywordResearch(crawlResult, options) {
|
|
|
21654
21682
|
}
|
|
21655
21683
|
const documents = crawlResult.pages.map((p) => p.mainContent);
|
|
21656
21684
|
const nlpAnalysis = await runNLPAnalysis(documents, {
|
|
21657
|
-
|
|
21685
|
+
llm,
|
|
21658
21686
|
numClusters: 8,
|
|
21659
21687
|
numTopics: 6
|
|
21660
21688
|
});
|
|
@@ -21681,13 +21709,13 @@ async function runPaidKeywordResearch(crawlResult, options) {
|
|
|
21681
21709
|
nlpAnalysis
|
|
21682
21710
|
);
|
|
21683
21711
|
let freeToolIdeas;
|
|
21684
|
-
if (enableEnhancedToolIdeas
|
|
21712
|
+
if (enableEnhancedToolIdeas) {
|
|
21685
21713
|
console.log("\u{1F52C} Running enhanced tool idea analysis with competitive research...");
|
|
21686
21714
|
freeToolIdeas = await generateEnhancedFreeToolIdeas(
|
|
21687
21715
|
opportunities,
|
|
21688
21716
|
siteSummary,
|
|
21689
21717
|
{
|
|
21690
|
-
|
|
21718
|
+
llm,
|
|
21691
21719
|
...competitiveSearchKeys
|
|
21692
21720
|
}
|
|
21693
21721
|
);
|
|
@@ -21785,7 +21813,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
|
|
|
21785
21813
|
kw.volume >= 300 && // Slightly lower threshold
|
|
21786
21814
|
(kw.keyword.includes("generator") || kw.keyword.includes("tool") || kw.keyword.includes("maker") || kw.keyword.includes("builder") || kw.keyword.includes("converter") || kw.keyword.includes("calculator") || kw.keyword.includes("checker") || kw.keyword.includes("validator") || kw.keyword.includes("formatter") || kw.keyword.includes("free") || kw.intent === "informational")
|
|
21787
21815
|
);
|
|
21788
|
-
const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
|
|
21816
|
+
const basicIdeas = toolKeywords.slice(0, options.llm.maxToolIdeas ?? 15).map((kw) => ({
|
|
21789
21817
|
keyword: kw.keyword,
|
|
21790
21818
|
toolName: generateToolName(kw.keyword),
|
|
21791
21819
|
toolDescription: generateToolDescription(kw.keyword, summary),
|
|
@@ -21815,7 +21843,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
|
|
|
21815
21843
|
basicIdeas,
|
|
21816
21844
|
summary,
|
|
21817
21845
|
getCompetitiveData,
|
|
21818
|
-
{
|
|
21846
|
+
{ llm: options.llm }
|
|
21819
21847
|
);
|
|
21820
21848
|
return enhancedIdeas.map((enhanced) => ({
|
|
21821
21849
|
keyword: enhanced.keyword,
|
|
@@ -22146,10 +22174,9 @@ async function startWizardSession(url, options = {}) {
|
|
|
22146
22174
|
console.log("\u{1F50D} Analyzing your site...");
|
|
22147
22175
|
const crawlResult = await crawlSite(url, { maxPages: 10 });
|
|
22148
22176
|
let siteSummary;
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
});
|
|
22177
|
+
const llm = keywordLLM(options);
|
|
22178
|
+
if (llm) {
|
|
22179
|
+
siteSummary = await summarizeSite(crawlResult, { llm });
|
|
22153
22180
|
} else {
|
|
22154
22181
|
siteSummary = {
|
|
22155
22182
|
productName: crawlResult.domain,
|
|
@@ -35070,24 +35097,32 @@ var RankTracker = class {
|
|
|
35070
35097
|
}).eq("id", keywordId);
|
|
35071
35098
|
if (updateError) {
|
|
35072
35099
|
console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
|
|
35100
|
+
return;
|
|
35101
|
+
}
|
|
35102
|
+
const { error: historyError } = await this.supabase.from("keyword_position_history").insert({
|
|
35103
|
+
keyword_id: keywordId,
|
|
35104
|
+
position: result.position,
|
|
35105
|
+
url: result.url,
|
|
35106
|
+
checked_at: result.checkedAt.toISOString()
|
|
35107
|
+
});
|
|
35108
|
+
if (historyError) {
|
|
35109
|
+
console.error(`Failed to record history for ${result.keyword}:`, historyError);
|
|
35073
35110
|
}
|
|
35074
35111
|
}
|
|
35075
35112
|
/**
|
|
35076
|
-
*
|
|
35077
|
-
* Note: Full history requires keyword_ranking_history table with keyword_ranking_id
|
|
35078
|
-
* For now, returns current state as single history entry
|
|
35113
|
+
* Ranking history for a keyword: one entry per check in the last `days`, newest first.
|
|
35079
35114
|
*/
|
|
35080
|
-
async getHistory(keywordId,
|
|
35081
|
-
const { data, error } = await this.supabase.from("
|
|
35115
|
+
async getHistory(keywordId, days = 30) {
|
|
35116
|
+
const { data, error } = await this.supabase.from("keyword_position_history").select("position, url, checked_at").eq("keyword_id", keywordId).gte("checked_at", new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString()).order("checked_at", { ascending: false });
|
|
35082
35117
|
if (error || !data) {
|
|
35083
35118
|
return [];
|
|
35084
35119
|
}
|
|
35085
|
-
return
|
|
35086
|
-
position:
|
|
35087
|
-
url:
|
|
35120
|
+
return data.map((row) => ({
|
|
35121
|
+
position: row.position,
|
|
35122
|
+
url: row.url,
|
|
35088
35123
|
serpFeatures: [],
|
|
35089
|
-
recordedAt:
|
|
35090
|
-
}
|
|
35124
|
+
recordedAt: new Date(row.checked_at)
|
|
35125
|
+
}));
|
|
35091
35126
|
}
|
|
35092
35127
|
/**
|
|
35093
35128
|
* Get keyword trends for a project
|
|
@@ -35592,14 +35627,17 @@ var SUPPORTED_FRAMEWORKS = [
|
|
|
35592
35627
|
injectGSCVerification,
|
|
35593
35628
|
interpolatePrompt,
|
|
35594
35629
|
isGitRepo,
|
|
35630
|
+
keywordLLM,
|
|
35595
35631
|
listFiles,
|
|
35596
35632
|
loadAgent,
|
|
35597
35633
|
loadAgentByName,
|
|
35598
35634
|
mergePages,
|
|
35635
|
+
openAIKeywordLLM,
|
|
35599
35636
|
optimizeForAI,
|
|
35600
35637
|
optimizeReadme,
|
|
35601
35638
|
parseGEOResponse,
|
|
35602
35639
|
parseGSCResponse,
|
|
35640
|
+
parseJsonReply,
|
|
35603
35641
|
parseSitemap,
|
|
35604
35642
|
prioritizeKeywords,
|
|
35605
35643
|
processWizardResponse,
|