@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/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
- runCrawlabilityChecks(url).catch((err) => {
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
- var import_openai = __toESM(require("openai"));
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 openai = new import_openai.default({ apiKey: options.openaiApiKey });
20366
- const model = options.model || "gpt-4o-mini";
20418
+ const llm = summarizerLLM(options);
20419
+ const maxContent = llm.maxContentChars ?? 15e3;
20367
20420
  let content = crawlResult.aggregatedContent;
20368
- if (content.length > 15e3) {
20369
- content = content.substring(0, 15e3) + "\n...[truncated]";
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 response = await openai.chat.completions.create({
20376
- model,
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
- max_tokens: options.maxTokens || 2e3
20431
+ maxTokens: options.maxTokens || 2e3
20389
20432
  });
20390
- const responseText = response.choices[0]?.message?.content || "";
20391
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
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 = JSON.parse(jsonMatch[0]);
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 openai = new import_openai.default({ apiKey: options.openaiApiKey });
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 response = await openai.chat.completions.create({
20477
- model: options.model || "gpt-4o-mini",
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
- max_tokens: 1e3
20521
+ maxTokens: 1e3
20484
20522
  });
20485
- const responseText = response.choices[0]?.message?.content || "";
20486
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
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
- const response = await openai.embeddings.create({
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
- if (options.openaiApiKey && keywordsToCluster.length > 0) {
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
- openai,
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
- var import_openai3 = __toESM(require("openai"));
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 openai = new import_openai3.default({ apiKey: options.openaiApiKey });
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 response = await openai.chat.completions.create({
21454
- model: "gpt-4o-mini",
21455
- messages: [{ role: "user", content: prompt }],
21456
- response_format: { type: "json_object" },
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 openai = new import_openai3.default({ apiKey: options.openaiApiKey });
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 response = await openai.chat.completions.create({
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, openaiApiKey, maxPages = 20 } = options;
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
- if (!openaiApiKey) {
21586
- throw new Error("OpenAI API key required for paid tier keyword research");
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 { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
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
- openaiApiKey,
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 && openaiApiKey) {
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
- openaiApiKey,
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
- { openaiApiKey: options.openaiApiKey }
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
- if (options.openaiApiKey) {
22150
- siteSummary = await summarizeSite(crawlResult, {
22151
- openaiApiKey: options.openaiApiKey
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
- * Get ranking history for a keyword
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, _days = 30) {
35081
- const { data, error } = await this.supabase.from("keywords").select("current_position, target_url, last_checked").eq("id", keywordId).single();
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: data.current_position,
35087
- url: data.target_url,
35120
+ return data.map((row) => ({
35121
+ position: row.position,
35122
+ url: row.url,
35088
35123
  serpFeatures: [],
35089
- recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
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,