@rankcli/agent-runtime 0.0.23 → 0.0.25

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.d.mts CHANGED
@@ -1631,6 +1631,39 @@ declare function runKeywordResearch(options: KeywordResearchOptions): Promise<Ke
1631
1631
  declare function formatKeywordReport(result: KeywordResearchResult): string;
1632
1632
  declare function extractSeedKeywords(url: string): Promise<string[]>;
1633
1633
 
1634
+ interface KeywordLLM {
1635
+ /**
1636
+ * A reply to one prompt that asks for a JSON object. `best` asks for the provider's stronger
1637
+ * model where it has one. Replies are parsed with parseJsonReply, never trusted to be clean.
1638
+ */
1639
+ complete(prompt: string, opts?: {
1640
+ system?: string;
1641
+ maxTokens?: number;
1642
+ temperature?: number;
1643
+ best?: boolean;
1644
+ }): Promise<string>;
1645
+ /** Embeddings for keyword clustering. Without it, clustering falls back to term vectors. */
1646
+ embed?(texts: string[]): Promise<number[][]>;
1647
+ /** Most page text worth sending in one prompt: small-context models get less. */
1648
+ maxContentChars?: number;
1649
+ /** How many tool ideas to judge (two calls each): slow local models get fewer. */
1650
+ maxToolIdeas?: number;
1651
+ }
1652
+ /** The engine's original backend: gpt-4o-mini, gpt-4o for `best`, JSON mode, text-embedding-3-small. */
1653
+ declare function openAIKeywordLLM(apiKey: string, opts?: {
1654
+ model?: string;
1655
+ }): KeywordLLM;
1656
+ /** The model to use: an explicit one, else OpenAI when there's a key, else none. */
1657
+ declare function keywordLLM(options: {
1658
+ llm?: KeywordLLM;
1659
+ openaiApiKey?: string;
1660
+ }): KeywordLLM | undefined;
1661
+ /**
1662
+ * The JSON object in a model reply. Local and small models wrap it in ``` fences, prepend prose
1663
+ * or emit <think> blocks; this takes the outermost {...} and tolerates trailing commas.
1664
+ */
1665
+ declare function parseJsonReply(text: string): Record<string, any> | null;
1666
+
1634
1667
  /**
1635
1668
  * Site Crawler for Keyword Research
1636
1669
  *
@@ -1687,7 +1720,7 @@ declare function extractKeyPhrases(crawlResult: SiteCrawlResult): string[];
1687
1720
  /**
1688
1721
  * AI-Powered Site Summarizer
1689
1722
  *
1690
- * Uses OpenAI to understand what a site does from crawled content.
1723
+ * Uses the configured AI provider (llm.ts) to understand what a site does from crawled content.
1691
1724
  * Extracts: product, audience, features, industry, positioning.
1692
1725
  */
1693
1726
 
@@ -1724,7 +1757,10 @@ interface SiteSummary {
1724
1757
  uncertainties: string[];
1725
1758
  }
1726
1759
  interface SummarizerOptions {
1727
- openaiApiKey: string;
1760
+ /** The model to ask (see llm.ts); or an OpenAI key, as before. */
1761
+ llm?: KeywordLLM;
1762
+ openaiApiKey?: string;
1763
+ /** OpenAI model override, with openaiApiKey. */
1728
1764
  model?: string;
1729
1765
  maxTokens?: number;
1730
1766
  }
@@ -1831,10 +1867,12 @@ declare function calculateBM25(documents: string[], query: string, k1?: number,
1831
1867
  docIndex: number;
1832
1868
  score: number;
1833
1869
  }>;
1870
+ /** An OpenAI client, or any function returning one embedding per text (llm.ts). */
1871
+ type Embedder = OpenAI | ((texts: string[]) => Promise<number[][]>);
1834
1872
  /**
1835
1873
  * Cluster keywords using embeddings
1836
1874
  */
1837
- declare function clusterKeywordsByEmbedding(keywords: string[], openai: OpenAI, numClusters?: number): Promise<KeywordCluster[]>;
1875
+ declare function clusterKeywordsByEmbedding(keywords: string[], openai: Embedder, numClusters?: number): Promise<KeywordCluster[]>;
1838
1876
  /**
1839
1877
  * Extract entity-like phrases (capitalized sequences, proper nouns)
1840
1878
  */
@@ -1848,6 +1886,8 @@ declare function extractTopics(documents: string[], numTopics?: number): TopicMo
1848
1886
  * Run full NLP analysis on content
1849
1887
  */
1850
1888
  declare function runNLPAnalysis(content: string | string[], options?: {
1889
+ /** Embeddings come from here when it has them (see llm.ts); or an OpenAI key, as before. */
1890
+ llm?: KeywordLLM;
1851
1891
  openaiApiKey?: string;
1852
1892
  numClusters?: number;
1853
1893
  numTopics?: number;
@@ -1871,6 +1911,8 @@ type PlanTier = 'free' | 'solo' | 'pro' | 'agency';
1871
1911
  interface AIKeywordResearchOptions {
1872
1912
  url: string;
1873
1913
  tier: PlanTier;
1914
+ /** The model to ask for paid-tier research (see llm.ts); or an OpenAI key, as before. */
1915
+ llm?: KeywordLLM;
1874
1916
  openaiApiKey?: string;
1875
1917
  maxPages?: number;
1876
1918
  userContext?: {
@@ -2066,6 +2108,8 @@ interface WizardResult {
2066
2108
  * Start a new wizard session
2067
2109
  */
2068
2110
  declare function startWizardSession(url: string, options?: {
2111
+ /** The model for the site summary (see llm.ts); or an OpenAI key, as before. */
2112
+ llm?: KeywordLLM;
2069
2113
  openaiApiKey?: string;
2070
2114
  existingResponses?: WizardResponse[];
2071
2115
  }): Promise<WizardSession>;
@@ -2370,7 +2414,9 @@ interface EnhancedToolIdea {
2370
2414
  priority: number;
2371
2415
  }
2372
2416
  interface LLMJudgeOptions {
2373
- openaiApiKey: string;
2417
+ /** The model to ask (see llm.ts); or an OpenAI key, as before. */
2418
+ llm?: KeywordLLM;
2419
+ openaiApiKey?: string;
2374
2420
  minFeasibilityScore?: number;
2375
2421
  minSpecificityScore?: number;
2376
2422
  minProductFitScore?: number;
@@ -4691,11 +4737,9 @@ declare class RankTracker {
4691
4737
  */
4692
4738
  private saveRanking;
4693
4739
  /**
4694
- * Get ranking history for a keyword
4695
- * Note: Full history requires keyword_ranking_history table with keyword_ranking_id
4696
- * For now, returns current state as single history entry
4740
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
4697
4741
  */
4698
- getHistory(keywordId: string, _days?: number): Promise<RankHistory[]>;
4742
+ getHistory(keywordId: string, days?: number): Promise<RankHistory[]>;
4699
4743
  /**
4700
4744
  * Get keyword trends for a project
4701
4745
  */
@@ -5498,4 +5542,4 @@ declare namespace index {
5498
5542
  export { type index_Framework as Framework, type index_FrameworkCheck as FrameworkCheck, type index_FrameworkInfo as FrameworkInfo, type index_FrameworkRecipe as FrameworkRecipe, index_SUPPORTED_FRAMEWORKS as SUPPORTED_FRAMEWORKS, index_detectFramework as detectFramework, index_detectFromHeaders as detectFromHeaders, index_detectFromHtml as detectFromHtml, index_detectFromPackageJson as detectFromPackageJson, index_embedRecipe as embedRecipe, index_getFrameworkChecks as getFrameworkChecks, index_getFrameworkDisplayName as getFrameworkDisplayName, index_getFrameworkIssues as getFrameworkIssues, index_getFrameworkSuggestions as getFrameworkSuggestions, index_getFrameworkSummary as getFrameworkSummary, index_getMigrationPaths as getMigrationPaths, index_getPerformanceTips as getPerformanceTips, index_getSeoChallenges as getSeoChallenges, index_getTotalFrameworkChecks as getTotalFrameworkChecks, index_loadRecipe as loadRecipe };
5499
5543
  }
5500
5544
 
5501
- export { type AIContentConfig, type AIKeywordResearchOptions, type AgentDefinition, type AgentTool, type AlertChannel, type AlertConfig, type AlertMessage, type AlertResult, type AlertSeverity, type AlertType, type AuditAlertPayload, type AuditIssue, type AuditIssueData, type AuditOptions, type AuditPRResult, type AuditReport, type AuditReportData, type AuditRunnerOptions, type AuditRunnerResult, type AuditSchedule, type BlogPostConfig, type BrandConfig, type CIAction, type CIKeywordOptions, type CIKeywordResult, COMMIT_TYPES, type ChangelogConfig, type ChangelogResult, type CitationCheckOptions, type CommitConfig, type ComparisonConfig, type ComparisonRow, type CompetitiveInsight, type CompetitiveSearchOptions, type CompetitiveSearchResult, type CompetitorComparison, type CompetitorKeywordResult, type CompetitorOverlap, type CompetitorTool, type CompetitorUrl, type ContentElements, type ContentGeneratorOptions, type ContentRecommendation, type ConventionalCommit, type CrawlResult, type CrawlStats, type CrawledPage, type CreateAuditPROptions, type CreatePROptions, type DataForSEOCredentials, type DiscoveredPage, type EnhancedToolIdea, type ExecOptions, type ExecuteOptions, type ExecutionResult, type FAQItem, type FAQSchemaOptions, type FeaturedSnippetAnalysis, type Fix, type FixFile, type FixGeneratorOptions, type FixResult, type FrameworkInfo$1 as FrameworkInfo, type FreeKeywordResult, type FreeToolIdea, type GA4Config, type GEOAlert, type GEOAlertType, type GEOHistory, type GEOHistoryOptions, type GEOQuery, type GEOReport, type GEOResult, type GEOTrend, type GSCConfig, type GSCCredentials, type GSCPerformanceData, type GSCQueryData, type GSCQueryResult, type GeneratedCode, type GeneratedContent, type GeneratedFix, type HeadingStructure, type HeadlineAnalysis, type HealthScore, ISSUE_DEFINITIONS, type ImageInfo$2 as ImageInfo, type InjectionResult, type IntentAnalysis, type IssueCategory, type IssueDefinition, type IssueSeverity, type KeyFact, type KeyFactsOptions, type KeywordAction, type KeywordCluster, type KeywordData, type KeywordDensityAnalysis, type KeywordOpportunity, type KeywordRecommendation, type KeywordResearchOptions, type KeywordResearchResult, type KeywordStats, type KeywordTopic, type KeywordTrend, type LLMCitationResult, type LLMJudgeOptions, type LLMProvider, LOCATION_CODES, type LSIKeyword, type LinkInfo$1 as LinkInfo, type Mention, type MentionType, type MetaData, type MetaFixOptions, type NGram, type NLPAnalysisResult, OG_IMAGE_SPECS, type OptimizeOptions, type PRConfig, type PRDescription, PRIORITY_WEIGHTS, type PRResult, type PageAudit, type PageData, type PageMeta, type PaidKeywordResult, type ParsedResponse, type PlanTier, type ProviderStats, type QuickWin, type RankCheckOptions, type RankCheckResult, type RankHistory, RankTracker, type RankingResult, type RankingTierLimits, type ReadabilityResult, type ReadmeConfig, type ReadmeResult, type ReportBranding, type ReportConfig, type ReportData, type ReportPageData, type RouteInfo, type SEOAnalysisResult, type SEOFixCommit, type SEOFixSummary, type SEOIssue, type SEORecommendation, type SEOScore, SEO_SCOPES, SITE_PROFILE_QUESTIONS, type ScheduledAuditConfig, type ScheduledAuditResult, type SchemaData, Schemas, type SearchIntent, type Sentiment, type SerpApiConfig, SerpClient, type SerpClientConfig, type SerpFeature, type SiteCrawlResult, type SiteProfile, type SiteSummary, type SnippetRecommendation, type SnippetType, type SocialMetaConfig, type SocialMetaFix, type SummarizerOptions, type TFIDFResult, TIER_LIMITS, type ToolFeasibilityScore, type ToolFunction, type ToolResult, type TopicClusterResult, type TopicModel, type TrackedKeyword, type TrackerConfig, type TrackingOptions, type TrendDirection, type UncertaintyAssessment, type WizardQuestion, type WizardResponse, type WizardResult, type WizardSession, type WorkflowConfig, addTrackingResult, analyzeAnchorText, analyzeCanonicalAdvanced, analyzeClientRendering, analyzeContentFreshness, analyzeDOMStructure, analyzeFeaturedSnippetPotential, analyzeHeadings, analyzeHeadline, analyzeHreflang, analyzeImages$1 as analyzeImages, analyzeKeywordDensity, analyzeKeywords, analyzeLinks, analyzeLocalSEO, analyzeMobile, analyzeModernImages, analyzeOnPage, analyzePagination, analyzePerformance, analyzeReadability, analyzeRedirectChain, analyzeRedirects, analyzeResponsiveImages, analyzeSERPPreview, analyzeSecurity, analyzeSecurityHeaders$1 as analyzeSecurityHeaders, analyzeSocialMeta, analyzeStructuredData$1 as analyzeStructuredData, analyzeTrackerBloat, analyzeUrl, analyzeUrlSafety, index$1 as analyzers, applyFixes, buildGSCApiRequest, buildGSCRequest, calculateAIVisibilityScore, calculateBM25, calculateTFIDF$1 as calculateKeywordTFIDF, calculateNextRun, calculateTFIDF, checkAIBotBlocking, checkAMP, checkAdsTxt, checkAppleTouchIcon, checkBalance, checkCertificate, checkDMARC, checkGitHubCLI, checkInternalRedirects, checkJSRenderingRatio, checkLLMCitations, checkLlmsTxt, checkMobileResources, checkPlaintextEmails, checkRedirects, checkRobots, checkRobotsTxt, checkSPF, checkSitemap, classifyIntent, classifyIntents, clusterKeywordsByEmbedding, compareCompetitorVisibility, comparePeriods, completeWizard, crawlSite, crawlUrl, createAuditPR, createFallbackSummary, createGEOHistory, createPullRequest, createSEOCommit, createSEOCommits, detectDuplicates, detectFramework$1 as detectFramework, detectGitHubPages, detectMentions, detectSoft404, detectTechnologies, detectVisibilityChanges, discoverCompetitorKeywords, discoverPagesFromLinks, discoverRoutesFromRepo, enhanceSummaryWithCompetitors, enhanceToolIdea, enrichKeywordsWithEstimates, ensureGitRepo, evaluateAndEnhanceToolIdeas, evaluateToolFeasibility, executeAgent, extractContentHash, extractEntityPhrases, extractImages, extractKeyPhrases, extractLinks, extractMeta, extractNgrams, extractSchema, extractSeedKeywords, extractTopics, fetchCoreWebVitals, findAlmostPage1Keywords, findCTROpportunities, findHtmlEntry, findLSIKeywords, findPageFiles, findTopPerformers, formatAlertMessage, formatCIResult, formatCompetitorReport, formatConventionalCommit, formatFeaturedSnippetReport, formatHeadlineReport, formatIntentReport, formatKeywordDensityReport, formatKeywordReport, formatPRBody, formatPRTitle, formatReadabilityReport, formatReport, formatSEOCommitMessage, formatTopicReport, formatWizardProgress, index as frameworks, generateAICitableContent, generateAllFixes, generateAngularSEOService, generateAspNetCoreSEO, generateAstroBaseHead, generateAstroMeta, generateBlogPost, generateBranchName, generateChangelog, generateCommitSummary, generateComparisonTable, generateCompleteSocialMetaSetup, generateDjangoSEOHelper, generateDuplicateIssues, generateEleventySEO, generateFAQSchema, generateFixes, generateGA4EnvTemplate, generateGA4ReactComponent, generateGA4Script, generateGA4ViteScript, generateGEOReport, generateGSCVerificationTag, generateGitHubActionSetup, generateGoSEO, generateHTMLReport, generateHTMLSocialMeta, generateHTMXSEO, generateHeadlineVariations, generateHugoSEO, generateJekyllSEO, generateJsonReport, generateKeyFacts, generateLaravelSEOHelper, generateMarkdownReport, generateNextAppMetadata, generateNextJsAppRouterMetadata, generateNextJsPagesRouterHead, generateNextPagesHead, generateNuxtSEOHead, generatePDFReport, generatePRDescription, generatePhoenixSEO, generateRailsSEOHelper, generateReactHelmetSocialMeta, generateReactSEOHead, generateRecommendationQueries, generateRemixMeta, generateRemixSEO, generateSecretsDoc, generateSocialMetaFix, generateSpringBootSEO, generateSvelteKitMeta, generateSvelteKitSEOHead, generateUncertaintyQuestions, generateVueSEOHead, generateWizardQuestions, generateWorkflow, getAIVisibilitySummary, getAutocompleteSuggestions, getDateRange, getExpandedSuggestions, getFrameworkSpecificFix, getFrameworkSpecificFixExtended, getGSCSetupInstructions, getGitUser, getKeywordData, getKeywordSuggestions, getMaxKdThreshold, getNextQuestion, getRelatedKeywords, getVisibilityTrend, groupKeywordsByTopic, identifyQuickWins, injectGA4, injectGSCVerification, interpolatePrompt, isGitRepo, listFiles, loadAgent, loadAgentByName, mergePages, optimizeForAI, optimizeReadme, parseGEOResponse, parseGSCResponse, parseSitemap, prioritizeKeywords, processWizardResponse, readFile, routesToUrls, runAIKeywordResearch, runAIReadinessChecks, runAdditionalChecks, runAuditWithFixes, runCIKeywordResearch, runCrawlabilityChecks, runDirectAnalysis, runFullAudit, runKeywordResearch, runNLPAnalysis, runScheduledAudit, scoreContentSEO, scoreMention, searchCompetitors, searchFormatConverters, searchHackerNews, sendAlert, sendAlerts, sendDiscordAlert, sendSlackAlert, shouldRunAudit, shouldSendAlert, startWizardSession, suggestSchemaTypes, summarizeSite, tokenize, tools, trackLLMVisibility, transformGSCData, urlSafetyDatabase, wizardResponsesToContext, writeFile, writeGitHubActionFiles };
5545
+ export { type AIContentConfig, type AIKeywordResearchOptions, type AgentDefinition, type AgentTool, type AlertChannel, type AlertConfig, type AlertMessage, type AlertResult, type AlertSeverity, type AlertType, type AuditAlertPayload, type AuditIssue, type AuditIssueData, type AuditOptions, type AuditPRResult, type AuditReport, type AuditReportData, type AuditRunnerOptions, type AuditRunnerResult, type AuditSchedule, type BlogPostConfig, type BrandConfig, type CIAction, type CIKeywordOptions, type CIKeywordResult, COMMIT_TYPES, type ChangelogConfig, type ChangelogResult, type CitationCheckOptions, type CommitConfig, type ComparisonConfig, type ComparisonRow, type CompetitiveInsight, type CompetitiveSearchOptions, type CompetitiveSearchResult, type CompetitorComparison, type CompetitorKeywordResult, type CompetitorOverlap, type CompetitorTool, type CompetitorUrl, type ContentElements, type ContentGeneratorOptions, type ContentRecommendation, type ConventionalCommit, type CrawlResult, type CrawlStats, type CrawledPage, type CreateAuditPROptions, type CreatePROptions, type DataForSEOCredentials, type DiscoveredPage, type EnhancedToolIdea, type ExecOptions, type ExecuteOptions, type ExecutionResult, type FAQItem, type FAQSchemaOptions, type FeaturedSnippetAnalysis, type Fix, type FixFile, type FixGeneratorOptions, type FixResult, type FrameworkInfo$1 as FrameworkInfo, type FreeKeywordResult, type FreeToolIdea, type GA4Config, type GEOAlert, type GEOAlertType, type GEOHistory, type GEOHistoryOptions, type GEOQuery, type GEOReport, type GEOResult, type GEOTrend, type GSCConfig, type GSCCredentials, type GSCPerformanceData, type GSCQueryData, type GSCQueryResult, type GeneratedCode, type GeneratedContent, type GeneratedFix, type HeadingStructure, type HeadlineAnalysis, type HealthScore, ISSUE_DEFINITIONS, type ImageInfo$2 as ImageInfo, type InjectionResult, type IntentAnalysis, type IssueCategory, type IssueDefinition, type IssueSeverity, type KeyFact, type KeyFactsOptions, type KeywordAction, type KeywordCluster, type KeywordData, type KeywordDensityAnalysis, type KeywordLLM, type KeywordOpportunity, type KeywordRecommendation, type KeywordResearchOptions, type KeywordResearchResult, type KeywordStats, type KeywordTopic, type KeywordTrend, type LLMCitationResult, type LLMJudgeOptions, type LLMProvider, LOCATION_CODES, type LSIKeyword, type LinkInfo$1 as LinkInfo, type Mention, type MentionType, type MetaData, type MetaFixOptions, type NGram, type NLPAnalysisResult, OG_IMAGE_SPECS, type OptimizeOptions, type PRConfig, type PRDescription, PRIORITY_WEIGHTS, type PRResult, type PageAudit, type PageData, type PageMeta, type PaidKeywordResult, type ParsedResponse, type PlanTier, type ProviderStats, type QuickWin, type RankCheckOptions, type RankCheckResult, type RankHistory, RankTracker, type RankingResult, type RankingTierLimits, type ReadabilityResult, type ReadmeConfig, type ReadmeResult, type ReportBranding, type ReportConfig, type ReportData, type ReportPageData, type RouteInfo, type SEOAnalysisResult, type SEOFixCommit, type SEOFixSummary, type SEOIssue, type SEORecommendation, type SEOScore, SEO_SCOPES, SITE_PROFILE_QUESTIONS, type ScheduledAuditConfig, type ScheduledAuditResult, type SchemaData, Schemas, type SearchIntent, type Sentiment, type SerpApiConfig, SerpClient, type SerpClientConfig, type SerpFeature, type SiteCrawlResult, type SiteProfile, type SiteSummary, type SnippetRecommendation, type SnippetType, type SocialMetaConfig, type SocialMetaFix, type SummarizerOptions, type TFIDFResult, TIER_LIMITS, type ToolFeasibilityScore, type ToolFunction, type ToolResult, type TopicClusterResult, type TopicModel, type TrackedKeyword, type TrackerConfig, type TrackingOptions, type TrendDirection, type UncertaintyAssessment, type WizardQuestion, type WizardResponse, type WizardResult, type WizardSession, type WorkflowConfig, addTrackingResult, analyzeAnchorText, analyzeCanonicalAdvanced, analyzeClientRendering, analyzeContentFreshness, analyzeDOMStructure, analyzeFeaturedSnippetPotential, analyzeHeadings, analyzeHeadline, analyzeHreflang, analyzeImages$1 as analyzeImages, analyzeKeywordDensity, analyzeKeywords, analyzeLinks, analyzeLocalSEO, analyzeMobile, analyzeModernImages, analyzeOnPage, analyzePagination, analyzePerformance, analyzeReadability, analyzeRedirectChain, analyzeRedirects, analyzeResponsiveImages, analyzeSERPPreview, analyzeSecurity, analyzeSecurityHeaders$1 as analyzeSecurityHeaders, analyzeSocialMeta, analyzeStructuredData$1 as analyzeStructuredData, analyzeTrackerBloat, analyzeUrl, analyzeUrlSafety, index$1 as analyzers, applyFixes, buildGSCApiRequest, buildGSCRequest, calculateAIVisibilityScore, calculateBM25, calculateTFIDF$1 as calculateKeywordTFIDF, calculateNextRun, calculateTFIDF, checkAIBotBlocking, checkAMP, checkAdsTxt, checkAppleTouchIcon, checkBalance, checkCertificate, checkDMARC, checkGitHubCLI, checkInternalRedirects, checkJSRenderingRatio, checkLLMCitations, checkLlmsTxt, checkMobileResources, checkPlaintextEmails, checkRedirects, checkRobots, checkRobotsTxt, checkSPF, checkSitemap, classifyIntent, classifyIntents, clusterKeywordsByEmbedding, compareCompetitorVisibility, comparePeriods, completeWizard, crawlSite, crawlUrl, createAuditPR, createFallbackSummary, createGEOHistory, createPullRequest, createSEOCommit, createSEOCommits, detectDuplicates, detectFramework$1 as detectFramework, detectGitHubPages, detectMentions, detectSoft404, detectTechnologies, detectVisibilityChanges, discoverCompetitorKeywords, discoverPagesFromLinks, discoverRoutesFromRepo, enhanceSummaryWithCompetitors, enhanceToolIdea, enrichKeywordsWithEstimates, ensureGitRepo, evaluateAndEnhanceToolIdeas, evaluateToolFeasibility, executeAgent, extractContentHash, extractEntityPhrases, extractImages, extractKeyPhrases, extractLinks, extractMeta, extractNgrams, extractSchema, extractSeedKeywords, extractTopics, fetchCoreWebVitals, findAlmostPage1Keywords, findCTROpportunities, findHtmlEntry, findLSIKeywords, findPageFiles, findTopPerformers, formatAlertMessage, formatCIResult, formatCompetitorReport, formatConventionalCommit, formatFeaturedSnippetReport, formatHeadlineReport, formatIntentReport, formatKeywordDensityReport, formatKeywordReport, formatPRBody, formatPRTitle, formatReadabilityReport, formatReport, formatSEOCommitMessage, formatTopicReport, formatWizardProgress, index as frameworks, generateAICitableContent, generateAllFixes, generateAngularSEOService, generateAspNetCoreSEO, generateAstroBaseHead, generateAstroMeta, generateBlogPost, generateBranchName, generateChangelog, generateCommitSummary, generateComparisonTable, generateCompleteSocialMetaSetup, generateDjangoSEOHelper, generateDuplicateIssues, generateEleventySEO, generateFAQSchema, generateFixes, generateGA4EnvTemplate, generateGA4ReactComponent, generateGA4Script, generateGA4ViteScript, generateGEOReport, generateGSCVerificationTag, generateGitHubActionSetup, generateGoSEO, generateHTMLReport, generateHTMLSocialMeta, generateHTMXSEO, generateHeadlineVariations, generateHugoSEO, generateJekyllSEO, generateJsonReport, generateKeyFacts, generateLaravelSEOHelper, generateMarkdownReport, generateNextAppMetadata, generateNextJsAppRouterMetadata, generateNextJsPagesRouterHead, generateNextPagesHead, generateNuxtSEOHead, generatePDFReport, generatePRDescription, generatePhoenixSEO, generateRailsSEOHelper, generateReactHelmetSocialMeta, generateReactSEOHead, generateRecommendationQueries, generateRemixMeta, generateRemixSEO, generateSecretsDoc, generateSocialMetaFix, generateSpringBootSEO, generateSvelteKitMeta, generateSvelteKitSEOHead, generateUncertaintyQuestions, generateVueSEOHead, generateWizardQuestions, generateWorkflow, getAIVisibilitySummary, getAutocompleteSuggestions, getDateRange, getExpandedSuggestions, getFrameworkSpecificFix, getFrameworkSpecificFixExtended, getGSCSetupInstructions, getGitUser, getKeywordData, getKeywordSuggestions, getMaxKdThreshold, getNextQuestion, getRelatedKeywords, getVisibilityTrend, groupKeywordsByTopic, identifyQuickWins, injectGA4, injectGSCVerification, interpolatePrompt, isGitRepo, keywordLLM, listFiles, loadAgent, loadAgentByName, mergePages, openAIKeywordLLM, optimizeForAI, optimizeReadme, parseGEOResponse, parseGSCResponse, parseJsonReply, parseSitemap, prioritizeKeywords, processWizardResponse, readFile, routesToUrls, runAIKeywordResearch, runAIReadinessChecks, runAdditionalChecks, runAuditWithFixes, runCIKeywordResearch, runCrawlabilityChecks, runDirectAnalysis, runFullAudit, runKeywordResearch, runNLPAnalysis, runScheduledAudit, scoreContentSEO, scoreMention, searchCompetitors, searchFormatConverters, searchHackerNews, sendAlert, sendAlerts, sendDiscordAlert, sendSlackAlert, shouldRunAudit, shouldSendAlert, startWizardSession, suggestSchemaTypes, summarizeSite, tokenize, tools, trackLLMVisibility, transformGSCData, urlSafetyDatabase, wizardResponsesToContext, writeFile, writeGitHubActionFiles };
package/dist/index.d.ts CHANGED
@@ -1631,6 +1631,39 @@ declare function runKeywordResearch(options: KeywordResearchOptions): Promise<Ke
1631
1631
  declare function formatKeywordReport(result: KeywordResearchResult): string;
1632
1632
  declare function extractSeedKeywords(url: string): Promise<string[]>;
1633
1633
 
1634
+ interface KeywordLLM {
1635
+ /**
1636
+ * A reply to one prompt that asks for a JSON object. `best` asks for the provider's stronger
1637
+ * model where it has one. Replies are parsed with parseJsonReply, never trusted to be clean.
1638
+ */
1639
+ complete(prompt: string, opts?: {
1640
+ system?: string;
1641
+ maxTokens?: number;
1642
+ temperature?: number;
1643
+ best?: boolean;
1644
+ }): Promise<string>;
1645
+ /** Embeddings for keyword clustering. Without it, clustering falls back to term vectors. */
1646
+ embed?(texts: string[]): Promise<number[][]>;
1647
+ /** Most page text worth sending in one prompt: small-context models get less. */
1648
+ maxContentChars?: number;
1649
+ /** How many tool ideas to judge (two calls each): slow local models get fewer. */
1650
+ maxToolIdeas?: number;
1651
+ }
1652
+ /** The engine's original backend: gpt-4o-mini, gpt-4o for `best`, JSON mode, text-embedding-3-small. */
1653
+ declare function openAIKeywordLLM(apiKey: string, opts?: {
1654
+ model?: string;
1655
+ }): KeywordLLM;
1656
+ /** The model to use: an explicit one, else OpenAI when there's a key, else none. */
1657
+ declare function keywordLLM(options: {
1658
+ llm?: KeywordLLM;
1659
+ openaiApiKey?: string;
1660
+ }): KeywordLLM | undefined;
1661
+ /**
1662
+ * The JSON object in a model reply. Local and small models wrap it in ``` fences, prepend prose
1663
+ * or emit <think> blocks; this takes the outermost {...} and tolerates trailing commas.
1664
+ */
1665
+ declare function parseJsonReply(text: string): Record<string, any> | null;
1666
+
1634
1667
  /**
1635
1668
  * Site Crawler for Keyword Research
1636
1669
  *
@@ -1687,7 +1720,7 @@ declare function extractKeyPhrases(crawlResult: SiteCrawlResult): string[];
1687
1720
  /**
1688
1721
  * AI-Powered Site Summarizer
1689
1722
  *
1690
- * Uses OpenAI to understand what a site does from crawled content.
1723
+ * Uses the configured AI provider (llm.ts) to understand what a site does from crawled content.
1691
1724
  * Extracts: product, audience, features, industry, positioning.
1692
1725
  */
1693
1726
 
@@ -1724,7 +1757,10 @@ interface SiteSummary {
1724
1757
  uncertainties: string[];
1725
1758
  }
1726
1759
  interface SummarizerOptions {
1727
- openaiApiKey: string;
1760
+ /** The model to ask (see llm.ts); or an OpenAI key, as before. */
1761
+ llm?: KeywordLLM;
1762
+ openaiApiKey?: string;
1763
+ /** OpenAI model override, with openaiApiKey. */
1728
1764
  model?: string;
1729
1765
  maxTokens?: number;
1730
1766
  }
@@ -1831,10 +1867,12 @@ declare function calculateBM25(documents: string[], query: string, k1?: number,
1831
1867
  docIndex: number;
1832
1868
  score: number;
1833
1869
  }>;
1870
+ /** An OpenAI client, or any function returning one embedding per text (llm.ts). */
1871
+ type Embedder = OpenAI | ((texts: string[]) => Promise<number[][]>);
1834
1872
  /**
1835
1873
  * Cluster keywords using embeddings
1836
1874
  */
1837
- declare function clusterKeywordsByEmbedding(keywords: string[], openai: OpenAI, numClusters?: number): Promise<KeywordCluster[]>;
1875
+ declare function clusterKeywordsByEmbedding(keywords: string[], openai: Embedder, numClusters?: number): Promise<KeywordCluster[]>;
1838
1876
  /**
1839
1877
  * Extract entity-like phrases (capitalized sequences, proper nouns)
1840
1878
  */
@@ -1848,6 +1886,8 @@ declare function extractTopics(documents: string[], numTopics?: number): TopicMo
1848
1886
  * Run full NLP analysis on content
1849
1887
  */
1850
1888
  declare function runNLPAnalysis(content: string | string[], options?: {
1889
+ /** Embeddings come from here when it has them (see llm.ts); or an OpenAI key, as before. */
1890
+ llm?: KeywordLLM;
1851
1891
  openaiApiKey?: string;
1852
1892
  numClusters?: number;
1853
1893
  numTopics?: number;
@@ -1871,6 +1911,8 @@ type PlanTier = 'free' | 'solo' | 'pro' | 'agency';
1871
1911
  interface AIKeywordResearchOptions {
1872
1912
  url: string;
1873
1913
  tier: PlanTier;
1914
+ /** The model to ask for paid-tier research (see llm.ts); or an OpenAI key, as before. */
1915
+ llm?: KeywordLLM;
1874
1916
  openaiApiKey?: string;
1875
1917
  maxPages?: number;
1876
1918
  userContext?: {
@@ -2066,6 +2108,8 @@ interface WizardResult {
2066
2108
  * Start a new wizard session
2067
2109
  */
2068
2110
  declare function startWizardSession(url: string, options?: {
2111
+ /** The model for the site summary (see llm.ts); or an OpenAI key, as before. */
2112
+ llm?: KeywordLLM;
2069
2113
  openaiApiKey?: string;
2070
2114
  existingResponses?: WizardResponse[];
2071
2115
  }): Promise<WizardSession>;
@@ -2370,7 +2414,9 @@ interface EnhancedToolIdea {
2370
2414
  priority: number;
2371
2415
  }
2372
2416
  interface LLMJudgeOptions {
2373
- openaiApiKey: string;
2417
+ /** The model to ask (see llm.ts); or an OpenAI key, as before. */
2418
+ llm?: KeywordLLM;
2419
+ openaiApiKey?: string;
2374
2420
  minFeasibilityScore?: number;
2375
2421
  minSpecificityScore?: number;
2376
2422
  minProductFitScore?: number;
@@ -4691,11 +4737,9 @@ declare class RankTracker {
4691
4737
  */
4692
4738
  private saveRanking;
4693
4739
  /**
4694
- * Get ranking history for a keyword
4695
- * Note: Full history requires keyword_ranking_history table with keyword_ranking_id
4696
- * For now, returns current state as single history entry
4740
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
4697
4741
  */
4698
- getHistory(keywordId: string, _days?: number): Promise<RankHistory[]>;
4742
+ getHistory(keywordId: string, days?: number): Promise<RankHistory[]>;
4699
4743
  /**
4700
4744
  * Get keyword trends for a project
4701
4745
  */
@@ -5498,4 +5542,4 @@ declare namespace index {
5498
5542
  export { type index_Framework as Framework, type index_FrameworkCheck as FrameworkCheck, type index_FrameworkInfo as FrameworkInfo, type index_FrameworkRecipe as FrameworkRecipe, index_SUPPORTED_FRAMEWORKS as SUPPORTED_FRAMEWORKS, index_detectFramework as detectFramework, index_detectFromHeaders as detectFromHeaders, index_detectFromHtml as detectFromHtml, index_detectFromPackageJson as detectFromPackageJson, index_embedRecipe as embedRecipe, index_getFrameworkChecks as getFrameworkChecks, index_getFrameworkDisplayName as getFrameworkDisplayName, index_getFrameworkIssues as getFrameworkIssues, index_getFrameworkSuggestions as getFrameworkSuggestions, index_getFrameworkSummary as getFrameworkSummary, index_getMigrationPaths as getMigrationPaths, index_getPerformanceTips as getPerformanceTips, index_getSeoChallenges as getSeoChallenges, index_getTotalFrameworkChecks as getTotalFrameworkChecks, index_loadRecipe as loadRecipe };
5499
5543
  }
5500
5544
 
5501
- export { type AIContentConfig, type AIKeywordResearchOptions, type AgentDefinition, type AgentTool, type AlertChannel, type AlertConfig, type AlertMessage, type AlertResult, type AlertSeverity, type AlertType, type AuditAlertPayload, type AuditIssue, type AuditIssueData, type AuditOptions, type AuditPRResult, type AuditReport, type AuditReportData, type AuditRunnerOptions, type AuditRunnerResult, type AuditSchedule, type BlogPostConfig, type BrandConfig, type CIAction, type CIKeywordOptions, type CIKeywordResult, COMMIT_TYPES, type ChangelogConfig, type ChangelogResult, type CitationCheckOptions, type CommitConfig, type ComparisonConfig, type ComparisonRow, type CompetitiveInsight, type CompetitiveSearchOptions, type CompetitiveSearchResult, type CompetitorComparison, type CompetitorKeywordResult, type CompetitorOverlap, type CompetitorTool, type CompetitorUrl, type ContentElements, type ContentGeneratorOptions, type ContentRecommendation, type ConventionalCommit, type CrawlResult, type CrawlStats, type CrawledPage, type CreateAuditPROptions, type CreatePROptions, type DataForSEOCredentials, type DiscoveredPage, type EnhancedToolIdea, type ExecOptions, type ExecuteOptions, type ExecutionResult, type FAQItem, type FAQSchemaOptions, type FeaturedSnippetAnalysis, type Fix, type FixFile, type FixGeneratorOptions, type FixResult, type FrameworkInfo$1 as FrameworkInfo, type FreeKeywordResult, type FreeToolIdea, type GA4Config, type GEOAlert, type GEOAlertType, type GEOHistory, type GEOHistoryOptions, type GEOQuery, type GEOReport, type GEOResult, type GEOTrend, type GSCConfig, type GSCCredentials, type GSCPerformanceData, type GSCQueryData, type GSCQueryResult, type GeneratedCode, type GeneratedContent, type GeneratedFix, type HeadingStructure, type HeadlineAnalysis, type HealthScore, ISSUE_DEFINITIONS, type ImageInfo$2 as ImageInfo, type InjectionResult, type IntentAnalysis, type IssueCategory, type IssueDefinition, type IssueSeverity, type KeyFact, type KeyFactsOptions, type KeywordAction, type KeywordCluster, type KeywordData, type KeywordDensityAnalysis, type KeywordOpportunity, type KeywordRecommendation, type KeywordResearchOptions, type KeywordResearchResult, type KeywordStats, type KeywordTopic, type KeywordTrend, type LLMCitationResult, type LLMJudgeOptions, type LLMProvider, LOCATION_CODES, type LSIKeyword, type LinkInfo$1 as LinkInfo, type Mention, type MentionType, type MetaData, type MetaFixOptions, type NGram, type NLPAnalysisResult, OG_IMAGE_SPECS, type OptimizeOptions, type PRConfig, type PRDescription, PRIORITY_WEIGHTS, type PRResult, type PageAudit, type PageData, type PageMeta, type PaidKeywordResult, type ParsedResponse, type PlanTier, type ProviderStats, type QuickWin, type RankCheckOptions, type RankCheckResult, type RankHistory, RankTracker, type RankingResult, type RankingTierLimits, type ReadabilityResult, type ReadmeConfig, type ReadmeResult, type ReportBranding, type ReportConfig, type ReportData, type ReportPageData, type RouteInfo, type SEOAnalysisResult, type SEOFixCommit, type SEOFixSummary, type SEOIssue, type SEORecommendation, type SEOScore, SEO_SCOPES, SITE_PROFILE_QUESTIONS, type ScheduledAuditConfig, type ScheduledAuditResult, type SchemaData, Schemas, type SearchIntent, type Sentiment, type SerpApiConfig, SerpClient, type SerpClientConfig, type SerpFeature, type SiteCrawlResult, type SiteProfile, type SiteSummary, type SnippetRecommendation, type SnippetType, type SocialMetaConfig, type SocialMetaFix, type SummarizerOptions, type TFIDFResult, TIER_LIMITS, type ToolFeasibilityScore, type ToolFunction, type ToolResult, type TopicClusterResult, type TopicModel, type TrackedKeyword, type TrackerConfig, type TrackingOptions, type TrendDirection, type UncertaintyAssessment, type WizardQuestion, type WizardResponse, type WizardResult, type WizardSession, type WorkflowConfig, addTrackingResult, analyzeAnchorText, analyzeCanonicalAdvanced, analyzeClientRendering, analyzeContentFreshness, analyzeDOMStructure, analyzeFeaturedSnippetPotential, analyzeHeadings, analyzeHeadline, analyzeHreflang, analyzeImages$1 as analyzeImages, analyzeKeywordDensity, analyzeKeywords, analyzeLinks, analyzeLocalSEO, analyzeMobile, analyzeModernImages, analyzeOnPage, analyzePagination, analyzePerformance, analyzeReadability, analyzeRedirectChain, analyzeRedirects, analyzeResponsiveImages, analyzeSERPPreview, analyzeSecurity, analyzeSecurityHeaders$1 as analyzeSecurityHeaders, analyzeSocialMeta, analyzeStructuredData$1 as analyzeStructuredData, analyzeTrackerBloat, analyzeUrl, analyzeUrlSafety, index$1 as analyzers, applyFixes, buildGSCApiRequest, buildGSCRequest, calculateAIVisibilityScore, calculateBM25, calculateTFIDF$1 as calculateKeywordTFIDF, calculateNextRun, calculateTFIDF, checkAIBotBlocking, checkAMP, checkAdsTxt, checkAppleTouchIcon, checkBalance, checkCertificate, checkDMARC, checkGitHubCLI, checkInternalRedirects, checkJSRenderingRatio, checkLLMCitations, checkLlmsTxt, checkMobileResources, checkPlaintextEmails, checkRedirects, checkRobots, checkRobotsTxt, checkSPF, checkSitemap, classifyIntent, classifyIntents, clusterKeywordsByEmbedding, compareCompetitorVisibility, comparePeriods, completeWizard, crawlSite, crawlUrl, createAuditPR, createFallbackSummary, createGEOHistory, createPullRequest, createSEOCommit, createSEOCommits, detectDuplicates, detectFramework$1 as detectFramework, detectGitHubPages, detectMentions, detectSoft404, detectTechnologies, detectVisibilityChanges, discoverCompetitorKeywords, discoverPagesFromLinks, discoverRoutesFromRepo, enhanceSummaryWithCompetitors, enhanceToolIdea, enrichKeywordsWithEstimates, ensureGitRepo, evaluateAndEnhanceToolIdeas, evaluateToolFeasibility, executeAgent, extractContentHash, extractEntityPhrases, extractImages, extractKeyPhrases, extractLinks, extractMeta, extractNgrams, extractSchema, extractSeedKeywords, extractTopics, fetchCoreWebVitals, findAlmostPage1Keywords, findCTROpportunities, findHtmlEntry, findLSIKeywords, findPageFiles, findTopPerformers, formatAlertMessage, formatCIResult, formatCompetitorReport, formatConventionalCommit, formatFeaturedSnippetReport, formatHeadlineReport, formatIntentReport, formatKeywordDensityReport, formatKeywordReport, formatPRBody, formatPRTitle, formatReadabilityReport, formatReport, formatSEOCommitMessage, formatTopicReport, formatWizardProgress, index as frameworks, generateAICitableContent, generateAllFixes, generateAngularSEOService, generateAspNetCoreSEO, generateAstroBaseHead, generateAstroMeta, generateBlogPost, generateBranchName, generateChangelog, generateCommitSummary, generateComparisonTable, generateCompleteSocialMetaSetup, generateDjangoSEOHelper, generateDuplicateIssues, generateEleventySEO, generateFAQSchema, generateFixes, generateGA4EnvTemplate, generateGA4ReactComponent, generateGA4Script, generateGA4ViteScript, generateGEOReport, generateGSCVerificationTag, generateGitHubActionSetup, generateGoSEO, generateHTMLReport, generateHTMLSocialMeta, generateHTMXSEO, generateHeadlineVariations, generateHugoSEO, generateJekyllSEO, generateJsonReport, generateKeyFacts, generateLaravelSEOHelper, generateMarkdownReport, generateNextAppMetadata, generateNextJsAppRouterMetadata, generateNextJsPagesRouterHead, generateNextPagesHead, generateNuxtSEOHead, generatePDFReport, generatePRDescription, generatePhoenixSEO, generateRailsSEOHelper, generateReactHelmetSocialMeta, generateReactSEOHead, generateRecommendationQueries, generateRemixMeta, generateRemixSEO, generateSecretsDoc, generateSocialMetaFix, generateSpringBootSEO, generateSvelteKitMeta, generateSvelteKitSEOHead, generateUncertaintyQuestions, generateVueSEOHead, generateWizardQuestions, generateWorkflow, getAIVisibilitySummary, getAutocompleteSuggestions, getDateRange, getExpandedSuggestions, getFrameworkSpecificFix, getFrameworkSpecificFixExtended, getGSCSetupInstructions, getGitUser, getKeywordData, getKeywordSuggestions, getMaxKdThreshold, getNextQuestion, getRelatedKeywords, getVisibilityTrend, groupKeywordsByTopic, identifyQuickWins, injectGA4, injectGSCVerification, interpolatePrompt, isGitRepo, listFiles, loadAgent, loadAgentByName, mergePages, optimizeForAI, optimizeReadme, parseGEOResponse, parseGSCResponse, parseSitemap, prioritizeKeywords, processWizardResponse, readFile, routesToUrls, runAIKeywordResearch, runAIReadinessChecks, runAdditionalChecks, runAuditWithFixes, runCIKeywordResearch, runCrawlabilityChecks, runDirectAnalysis, runFullAudit, runKeywordResearch, runNLPAnalysis, runScheduledAudit, scoreContentSEO, scoreMention, searchCompetitors, searchFormatConverters, searchHackerNews, sendAlert, sendAlerts, sendDiscordAlert, sendSlackAlert, shouldRunAudit, shouldSendAlert, startWizardSession, suggestSchemaTypes, summarizeSite, tokenize, tools, trackLLMVisibility, transformGSCData, urlSafetyDatabase, wizardResponsesToContext, writeFile, writeGitHubActionFiles };
5545
+ export { type AIContentConfig, type AIKeywordResearchOptions, type AgentDefinition, type AgentTool, type AlertChannel, type AlertConfig, type AlertMessage, type AlertResult, type AlertSeverity, type AlertType, type AuditAlertPayload, type AuditIssue, type AuditIssueData, type AuditOptions, type AuditPRResult, type AuditReport, type AuditReportData, type AuditRunnerOptions, type AuditRunnerResult, type AuditSchedule, type BlogPostConfig, type BrandConfig, type CIAction, type CIKeywordOptions, type CIKeywordResult, COMMIT_TYPES, type ChangelogConfig, type ChangelogResult, type CitationCheckOptions, type CommitConfig, type ComparisonConfig, type ComparisonRow, type CompetitiveInsight, type CompetitiveSearchOptions, type CompetitiveSearchResult, type CompetitorComparison, type CompetitorKeywordResult, type CompetitorOverlap, type CompetitorTool, type CompetitorUrl, type ContentElements, type ContentGeneratorOptions, type ContentRecommendation, type ConventionalCommit, type CrawlResult, type CrawlStats, type CrawledPage, type CreateAuditPROptions, type CreatePROptions, type DataForSEOCredentials, type DiscoveredPage, type EnhancedToolIdea, type ExecOptions, type ExecuteOptions, type ExecutionResult, type FAQItem, type FAQSchemaOptions, type FeaturedSnippetAnalysis, type Fix, type FixFile, type FixGeneratorOptions, type FixResult, type FrameworkInfo$1 as FrameworkInfo, type FreeKeywordResult, type FreeToolIdea, type GA4Config, type GEOAlert, type GEOAlertType, type GEOHistory, type GEOHistoryOptions, type GEOQuery, type GEOReport, type GEOResult, type GEOTrend, type GSCConfig, type GSCCredentials, type GSCPerformanceData, type GSCQueryData, type GSCQueryResult, type GeneratedCode, type GeneratedContent, type GeneratedFix, type HeadingStructure, type HeadlineAnalysis, type HealthScore, ISSUE_DEFINITIONS, type ImageInfo$2 as ImageInfo, type InjectionResult, type IntentAnalysis, type IssueCategory, type IssueDefinition, type IssueSeverity, type KeyFact, type KeyFactsOptions, type KeywordAction, type KeywordCluster, type KeywordData, type KeywordDensityAnalysis, type KeywordLLM, type KeywordOpportunity, type KeywordRecommendation, type KeywordResearchOptions, type KeywordResearchResult, type KeywordStats, type KeywordTopic, type KeywordTrend, type LLMCitationResult, type LLMJudgeOptions, type LLMProvider, LOCATION_CODES, type LSIKeyword, type LinkInfo$1 as LinkInfo, type Mention, type MentionType, type MetaData, type MetaFixOptions, type NGram, type NLPAnalysisResult, OG_IMAGE_SPECS, type OptimizeOptions, type PRConfig, type PRDescription, PRIORITY_WEIGHTS, type PRResult, type PageAudit, type PageData, type PageMeta, type PaidKeywordResult, type ParsedResponse, type PlanTier, type ProviderStats, type QuickWin, type RankCheckOptions, type RankCheckResult, type RankHistory, RankTracker, type RankingResult, type RankingTierLimits, type ReadabilityResult, type ReadmeConfig, type ReadmeResult, type ReportBranding, type ReportConfig, type ReportData, type ReportPageData, type RouteInfo, type SEOAnalysisResult, type SEOFixCommit, type SEOFixSummary, type SEOIssue, type SEORecommendation, type SEOScore, SEO_SCOPES, SITE_PROFILE_QUESTIONS, type ScheduledAuditConfig, type ScheduledAuditResult, type SchemaData, Schemas, type SearchIntent, type Sentiment, type SerpApiConfig, SerpClient, type SerpClientConfig, type SerpFeature, type SiteCrawlResult, type SiteProfile, type SiteSummary, type SnippetRecommendation, type SnippetType, type SocialMetaConfig, type SocialMetaFix, type SummarizerOptions, type TFIDFResult, TIER_LIMITS, type ToolFeasibilityScore, type ToolFunction, type ToolResult, type TopicClusterResult, type TopicModel, type TrackedKeyword, type TrackerConfig, type TrackingOptions, type TrendDirection, type UncertaintyAssessment, type WizardQuestion, type WizardResponse, type WizardResult, type WizardSession, type WorkflowConfig, addTrackingResult, analyzeAnchorText, analyzeCanonicalAdvanced, analyzeClientRendering, analyzeContentFreshness, analyzeDOMStructure, analyzeFeaturedSnippetPotential, analyzeHeadings, analyzeHeadline, analyzeHreflang, analyzeImages$1 as analyzeImages, analyzeKeywordDensity, analyzeKeywords, analyzeLinks, analyzeLocalSEO, analyzeMobile, analyzeModernImages, analyzeOnPage, analyzePagination, analyzePerformance, analyzeReadability, analyzeRedirectChain, analyzeRedirects, analyzeResponsiveImages, analyzeSERPPreview, analyzeSecurity, analyzeSecurityHeaders$1 as analyzeSecurityHeaders, analyzeSocialMeta, analyzeStructuredData$1 as analyzeStructuredData, analyzeTrackerBloat, analyzeUrl, analyzeUrlSafety, index$1 as analyzers, applyFixes, buildGSCApiRequest, buildGSCRequest, calculateAIVisibilityScore, calculateBM25, calculateTFIDF$1 as calculateKeywordTFIDF, calculateNextRun, calculateTFIDF, checkAIBotBlocking, checkAMP, checkAdsTxt, checkAppleTouchIcon, checkBalance, checkCertificate, checkDMARC, checkGitHubCLI, checkInternalRedirects, checkJSRenderingRatio, checkLLMCitations, checkLlmsTxt, checkMobileResources, checkPlaintextEmails, checkRedirects, checkRobots, checkRobotsTxt, checkSPF, checkSitemap, classifyIntent, classifyIntents, clusterKeywordsByEmbedding, compareCompetitorVisibility, comparePeriods, completeWizard, crawlSite, crawlUrl, createAuditPR, createFallbackSummary, createGEOHistory, createPullRequest, createSEOCommit, createSEOCommits, detectDuplicates, detectFramework$1 as detectFramework, detectGitHubPages, detectMentions, detectSoft404, detectTechnologies, detectVisibilityChanges, discoverCompetitorKeywords, discoverPagesFromLinks, discoverRoutesFromRepo, enhanceSummaryWithCompetitors, enhanceToolIdea, enrichKeywordsWithEstimates, ensureGitRepo, evaluateAndEnhanceToolIdeas, evaluateToolFeasibility, executeAgent, extractContentHash, extractEntityPhrases, extractImages, extractKeyPhrases, extractLinks, extractMeta, extractNgrams, extractSchema, extractSeedKeywords, extractTopics, fetchCoreWebVitals, findAlmostPage1Keywords, findCTROpportunities, findHtmlEntry, findLSIKeywords, findPageFiles, findTopPerformers, formatAlertMessage, formatCIResult, formatCompetitorReport, formatConventionalCommit, formatFeaturedSnippetReport, formatHeadlineReport, formatIntentReport, formatKeywordDensityReport, formatKeywordReport, formatPRBody, formatPRTitle, formatReadabilityReport, formatReport, formatSEOCommitMessage, formatTopicReport, formatWizardProgress, index as frameworks, generateAICitableContent, generateAllFixes, generateAngularSEOService, generateAspNetCoreSEO, generateAstroBaseHead, generateAstroMeta, generateBlogPost, generateBranchName, generateChangelog, generateCommitSummary, generateComparisonTable, generateCompleteSocialMetaSetup, generateDjangoSEOHelper, generateDuplicateIssues, generateEleventySEO, generateFAQSchema, generateFixes, generateGA4EnvTemplate, generateGA4ReactComponent, generateGA4Script, generateGA4ViteScript, generateGEOReport, generateGSCVerificationTag, generateGitHubActionSetup, generateGoSEO, generateHTMLReport, generateHTMLSocialMeta, generateHTMXSEO, generateHeadlineVariations, generateHugoSEO, generateJekyllSEO, generateJsonReport, generateKeyFacts, generateLaravelSEOHelper, generateMarkdownReport, generateNextAppMetadata, generateNextJsAppRouterMetadata, generateNextJsPagesRouterHead, generateNextPagesHead, generateNuxtSEOHead, generatePDFReport, generatePRDescription, generatePhoenixSEO, generateRailsSEOHelper, generateReactHelmetSocialMeta, generateReactSEOHead, generateRecommendationQueries, generateRemixMeta, generateRemixSEO, generateSecretsDoc, generateSocialMetaFix, generateSpringBootSEO, generateSvelteKitMeta, generateSvelteKitSEOHead, generateUncertaintyQuestions, generateVueSEOHead, generateWizardQuestions, generateWorkflow, getAIVisibilitySummary, getAutocompleteSuggestions, getDateRange, getExpandedSuggestions, getFrameworkSpecificFix, getFrameworkSpecificFixExtended, getGSCSetupInstructions, getGitUser, getKeywordData, getKeywordSuggestions, getMaxKdThreshold, getNextQuestion, getRelatedKeywords, getVisibilityTrend, groupKeywordsByTopic, identifyQuickWins, injectGA4, injectGSCVerification, interpolatePrompt, isGitRepo, keywordLLM, listFiles, loadAgent, loadAgentByName, mergePages, openAIKeywordLLM, optimizeForAI, optimizeReadme, parseGEOResponse, parseGSCResponse, parseJsonReply, parseSitemap, prioritizeKeywords, processWizardResponse, readFile, routesToUrls, runAIKeywordResearch, runAIReadinessChecks, runAdditionalChecks, runAuditWithFixes, runCIKeywordResearch, runCrawlabilityChecks, runDirectAnalysis, runFullAudit, runKeywordResearch, runNLPAnalysis, runScheduledAudit, scoreContentSEO, scoreMention, searchCompetitors, searchFormatConverters, searchHackerNews, sendAlert, sendAlerts, sendDiscordAlert, sendSlackAlert, shouldRunAudit, shouldSendAlert, startWizardSession, suggestSchemaTypes, summarizeSite, tokenize, tools, trackLLMVisibility, transformGSCData, urlSafetyDatabase, wizardResponsesToContext, writeFile, writeGitHubActionFiles };
package/dist/index.js CHANGED
@@ -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,
@@ -7844,21 +7847,28 @@ var HTML_SIGNATURES = [
7844
7847
  {
7845
7848
  framework: "wordpress",
7846
7849
  language: "php",
7847
- patterns: [/wp-content/, /wp-includes/, /wp-json/, /wordpress/i],
7850
+ // Technical signatures only. These patterns run over the whole page, so the old
7851
+ // /wordpress/i matched the word anywhere in the copy: rankcli.dev's own "detects
7852
+ // Rails, Django, Laravel, and WordPress" line was enough to detect its React SPA as
7853
+ // WordPress and then tell it to install an SEO plugin. A generator meta tag is the
7854
+ // one place the page says it about itself.
7855
+ patterns: [/wp-content/, /wp-includes/, /wp-json/, /name=["']generator["'][^>]*content=["']WordPress/i],
7848
7856
  confidence: "high"
7849
7857
  },
7850
7858
  // Laravel
7851
7859
  {
7852
7860
  framework: "laravel",
7853
7861
  language: "php",
7854
- patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/, /laravel/i],
7862
+ // /laravel/i had the same problem as WordPress above - it matched the word in prose.
7863
+ patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/],
7855
7864
  confidence: "medium"
7856
7865
  },
7857
7866
  // Symfony
7858
7867
  {
7859
7868
  framework: "symfony",
7860
7869
  language: "php",
7861
- patterns: [/symfony/, /sf2/, /_sf_/, /profiler\.css/],
7870
+ // Likewise /symfony/, which matched any lowercase mention.
7871
+ patterns: [/sf2/, /_sf_/, /profiler\.css/],
7862
7872
  confidence: "medium"
7863
7873
  },
7864
7874
  // ============ Ruby Frameworks ============
@@ -20111,6 +20121,50 @@ async function extractSeedKeywords(url) {
20111
20121
  }
20112
20122
  }
20113
20123
 
20124
+ // src/keywords/llm.ts
20125
+ var import_openai = __toESM(require("openai"));
20126
+ function openAIKeywordLLM(apiKey, opts = {}) {
20127
+ const openai = new import_openai.default({ apiKey });
20128
+ const openAIModel = opts.model;
20129
+ return {
20130
+ async complete(prompt, opts2 = {}) {
20131
+ const response = await openai.chat.completions.create({
20132
+ model: opts2.best ? "gpt-4o" : openAIModel ?? "gpt-4o-mini",
20133
+ messages: [
20134
+ ...opts2.system ? [{ role: "system", content: opts2.system }] : [],
20135
+ { role: "user", content: prompt }
20136
+ ],
20137
+ response_format: { type: "json_object" },
20138
+ temperature: opts2.temperature ?? 0.3,
20139
+ ...opts2.maxTokens ? { max_tokens: opts2.maxTokens } : {}
20140
+ });
20141
+ return response.choices[0]?.message?.content || "";
20142
+ },
20143
+ async embed(texts) {
20144
+ const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: texts });
20145
+ return response.data.map((item) => item.embedding);
20146
+ }
20147
+ };
20148
+ }
20149
+ function keywordLLM(options) {
20150
+ return options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey) : void 0);
20151
+ }
20152
+ function parseJsonReply(text) {
20153
+ const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
20154
+ const start = cleaned.indexOf("{");
20155
+ const end = cleaned.lastIndexOf("}");
20156
+ if (start === -1 || end <= start) return null;
20157
+ const candidate = cleaned.slice(start, end + 1);
20158
+ for (const attempt of [candidate, candidate.replace(/,\s*([}\]])/g, "$1")]) {
20159
+ try {
20160
+ const parsed = JSON.parse(attempt);
20161
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
20162
+ } catch {
20163
+ }
20164
+ }
20165
+ return null;
20166
+ }
20167
+
20114
20168
  // src/keywords/site-crawler.ts
20115
20169
  var cheerio40 = __toESM(require("cheerio"));
20116
20170
  init_http();
@@ -20321,7 +20375,11 @@ function extractKeyPhrases(crawlResult) {
20321
20375
  }
20322
20376
 
20323
20377
  // src/keywords/ai-summarizer.ts
20324
- var import_openai = __toESM(require("openai"));
20378
+ function summarizerLLM(options) {
20379
+ const llm = options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey, { model: options.model }) : void 0);
20380
+ if (!llm) throw new Error("An AI provider is required to summarize the site");
20381
+ return llm;
20382
+ }
20325
20383
  var SUMMARIZER_PROMPT = `You are an expert SEO analyst. Analyze the following website content and extract key information for keyword research.
20326
20384
 
20327
20385
  WEBSITE CONTENT:
@@ -20364,37 +20422,26 @@ IMPORTANT:
20364
20422
 
20365
20423
  Respond ONLY with the JSON object, no other text.`;
20366
20424
  async function summarizeSite(crawlResult, options) {
20367
- const openai = new import_openai.default({ apiKey: options.openaiApiKey });
20368
- const model = options.model || "gpt-4o-mini";
20425
+ const llm = summarizerLLM(options);
20426
+ const maxContent = llm.maxContentChars ?? 15e3;
20369
20427
  let content = crawlResult.aggregatedContent;
20370
- if (content.length > 15e3) {
20371
- content = content.substring(0, 15e3) + "\n...[truncated]";
20428
+ if (content.length > maxContent) {
20429
+ content = content.substring(0, maxContent) + "\n...[truncated]";
20372
20430
  }
20373
20431
  const headings = crawlResult.uniqueHeadings.slice(0, 30).join("\n");
20374
20432
  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));
20375
20433
  console.log("\u{1F916} Analyzing site content with AI...");
20376
20434
  try {
20377
- const response = await openai.chat.completions.create({
20378
- model,
20379
- messages: [
20380
- {
20381
- role: "system",
20382
- content: "You are an expert SEO analyst. Always respond with valid JSON only."
20383
- },
20384
- {
20385
- role: "user",
20386
- content: prompt
20387
- }
20388
- ],
20435
+ const responseText = await llm.complete(prompt, {
20436
+ system: "You are an expert SEO analyst. Always respond with valid JSON only.",
20389
20437
  temperature: 0.3,
20390
- max_tokens: options.maxTokens || 2e3
20438
+ maxTokens: options.maxTokens || 2e3
20391
20439
  });
20392
- const responseText = response.choices[0]?.message?.content || "";
20393
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
20394
- if (!jsonMatch) {
20440
+ const parsed = parseJsonReply(responseText);
20441
+ if (!parsed) {
20395
20442
  throw new Error("No JSON found in response");
20396
20443
  }
20397
- const summary = JSON.parse(jsonMatch[0]);
20444
+ const summary = parsed;
20398
20445
  return {
20399
20446
  productName: summary.productName || "Unknown Product",
20400
20447
  productDescription: summary.productDescription || "No description available",
@@ -20456,7 +20503,7 @@ function createFallbackSummary(crawlResult) {
20456
20503
  };
20457
20504
  }
20458
20505
  async function enhanceSummaryWithCompetitors(summary, competitors, options) {
20459
- const openai = new import_openai.default({ apiKey: options.openaiApiKey });
20506
+ const llm = summarizerLLM(options);
20460
20507
  const prompt = `Given this product summary:
20461
20508
  Product: ${summary.productName}
20462
20509
  Description: ${summary.productDescription}
@@ -20475,19 +20522,13 @@ Analyze the competitive positioning. If this product focuses on a niche segment
20475
20522
  "updatedUncertainties": ["uncertainty1", "uncertainty2"]
20476
20523
  }`;
20477
20524
  try {
20478
- const response = await openai.chat.completions.create({
20479
- model: options.model || "gpt-4o-mini",
20480
- messages: [
20481
- { role: "system", content: "Respond with valid JSON only." },
20482
- { role: "user", content: prompt }
20483
- ],
20525
+ const responseText = await llm.complete(prompt, {
20526
+ system: "Respond with valid JSON only.",
20484
20527
  temperature: 0.3,
20485
- max_tokens: 1e3
20528
+ maxTokens: 1e3
20486
20529
  });
20487
- const responseText = response.choices[0]?.message?.content || "";
20488
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
20489
- if (jsonMatch) {
20490
- const analysis = JSON.parse(jsonMatch[0]);
20530
+ const analysis = parseJsonReply(responseText);
20531
+ if (analysis) {
20491
20532
  return {
20492
20533
  ...summary,
20493
20534
  competitivePositioning: analysis.competitivePositioning || "",
@@ -20574,7 +20615,6 @@ function generateUncertaintyQuestions(summary) {
20574
20615
  }
20575
20616
 
20576
20617
  // src/keywords/nlp-analysis.ts
20577
- var import_openai2 = __toESM(require("openai"));
20578
20618
  var STOP_WORDS2 = /* @__PURE__ */ new Set([
20579
20619
  "a",
20580
20620
  "an",
@@ -20859,19 +20899,18 @@ function createTermVectors(keywords, vocabulary) {
20859
20899
  return vector;
20860
20900
  });
20861
20901
  }
20902
+ async function embedBatch(embedder, batch) {
20903
+ if (typeof embedder === "function") return embedder(batch);
20904
+ const response = await embedder.embeddings.create({ model: "text-embedding-3-small", input: batch });
20905
+ return response.data.map((item) => item.embedding);
20906
+ }
20862
20907
  async function getEmbeddings(texts, openai) {
20863
20908
  const embeddings = [];
20864
20909
  const batchSize = 100;
20865
20910
  for (let i = 0; i < texts.length; i += batchSize) {
20866
20911
  const batch = texts.slice(i, i + batchSize);
20867
20912
  try {
20868
- const response = await openai.embeddings.create({
20869
- model: "text-embedding-3-small",
20870
- input: batch
20871
- });
20872
- for (const item of response.data) {
20873
- embeddings.push(item.embedding);
20874
- }
20913
+ embeddings.push(...await embedBatch(openai, batch));
20875
20914
  } catch (error) {
20876
20915
  console.warn("Embedding API failed, using fallback BOW vectors");
20877
20916
  for (const text of batch) {
@@ -21026,12 +21065,12 @@ async function runNLPAnalysis(content, options = {}) {
21026
21065
  const entityPhrases = extractEntityPhrases(combinedText);
21027
21066
  let clusters = [];
21028
21067
  const keywordsToCluster = tfidfKeywords.slice(0, 50).map((k) => k.term);
21029
- if (options.openaiApiKey && keywordsToCluster.length > 0) {
21068
+ const embed = keywordLLM(options)?.embed;
21069
+ if (embed && keywordsToCluster.length > 0) {
21030
21070
  try {
21031
- const openai = new import_openai2.default({ apiKey: options.openaiApiKey });
21032
21071
  clusters = await clusterKeywordsByEmbedding(
21033
21072
  keywordsToCluster,
21034
- openai,
21073
+ (texts) => embed(texts),
21035
21074
  options.numClusters
21036
21075
  );
21037
21076
  } catch (error) {
@@ -21374,9 +21413,13 @@ async function searchHackerNews(query, maxResults = 10) {
21374
21413
  }
21375
21414
 
21376
21415
  // src/keywords/llm-judge.ts
21377
- var import_openai3 = __toESM(require("openai"));
21416
+ function judgeLLM(options) {
21417
+ const llm = keywordLLM(options);
21418
+ if (!llm) throw new Error("An AI provider is required to judge tool ideas");
21419
+ return llm;
21420
+ }
21378
21421
  async function evaluateToolFeasibility(toolIdea, siteSummary, competitiveData, options) {
21379
- const openai = new import_openai3.default({ apiKey: options.openaiApiKey });
21422
+ const llm = judgeLLM(options);
21380
21423
  const minFeasibility = options.minFeasibilityScore ?? 3;
21381
21424
  const minSpecificity = options.minSpecificityScore ?? 3;
21382
21425
  const minProductFit = options.minProductFitScore ?? 2;
@@ -21452,13 +21495,10 @@ Respond in JSON:
21452
21495
  "blockers": ["<issues that prevent building>"],
21453
21496
  "improvements": ["<suggestions to make it more specific/buildable>"]
21454
21497
  }`;
21455
- const response = await openai.chat.completions.create({
21456
- model: "gpt-4o-mini",
21457
- messages: [{ role: "user", content: prompt }],
21458
- response_format: { type: "json_object" },
21459
- temperature: 0.3
21460
- });
21461
- const result = JSON.parse(response.choices[0].message.content || "{}");
21498
+ const result = parseJsonReply(await llm.complete(prompt, { temperature: 0.3 })) ?? {};
21499
+ for (const key of ["feasibility", "specificity", "productFit", "marketOpportunity"]) {
21500
+ result[key] = Number(result[key]) || 0;
21501
+ }
21462
21502
  const overallScore = result.feasibility * 0.3 + result.specificity * 0.3 + result.productFit * 0.2 + result.marketOpportunity * 0.2;
21463
21503
  const passes = result.feasibility >= minFeasibility && result.specificity >= minSpecificity && result.productFit >= minProductFit;
21464
21504
  return {
@@ -21474,7 +21514,7 @@ Respond in JSON:
21474
21514
  };
21475
21515
  }
21476
21516
  async function enhanceToolIdea(toolIdea, siteSummary, competitiveData, options) {
21477
- const openai = new import_openai3.default({ apiKey: options.openaiApiKey });
21517
+ const llm = judgeLLM(options);
21478
21518
  const feasibilityScore = await evaluateToolFeasibility(
21479
21519
  toolIdea,
21480
21520
  siteSummary,
@@ -21519,13 +21559,7 @@ ${competitiveData?.openSourceOptions.slice(0, 3).map((t) => `- OSS: ${t.name} ($
21519
21559
  "canUseExistingOSS": <true if an OSS library does most of the work>,
21520
21560
  "ossLibrary": "<name of OSS library to use, or null>"
21521
21561
  }`;
21522
- const response = await openai.chat.completions.create({
21523
- model: "gpt-4o",
21524
- messages: [{ role: "user", content: prompt }],
21525
- response_format: { type: "json_object" },
21526
- temperature: 0.5
21527
- });
21528
- const enhanced = JSON.parse(response.choices[0].message.content || "{}");
21562
+ const enhanced = parseJsonReply(await llm.complete(prompt, { temperature: 0.5, best: true })) ?? {};
21529
21563
  const priority = Math.round(
21530
21564
  feasibilityScore.overallScore * 20 + toolIdea.volume / 1e3 * 10 + (100 - toolIdea.difficulty) / 100 * 20
21531
21565
  );
@@ -21576,7 +21610,7 @@ async function evaluateAndEnhanceToolIdeas(toolIdeas, siteSummary, getCompetitiv
21576
21610
 
21577
21611
  // src/keywords/ai-keyword-engine.ts
21578
21612
  async function runAIKeywordResearch(options) {
21579
- const { url, tier, openaiApiKey, maxPages = 20 } = options;
21613
+ const { url, tier, maxPages = 20 } = options;
21580
21614
  console.log(`
21581
21615
  \u{1F52C} Starting AI Keyword Research (${tier} tier)...
21582
21616
  `);
@@ -21584,10 +21618,11 @@ async function runAIKeywordResearch(options) {
21584
21618
  if (tier === "free") {
21585
21619
  return runFreeKeywordResearch(crawlResult, options);
21586
21620
  } else {
21587
- if (!openaiApiKey) {
21588
- throw new Error("OpenAI API key required for paid tier keyword research");
21621
+ const llm = keywordLLM(options);
21622
+ if (!llm) {
21623
+ throw new Error("An AI provider (or an OpenAI API key) is required for paid tier keyword research");
21589
21624
  }
21590
- return runPaidKeywordResearch(crawlResult, options);
21625
+ return runPaidKeywordResearch(crawlResult, options, llm);
21591
21626
  }
21592
21627
  }
21593
21628
  async function runFreeKeywordResearch(crawlResult, options) {
@@ -21641,13 +21676,11 @@ async function runFreeKeywordResearch(crawlResult, options) {
21641
21676
  ]
21642
21677
  };
21643
21678
  }
21644
- async function runPaidKeywordResearch(crawlResult, options) {
21645
- const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
21679
+ async function runPaidKeywordResearch(crawlResult, options, llm) {
21680
+ const { userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
21646
21681
  const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false;
21647
21682
  console.log("\u{1F680} Running full AI analysis...");
21648
- let siteSummary = await summarizeSite(crawlResult, {
21649
- openaiApiKey
21650
- });
21683
+ let siteSummary = await summarizeSite(crawlResult, { llm });
21651
21684
  if (userContext) {
21652
21685
  siteSummary = applyUserContext(siteSummary, userContext);
21653
21686
  }
@@ -21656,7 +21689,7 @@ async function runPaidKeywordResearch(crawlResult, options) {
21656
21689
  }
21657
21690
  const documents = crawlResult.pages.map((p) => p.mainContent);
21658
21691
  const nlpAnalysis = await runNLPAnalysis(documents, {
21659
- openaiApiKey,
21692
+ llm,
21660
21693
  numClusters: 8,
21661
21694
  numTopics: 6
21662
21695
  });
@@ -21683,13 +21716,13 @@ async function runPaidKeywordResearch(crawlResult, options) {
21683
21716
  nlpAnalysis
21684
21717
  );
21685
21718
  let freeToolIdeas;
21686
- if (enableEnhancedToolIdeas && openaiApiKey) {
21719
+ if (enableEnhancedToolIdeas) {
21687
21720
  console.log("\u{1F52C} Running enhanced tool idea analysis with competitive research...");
21688
21721
  freeToolIdeas = await generateEnhancedFreeToolIdeas(
21689
21722
  opportunities,
21690
21723
  siteSummary,
21691
21724
  {
21692
- openaiApiKey,
21725
+ llm,
21693
21726
  ...competitiveSearchKeys
21694
21727
  }
21695
21728
  );
@@ -21787,7 +21820,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
21787
21820
  kw.volume >= 300 && // Slightly lower threshold
21788
21821
  (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")
21789
21822
  );
21790
- const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
21823
+ const basicIdeas = toolKeywords.slice(0, options.llm.maxToolIdeas ?? 15).map((kw) => ({
21791
21824
  keyword: kw.keyword,
21792
21825
  toolName: generateToolName(kw.keyword),
21793
21826
  toolDescription: generateToolDescription(kw.keyword, summary),
@@ -21817,7 +21850,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
21817
21850
  basicIdeas,
21818
21851
  summary,
21819
21852
  getCompetitiveData,
21820
- { openaiApiKey: options.openaiApiKey }
21853
+ { llm: options.llm }
21821
21854
  );
21822
21855
  return enhancedIdeas.map((enhanced) => ({
21823
21856
  keyword: enhanced.keyword,
@@ -22148,10 +22181,9 @@ async function startWizardSession(url, options = {}) {
22148
22181
  console.log("\u{1F50D} Analyzing your site...");
22149
22182
  const crawlResult = await crawlSite(url, { maxPages: 10 });
22150
22183
  let siteSummary;
22151
- if (options.openaiApiKey) {
22152
- siteSummary = await summarizeSite(crawlResult, {
22153
- openaiApiKey: options.openaiApiKey
22154
- });
22184
+ const llm = keywordLLM(options);
22185
+ if (llm) {
22186
+ siteSummary = await summarizeSite(crawlResult, { llm });
22155
22187
  } else {
22156
22188
  siteSummary = {
22157
22189
  productName: crawlResult.domain,
@@ -35072,24 +35104,32 @@ var RankTracker = class {
35072
35104
  }).eq("id", keywordId);
35073
35105
  if (updateError) {
35074
35106
  console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
35107
+ return;
35108
+ }
35109
+ const { error: historyError } = await this.supabase.from("keyword_position_history").insert({
35110
+ keyword_id: keywordId,
35111
+ position: result.position,
35112
+ url: result.url,
35113
+ checked_at: result.checkedAt.toISOString()
35114
+ });
35115
+ if (historyError) {
35116
+ console.error(`Failed to record history for ${result.keyword}:`, historyError);
35075
35117
  }
35076
35118
  }
35077
35119
  /**
35078
- * Get ranking history for a keyword
35079
- * Note: Full history requires keyword_ranking_history table with keyword_ranking_id
35080
- * For now, returns current state as single history entry
35120
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
35081
35121
  */
35082
- async getHistory(keywordId, _days = 30) {
35083
- const { data, error } = await this.supabase.from("keywords").select("current_position, target_url, last_checked").eq("id", keywordId).single();
35122
+ async getHistory(keywordId, days = 30) {
35123
+ 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 });
35084
35124
  if (error || !data) {
35085
35125
  return [];
35086
35126
  }
35087
- return [{
35088
- position: data.current_position,
35089
- url: data.target_url,
35127
+ return data.map((row) => ({
35128
+ position: row.position,
35129
+ url: row.url,
35090
35130
  serpFeatures: [],
35091
- recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
35092
- }];
35131
+ recordedAt: new Date(row.checked_at)
35132
+ }));
35093
35133
  }
35094
35134
  /**
35095
35135
  * Get keyword trends for a project
@@ -35594,14 +35634,17 @@ var SUPPORTED_FRAMEWORKS = [
35594
35634
  injectGSCVerification,
35595
35635
  interpolatePrompt,
35596
35636
  isGitRepo,
35637
+ keywordLLM,
35597
35638
  listFiles,
35598
35639
  loadAgent,
35599
35640
  loadAgentByName,
35600
35641
  mergePages,
35642
+ openAIKeywordLLM,
35601
35643
  optimizeForAI,
35602
35644
  optimizeReadme,
35603
35645
  parseGEOResponse,
35604
35646
  parseGSCResponse,
35647
+ parseJsonReply,
35605
35648
  parseSitemap,
35606
35649
  prioritizeKeywords,
35607
35650
  processWizardResponse,
package/dist/index.mjs CHANGED
@@ -4326,21 +4326,28 @@ var HTML_SIGNATURES = [
4326
4326
  {
4327
4327
  framework: "wordpress",
4328
4328
  language: "php",
4329
- patterns: [/wp-content/, /wp-includes/, /wp-json/, /wordpress/i],
4329
+ // Technical signatures only. These patterns run over the whole page, so the old
4330
+ // /wordpress/i matched the word anywhere in the copy: rankcli.dev's own "detects
4331
+ // Rails, Django, Laravel, and WordPress" line was enough to detect its React SPA as
4332
+ // WordPress and then tell it to install an SEO plugin. A generator meta tag is the
4333
+ // one place the page says it about itself.
4334
+ patterns: [/wp-content/, /wp-includes/, /wp-json/, /name=["']generator["'][^>]*content=["']WordPress/i],
4330
4335
  confidence: "high"
4331
4336
  },
4332
4337
  // Laravel
4333
4338
  {
4334
4339
  framework: "laravel",
4335
4340
  language: "php",
4336
- patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/, /laravel/i],
4341
+ // /laravel/i had the same problem as WordPress above - it matched the word in prose.
4342
+ patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/],
4337
4343
  confidence: "medium"
4338
4344
  },
4339
4345
  // Symfony
4340
4346
  {
4341
4347
  framework: "symfony",
4342
4348
  language: "php",
4343
- patterns: [/symfony/, /sf2/, /_sf_/, /profiler\.css/],
4349
+ // Likewise /symfony/, which matched any lowercase mention.
4350
+ patterns: [/sf2/, /_sf_/, /profiler\.css/],
4344
4351
  confidence: "medium"
4345
4352
  },
4346
4353
  // ============ Ruby Frameworks ============
@@ -16564,6 +16571,50 @@ async function extractSeedKeywords(url) {
16564
16571
  }
16565
16572
  }
16566
16573
 
16574
+ // src/keywords/llm.ts
16575
+ import OpenAI from "openai";
16576
+ function openAIKeywordLLM(apiKey, opts = {}) {
16577
+ const openai = new OpenAI({ apiKey });
16578
+ const openAIModel = opts.model;
16579
+ return {
16580
+ async complete(prompt, opts2 = {}) {
16581
+ const response = await openai.chat.completions.create({
16582
+ model: opts2.best ? "gpt-4o" : openAIModel ?? "gpt-4o-mini",
16583
+ messages: [
16584
+ ...opts2.system ? [{ role: "system", content: opts2.system }] : [],
16585
+ { role: "user", content: prompt }
16586
+ ],
16587
+ response_format: { type: "json_object" },
16588
+ temperature: opts2.temperature ?? 0.3,
16589
+ ...opts2.maxTokens ? { max_tokens: opts2.maxTokens } : {}
16590
+ });
16591
+ return response.choices[0]?.message?.content || "";
16592
+ },
16593
+ async embed(texts) {
16594
+ const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: texts });
16595
+ return response.data.map((item) => item.embedding);
16596
+ }
16597
+ };
16598
+ }
16599
+ function keywordLLM(options) {
16600
+ return options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey) : void 0);
16601
+ }
16602
+ function parseJsonReply(text) {
16603
+ const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
16604
+ const start = cleaned.indexOf("{");
16605
+ const end = cleaned.lastIndexOf("}");
16606
+ if (start === -1 || end <= start) return null;
16607
+ const candidate = cleaned.slice(start, end + 1);
16608
+ for (const attempt of [candidate, candidate.replace(/,\s*([}\]])/g, "$1")]) {
16609
+ try {
16610
+ const parsed = JSON.parse(attempt);
16611
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
16612
+ } catch {
16613
+ }
16614
+ }
16615
+ return null;
16616
+ }
16617
+
16567
16618
  // src/keywords/site-crawler.ts
16568
16619
  import * as cheerio40 from "cheerio";
16569
16620
  var EXCLUDED_PATHS = [
@@ -16773,7 +16824,11 @@ function extractKeyPhrases(crawlResult) {
16773
16824
  }
16774
16825
 
16775
16826
  // src/keywords/ai-summarizer.ts
16776
- import OpenAI from "openai";
16827
+ function summarizerLLM(options) {
16828
+ const llm = options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey, { model: options.model }) : void 0);
16829
+ if (!llm) throw new Error("An AI provider is required to summarize the site");
16830
+ return llm;
16831
+ }
16777
16832
  var SUMMARIZER_PROMPT = `You are an expert SEO analyst. Analyze the following website content and extract key information for keyword research.
16778
16833
 
16779
16834
  WEBSITE CONTENT:
@@ -16816,37 +16871,26 @@ IMPORTANT:
16816
16871
 
16817
16872
  Respond ONLY with the JSON object, no other text.`;
16818
16873
  async function summarizeSite(crawlResult, options) {
16819
- const openai = new OpenAI({ apiKey: options.openaiApiKey });
16820
- const model = options.model || "gpt-4o-mini";
16874
+ const llm = summarizerLLM(options);
16875
+ const maxContent = llm.maxContentChars ?? 15e3;
16821
16876
  let content = crawlResult.aggregatedContent;
16822
- if (content.length > 15e3) {
16823
- content = content.substring(0, 15e3) + "\n...[truncated]";
16877
+ if (content.length > maxContent) {
16878
+ content = content.substring(0, maxContent) + "\n...[truncated]";
16824
16879
  }
16825
16880
  const headings = crawlResult.uniqueHeadings.slice(0, 30).join("\n");
16826
16881
  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));
16827
16882
  console.log("\u{1F916} Analyzing site content with AI...");
16828
16883
  try {
16829
- const response = await openai.chat.completions.create({
16830
- model,
16831
- messages: [
16832
- {
16833
- role: "system",
16834
- content: "You are an expert SEO analyst. Always respond with valid JSON only."
16835
- },
16836
- {
16837
- role: "user",
16838
- content: prompt
16839
- }
16840
- ],
16884
+ const responseText = await llm.complete(prompt, {
16885
+ system: "You are an expert SEO analyst. Always respond with valid JSON only.",
16841
16886
  temperature: 0.3,
16842
- max_tokens: options.maxTokens || 2e3
16887
+ maxTokens: options.maxTokens || 2e3
16843
16888
  });
16844
- const responseText = response.choices[0]?.message?.content || "";
16845
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
16846
- if (!jsonMatch) {
16889
+ const parsed = parseJsonReply(responseText);
16890
+ if (!parsed) {
16847
16891
  throw new Error("No JSON found in response");
16848
16892
  }
16849
- const summary = JSON.parse(jsonMatch[0]);
16893
+ const summary = parsed;
16850
16894
  return {
16851
16895
  productName: summary.productName || "Unknown Product",
16852
16896
  productDescription: summary.productDescription || "No description available",
@@ -16908,7 +16952,7 @@ function createFallbackSummary(crawlResult) {
16908
16952
  };
16909
16953
  }
16910
16954
  async function enhanceSummaryWithCompetitors(summary, competitors, options) {
16911
- const openai = new OpenAI({ apiKey: options.openaiApiKey });
16955
+ const llm = summarizerLLM(options);
16912
16956
  const prompt = `Given this product summary:
16913
16957
  Product: ${summary.productName}
16914
16958
  Description: ${summary.productDescription}
@@ -16927,19 +16971,13 @@ Analyze the competitive positioning. If this product focuses on a niche segment
16927
16971
  "updatedUncertainties": ["uncertainty1", "uncertainty2"]
16928
16972
  }`;
16929
16973
  try {
16930
- const response = await openai.chat.completions.create({
16931
- model: options.model || "gpt-4o-mini",
16932
- messages: [
16933
- { role: "system", content: "Respond with valid JSON only." },
16934
- { role: "user", content: prompt }
16935
- ],
16974
+ const responseText = await llm.complete(prompt, {
16975
+ system: "Respond with valid JSON only.",
16936
16976
  temperature: 0.3,
16937
- max_tokens: 1e3
16977
+ maxTokens: 1e3
16938
16978
  });
16939
- const responseText = response.choices[0]?.message?.content || "";
16940
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
16941
- if (jsonMatch) {
16942
- const analysis = JSON.parse(jsonMatch[0]);
16979
+ const analysis = parseJsonReply(responseText);
16980
+ if (analysis) {
16943
16981
  return {
16944
16982
  ...summary,
16945
16983
  competitivePositioning: analysis.competitivePositioning || "",
@@ -17026,7 +17064,6 @@ function generateUncertaintyQuestions(summary) {
17026
17064
  }
17027
17065
 
17028
17066
  // src/keywords/nlp-analysis.ts
17029
- import OpenAI2 from "openai";
17030
17067
  var STOP_WORDS2 = /* @__PURE__ */ new Set([
17031
17068
  "a",
17032
17069
  "an",
@@ -17311,19 +17348,18 @@ function createTermVectors(keywords, vocabulary) {
17311
17348
  return vector;
17312
17349
  });
17313
17350
  }
17351
+ async function embedBatch(embedder, batch) {
17352
+ if (typeof embedder === "function") return embedder(batch);
17353
+ const response = await embedder.embeddings.create({ model: "text-embedding-3-small", input: batch });
17354
+ return response.data.map((item) => item.embedding);
17355
+ }
17314
17356
  async function getEmbeddings(texts, openai) {
17315
17357
  const embeddings = [];
17316
17358
  const batchSize = 100;
17317
17359
  for (let i = 0; i < texts.length; i += batchSize) {
17318
17360
  const batch = texts.slice(i, i + batchSize);
17319
17361
  try {
17320
- const response = await openai.embeddings.create({
17321
- model: "text-embedding-3-small",
17322
- input: batch
17323
- });
17324
- for (const item of response.data) {
17325
- embeddings.push(item.embedding);
17326
- }
17362
+ embeddings.push(...await embedBatch(openai, batch));
17327
17363
  } catch (error) {
17328
17364
  console.warn("Embedding API failed, using fallback BOW vectors");
17329
17365
  for (const text of batch) {
@@ -17478,12 +17514,12 @@ async function runNLPAnalysis(content, options = {}) {
17478
17514
  const entityPhrases = extractEntityPhrases(combinedText);
17479
17515
  let clusters = [];
17480
17516
  const keywordsToCluster = tfidfKeywords.slice(0, 50).map((k) => k.term);
17481
- if (options.openaiApiKey && keywordsToCluster.length > 0) {
17517
+ const embed = keywordLLM(options)?.embed;
17518
+ if (embed && keywordsToCluster.length > 0) {
17482
17519
  try {
17483
- const openai = new OpenAI2({ apiKey: options.openaiApiKey });
17484
17520
  clusters = await clusterKeywordsByEmbedding(
17485
17521
  keywordsToCluster,
17486
- openai,
17522
+ (texts) => embed(texts),
17487
17523
  options.numClusters
17488
17524
  );
17489
17525
  } catch (error) {
@@ -17826,9 +17862,13 @@ async function searchHackerNews(query, maxResults = 10) {
17826
17862
  }
17827
17863
 
17828
17864
  // src/keywords/llm-judge.ts
17829
- import OpenAI3 from "openai";
17865
+ function judgeLLM(options) {
17866
+ const llm = keywordLLM(options);
17867
+ if (!llm) throw new Error("An AI provider is required to judge tool ideas");
17868
+ return llm;
17869
+ }
17830
17870
  async function evaluateToolFeasibility(toolIdea, siteSummary, competitiveData, options) {
17831
- const openai = new OpenAI3({ apiKey: options.openaiApiKey });
17871
+ const llm = judgeLLM(options);
17832
17872
  const minFeasibility = options.minFeasibilityScore ?? 3;
17833
17873
  const minSpecificity = options.minSpecificityScore ?? 3;
17834
17874
  const minProductFit = options.minProductFitScore ?? 2;
@@ -17904,13 +17944,10 @@ Respond in JSON:
17904
17944
  "blockers": ["<issues that prevent building>"],
17905
17945
  "improvements": ["<suggestions to make it more specific/buildable>"]
17906
17946
  }`;
17907
- const response = await openai.chat.completions.create({
17908
- model: "gpt-4o-mini",
17909
- messages: [{ role: "user", content: prompt }],
17910
- response_format: { type: "json_object" },
17911
- temperature: 0.3
17912
- });
17913
- const result = JSON.parse(response.choices[0].message.content || "{}");
17947
+ const result = parseJsonReply(await llm.complete(prompt, { temperature: 0.3 })) ?? {};
17948
+ for (const key of ["feasibility", "specificity", "productFit", "marketOpportunity"]) {
17949
+ result[key] = Number(result[key]) || 0;
17950
+ }
17914
17951
  const overallScore = result.feasibility * 0.3 + result.specificity * 0.3 + result.productFit * 0.2 + result.marketOpportunity * 0.2;
17915
17952
  const passes = result.feasibility >= minFeasibility && result.specificity >= minSpecificity && result.productFit >= minProductFit;
17916
17953
  return {
@@ -17926,7 +17963,7 @@ Respond in JSON:
17926
17963
  };
17927
17964
  }
17928
17965
  async function enhanceToolIdea(toolIdea, siteSummary, competitiveData, options) {
17929
- const openai = new OpenAI3({ apiKey: options.openaiApiKey });
17966
+ const llm = judgeLLM(options);
17930
17967
  const feasibilityScore = await evaluateToolFeasibility(
17931
17968
  toolIdea,
17932
17969
  siteSummary,
@@ -17971,13 +18008,7 @@ ${competitiveData?.openSourceOptions.slice(0, 3).map((t) => `- OSS: ${t.name} ($
17971
18008
  "canUseExistingOSS": <true if an OSS library does most of the work>,
17972
18009
  "ossLibrary": "<name of OSS library to use, or null>"
17973
18010
  }`;
17974
- const response = await openai.chat.completions.create({
17975
- model: "gpt-4o",
17976
- messages: [{ role: "user", content: prompt }],
17977
- response_format: { type: "json_object" },
17978
- temperature: 0.5
17979
- });
17980
- const enhanced = JSON.parse(response.choices[0].message.content || "{}");
18011
+ const enhanced = parseJsonReply(await llm.complete(prompt, { temperature: 0.5, best: true })) ?? {};
17981
18012
  const priority = Math.round(
17982
18013
  feasibilityScore.overallScore * 20 + toolIdea.volume / 1e3 * 10 + (100 - toolIdea.difficulty) / 100 * 20
17983
18014
  );
@@ -18028,7 +18059,7 @@ async function evaluateAndEnhanceToolIdeas(toolIdeas, siteSummary, getCompetitiv
18028
18059
 
18029
18060
  // src/keywords/ai-keyword-engine.ts
18030
18061
  async function runAIKeywordResearch(options) {
18031
- const { url, tier, openaiApiKey, maxPages = 20 } = options;
18062
+ const { url, tier, maxPages = 20 } = options;
18032
18063
  console.log(`
18033
18064
  \u{1F52C} Starting AI Keyword Research (${tier} tier)...
18034
18065
  `);
@@ -18036,10 +18067,11 @@ async function runAIKeywordResearch(options) {
18036
18067
  if (tier === "free") {
18037
18068
  return runFreeKeywordResearch(crawlResult, options);
18038
18069
  } else {
18039
- if (!openaiApiKey) {
18040
- throw new Error("OpenAI API key required for paid tier keyword research");
18070
+ const llm = keywordLLM(options);
18071
+ if (!llm) {
18072
+ throw new Error("An AI provider (or an OpenAI API key) is required for paid tier keyword research");
18041
18073
  }
18042
- return runPaidKeywordResearch(crawlResult, options);
18074
+ return runPaidKeywordResearch(crawlResult, options, llm);
18043
18075
  }
18044
18076
  }
18045
18077
  async function runFreeKeywordResearch(crawlResult, options) {
@@ -18093,13 +18125,11 @@ async function runFreeKeywordResearch(crawlResult, options) {
18093
18125
  ]
18094
18126
  };
18095
18127
  }
18096
- async function runPaidKeywordResearch(crawlResult, options) {
18097
- const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
18128
+ async function runPaidKeywordResearch(crawlResult, options, llm) {
18129
+ const { userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
18098
18130
  const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false;
18099
18131
  console.log("\u{1F680} Running full AI analysis...");
18100
- let siteSummary = await summarizeSite(crawlResult, {
18101
- openaiApiKey
18102
- });
18132
+ let siteSummary = await summarizeSite(crawlResult, { llm });
18103
18133
  if (userContext) {
18104
18134
  siteSummary = applyUserContext(siteSummary, userContext);
18105
18135
  }
@@ -18108,7 +18138,7 @@ async function runPaidKeywordResearch(crawlResult, options) {
18108
18138
  }
18109
18139
  const documents = crawlResult.pages.map((p) => p.mainContent);
18110
18140
  const nlpAnalysis = await runNLPAnalysis(documents, {
18111
- openaiApiKey,
18141
+ llm,
18112
18142
  numClusters: 8,
18113
18143
  numTopics: 6
18114
18144
  });
@@ -18135,13 +18165,13 @@ async function runPaidKeywordResearch(crawlResult, options) {
18135
18165
  nlpAnalysis
18136
18166
  );
18137
18167
  let freeToolIdeas;
18138
- if (enableEnhancedToolIdeas && openaiApiKey) {
18168
+ if (enableEnhancedToolIdeas) {
18139
18169
  console.log("\u{1F52C} Running enhanced tool idea analysis with competitive research...");
18140
18170
  freeToolIdeas = await generateEnhancedFreeToolIdeas(
18141
18171
  opportunities,
18142
18172
  siteSummary,
18143
18173
  {
18144
- openaiApiKey,
18174
+ llm,
18145
18175
  ...competitiveSearchKeys
18146
18176
  }
18147
18177
  );
@@ -18239,7 +18269,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
18239
18269
  kw.volume >= 300 && // Slightly lower threshold
18240
18270
  (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")
18241
18271
  );
18242
- const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
18272
+ const basicIdeas = toolKeywords.slice(0, options.llm.maxToolIdeas ?? 15).map((kw) => ({
18243
18273
  keyword: kw.keyword,
18244
18274
  toolName: generateToolName(kw.keyword),
18245
18275
  toolDescription: generateToolDescription(kw.keyword, summary),
@@ -18269,7 +18299,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
18269
18299
  basicIdeas,
18270
18300
  summary,
18271
18301
  getCompetitiveData,
18272
- { openaiApiKey: options.openaiApiKey }
18302
+ { llm: options.llm }
18273
18303
  );
18274
18304
  return enhancedIdeas.map((enhanced) => ({
18275
18305
  keyword: enhanced.keyword,
@@ -18600,10 +18630,9 @@ async function startWizardSession(url, options = {}) {
18600
18630
  console.log("\u{1F50D} Analyzing your site...");
18601
18631
  const crawlResult = await crawlSite(url, { maxPages: 10 });
18602
18632
  let siteSummary;
18603
- if (options.openaiApiKey) {
18604
- siteSummary = await summarizeSite(crawlResult, {
18605
- openaiApiKey: options.openaiApiKey
18606
- });
18633
+ const llm = keywordLLM(options);
18634
+ if (llm) {
18635
+ siteSummary = await summarizeSite(crawlResult, { llm });
18607
18636
  } else {
18608
18637
  siteSummary = {
18609
18638
  productName: crawlResult.domain,
@@ -31517,24 +31546,32 @@ var RankTracker = class {
31517
31546
  }).eq("id", keywordId);
31518
31547
  if (updateError) {
31519
31548
  console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
31549
+ return;
31550
+ }
31551
+ const { error: historyError } = await this.supabase.from("keyword_position_history").insert({
31552
+ keyword_id: keywordId,
31553
+ position: result.position,
31554
+ url: result.url,
31555
+ checked_at: result.checkedAt.toISOString()
31556
+ });
31557
+ if (historyError) {
31558
+ console.error(`Failed to record history for ${result.keyword}:`, historyError);
31520
31559
  }
31521
31560
  }
31522
31561
  /**
31523
- * Get ranking history for a keyword
31524
- * Note: Full history requires keyword_ranking_history table with keyword_ranking_id
31525
- * For now, returns current state as single history entry
31562
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
31526
31563
  */
31527
- async getHistory(keywordId, _days = 30) {
31528
- const { data, error } = await this.supabase.from("keywords").select("current_position, target_url, last_checked").eq("id", keywordId).single();
31564
+ async getHistory(keywordId, days = 30) {
31565
+ 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 });
31529
31566
  if (error || !data) {
31530
31567
  return [];
31531
31568
  }
31532
- return [{
31533
- position: data.current_position,
31534
- url: data.target_url,
31569
+ return data.map((row) => ({
31570
+ position: row.position,
31571
+ url: row.url,
31535
31572
  serpFeatures: [],
31536
- recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
31537
- }];
31573
+ recordedAt: new Date(row.checked_at)
31574
+ }));
31538
31575
  }
31539
31576
  /**
31540
31577
  * Get keyword trends for a project
@@ -32031,14 +32068,17 @@ export {
32031
32068
  injectGSCVerification,
32032
32069
  interpolatePrompt,
32033
32070
  isGitRepo,
32071
+ keywordLLM,
32034
32072
  listFiles,
32035
32073
  loadAgent,
32036
32074
  loadAgentByName,
32037
32075
  mergePages,
32076
+ openAIKeywordLLM,
32038
32077
  optimizeForAI,
32039
32078
  optimizeReadme,
32040
32079
  parseGEOResponse,
32041
32080
  parseGSCResponse,
32081
+ parseJsonReply,
32042
32082
  parseSitemap,
32043
32083
  prioritizeKeywords,
32044
32084
  processWizardResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/agent-runtime",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
5
5
  "homepage": "https://rankcli.dev",
6
6
  "main": "dist/index.js",