@rankcli/agent-runtime 0.0.23 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,
@@ -20111,6 +20114,50 @@ async function extractSeedKeywords(url) {
20111
20114
  }
20112
20115
  }
20113
20116
 
20117
+ // src/keywords/llm.ts
20118
+ var import_openai = __toESM(require("openai"));
20119
+ function openAIKeywordLLM(apiKey, opts = {}) {
20120
+ const openai = new import_openai.default({ apiKey });
20121
+ const openAIModel = opts.model;
20122
+ return {
20123
+ async complete(prompt, opts2 = {}) {
20124
+ const response = await openai.chat.completions.create({
20125
+ model: opts2.best ? "gpt-4o" : openAIModel ?? "gpt-4o-mini",
20126
+ messages: [
20127
+ ...opts2.system ? [{ role: "system", content: opts2.system }] : [],
20128
+ { role: "user", content: prompt }
20129
+ ],
20130
+ response_format: { type: "json_object" },
20131
+ temperature: opts2.temperature ?? 0.3,
20132
+ ...opts2.maxTokens ? { max_tokens: opts2.maxTokens } : {}
20133
+ });
20134
+ return response.choices[0]?.message?.content || "";
20135
+ },
20136
+ async embed(texts) {
20137
+ const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: texts });
20138
+ return response.data.map((item) => item.embedding);
20139
+ }
20140
+ };
20141
+ }
20142
+ function keywordLLM(options) {
20143
+ return options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey) : void 0);
20144
+ }
20145
+ function parseJsonReply(text) {
20146
+ const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
20147
+ const start = cleaned.indexOf("{");
20148
+ const end = cleaned.lastIndexOf("}");
20149
+ if (start === -1 || end <= start) return null;
20150
+ const candidate = cleaned.slice(start, end + 1);
20151
+ for (const attempt of [candidate, candidate.replace(/,\s*([}\]])/g, "$1")]) {
20152
+ try {
20153
+ const parsed = JSON.parse(attempt);
20154
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
20155
+ } catch {
20156
+ }
20157
+ }
20158
+ return null;
20159
+ }
20160
+
20114
20161
  // src/keywords/site-crawler.ts
20115
20162
  var cheerio40 = __toESM(require("cheerio"));
20116
20163
  init_http();
@@ -20321,7 +20368,11 @@ function extractKeyPhrases(crawlResult) {
20321
20368
  }
20322
20369
 
20323
20370
  // src/keywords/ai-summarizer.ts
20324
- var import_openai = __toESM(require("openai"));
20371
+ function summarizerLLM(options) {
20372
+ const llm = options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey, { model: options.model }) : void 0);
20373
+ if (!llm) throw new Error("An AI provider is required to summarize the site");
20374
+ return llm;
20375
+ }
20325
20376
  var SUMMARIZER_PROMPT = `You are an expert SEO analyst. Analyze the following website content and extract key information for keyword research.
20326
20377
 
20327
20378
  WEBSITE CONTENT:
@@ -20364,37 +20415,26 @@ IMPORTANT:
20364
20415
 
20365
20416
  Respond ONLY with the JSON object, no other text.`;
20366
20417
  async function summarizeSite(crawlResult, options) {
20367
- const openai = new import_openai.default({ apiKey: options.openaiApiKey });
20368
- const model = options.model || "gpt-4o-mini";
20418
+ const llm = summarizerLLM(options);
20419
+ const maxContent = llm.maxContentChars ?? 15e3;
20369
20420
  let content = crawlResult.aggregatedContent;
20370
- if (content.length > 15e3) {
20371
- content = content.substring(0, 15e3) + "\n...[truncated]";
20421
+ if (content.length > maxContent) {
20422
+ content = content.substring(0, maxContent) + "\n...[truncated]";
20372
20423
  }
20373
20424
  const headings = crawlResult.uniqueHeadings.slice(0, 30).join("\n");
20374
20425
  const prompt = SUMMARIZER_PROMPT.replace("{content}", content).replace("{headings}", headings).replace("{productPages}", String(crawlResult.detectedPageTypes.product)).replace("{pricingPages}", String(crawlResult.detectedPageTypes.pricing)).replace("{blogPages}", String(crawlResult.detectedPageTypes.blog)).replace("{featurePages}", String(crawlResult.detectedPageTypes.feature));
20375
20426
  console.log("\u{1F916} Analyzing site content with AI...");
20376
20427
  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
- ],
20428
+ const responseText = await llm.complete(prompt, {
20429
+ system: "You are an expert SEO analyst. Always respond with valid JSON only.",
20389
20430
  temperature: 0.3,
20390
- max_tokens: options.maxTokens || 2e3
20431
+ maxTokens: options.maxTokens || 2e3
20391
20432
  });
20392
- const responseText = response.choices[0]?.message?.content || "";
20393
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
20394
- if (!jsonMatch) {
20433
+ const parsed = parseJsonReply(responseText);
20434
+ if (!parsed) {
20395
20435
  throw new Error("No JSON found in response");
20396
20436
  }
20397
- const summary = JSON.parse(jsonMatch[0]);
20437
+ const summary = parsed;
20398
20438
  return {
20399
20439
  productName: summary.productName || "Unknown Product",
20400
20440
  productDescription: summary.productDescription || "No description available",
@@ -20456,7 +20496,7 @@ function createFallbackSummary(crawlResult) {
20456
20496
  };
20457
20497
  }
20458
20498
  async function enhanceSummaryWithCompetitors(summary, competitors, options) {
20459
- const openai = new import_openai.default({ apiKey: options.openaiApiKey });
20499
+ const llm = summarizerLLM(options);
20460
20500
  const prompt = `Given this product summary:
20461
20501
  Product: ${summary.productName}
20462
20502
  Description: ${summary.productDescription}
@@ -20475,19 +20515,13 @@ Analyze the competitive positioning. If this product focuses on a niche segment
20475
20515
  "updatedUncertainties": ["uncertainty1", "uncertainty2"]
20476
20516
  }`;
20477
20517
  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
- ],
20518
+ const responseText = await llm.complete(prompt, {
20519
+ system: "Respond with valid JSON only.",
20484
20520
  temperature: 0.3,
20485
- max_tokens: 1e3
20521
+ maxTokens: 1e3
20486
20522
  });
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]);
20523
+ const analysis = parseJsonReply(responseText);
20524
+ if (analysis) {
20491
20525
  return {
20492
20526
  ...summary,
20493
20527
  competitivePositioning: analysis.competitivePositioning || "",
@@ -20574,7 +20608,6 @@ function generateUncertaintyQuestions(summary) {
20574
20608
  }
20575
20609
 
20576
20610
  // src/keywords/nlp-analysis.ts
20577
- var import_openai2 = __toESM(require("openai"));
20578
20611
  var STOP_WORDS2 = /* @__PURE__ */ new Set([
20579
20612
  "a",
20580
20613
  "an",
@@ -20859,19 +20892,18 @@ function createTermVectors(keywords, vocabulary) {
20859
20892
  return vector;
20860
20893
  });
20861
20894
  }
20895
+ async function embedBatch(embedder, batch) {
20896
+ if (typeof embedder === "function") return embedder(batch);
20897
+ const response = await embedder.embeddings.create({ model: "text-embedding-3-small", input: batch });
20898
+ return response.data.map((item) => item.embedding);
20899
+ }
20862
20900
  async function getEmbeddings(texts, openai) {
20863
20901
  const embeddings = [];
20864
20902
  const batchSize = 100;
20865
20903
  for (let i = 0; i < texts.length; i += batchSize) {
20866
20904
  const batch = texts.slice(i, i + batchSize);
20867
20905
  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
- }
20906
+ embeddings.push(...await embedBatch(openai, batch));
20875
20907
  } catch (error) {
20876
20908
  console.warn("Embedding API failed, using fallback BOW vectors");
20877
20909
  for (const text of batch) {
@@ -21026,12 +21058,12 @@ async function runNLPAnalysis(content, options = {}) {
21026
21058
  const entityPhrases = extractEntityPhrases(combinedText);
21027
21059
  let clusters = [];
21028
21060
  const keywordsToCluster = tfidfKeywords.slice(0, 50).map((k) => k.term);
21029
- if (options.openaiApiKey && keywordsToCluster.length > 0) {
21061
+ const embed = keywordLLM(options)?.embed;
21062
+ if (embed && keywordsToCluster.length > 0) {
21030
21063
  try {
21031
- const openai = new import_openai2.default({ apiKey: options.openaiApiKey });
21032
21064
  clusters = await clusterKeywordsByEmbedding(
21033
21065
  keywordsToCluster,
21034
- openai,
21066
+ (texts) => embed(texts),
21035
21067
  options.numClusters
21036
21068
  );
21037
21069
  } catch (error) {
@@ -21374,9 +21406,13 @@ async function searchHackerNews(query, maxResults = 10) {
21374
21406
  }
21375
21407
 
21376
21408
  // src/keywords/llm-judge.ts
21377
- var import_openai3 = __toESM(require("openai"));
21409
+ function judgeLLM(options) {
21410
+ const llm = keywordLLM(options);
21411
+ if (!llm) throw new Error("An AI provider is required to judge tool ideas");
21412
+ return llm;
21413
+ }
21378
21414
  async function evaluateToolFeasibility(toolIdea, siteSummary, competitiveData, options) {
21379
- const openai = new import_openai3.default({ apiKey: options.openaiApiKey });
21415
+ const llm = judgeLLM(options);
21380
21416
  const minFeasibility = options.minFeasibilityScore ?? 3;
21381
21417
  const minSpecificity = options.minSpecificityScore ?? 3;
21382
21418
  const minProductFit = options.minProductFitScore ?? 2;
@@ -21452,13 +21488,10 @@ Respond in JSON:
21452
21488
  "blockers": ["<issues that prevent building>"],
21453
21489
  "improvements": ["<suggestions to make it more specific/buildable>"]
21454
21490
  }`;
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 || "{}");
21491
+ const result = parseJsonReply(await llm.complete(prompt, { temperature: 0.3 })) ?? {};
21492
+ for (const key of ["feasibility", "specificity", "productFit", "marketOpportunity"]) {
21493
+ result[key] = Number(result[key]) || 0;
21494
+ }
21462
21495
  const overallScore = result.feasibility * 0.3 + result.specificity * 0.3 + result.productFit * 0.2 + result.marketOpportunity * 0.2;
21463
21496
  const passes = result.feasibility >= minFeasibility && result.specificity >= minSpecificity && result.productFit >= minProductFit;
21464
21497
  return {
@@ -21474,7 +21507,7 @@ Respond in JSON:
21474
21507
  };
21475
21508
  }
21476
21509
  async function enhanceToolIdea(toolIdea, siteSummary, competitiveData, options) {
21477
- const openai = new import_openai3.default({ apiKey: options.openaiApiKey });
21510
+ const llm = judgeLLM(options);
21478
21511
  const feasibilityScore = await evaluateToolFeasibility(
21479
21512
  toolIdea,
21480
21513
  siteSummary,
@@ -21519,13 +21552,7 @@ ${competitiveData?.openSourceOptions.slice(0, 3).map((t) => `- OSS: ${t.name} ($
21519
21552
  "canUseExistingOSS": <true if an OSS library does most of the work>,
21520
21553
  "ossLibrary": "<name of OSS library to use, or null>"
21521
21554
  }`;
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 || "{}");
21555
+ const enhanced = parseJsonReply(await llm.complete(prompt, { temperature: 0.5, best: true })) ?? {};
21529
21556
  const priority = Math.round(
21530
21557
  feasibilityScore.overallScore * 20 + toolIdea.volume / 1e3 * 10 + (100 - toolIdea.difficulty) / 100 * 20
21531
21558
  );
@@ -21576,7 +21603,7 @@ async function evaluateAndEnhanceToolIdeas(toolIdeas, siteSummary, getCompetitiv
21576
21603
 
21577
21604
  // src/keywords/ai-keyword-engine.ts
21578
21605
  async function runAIKeywordResearch(options) {
21579
- const { url, tier, openaiApiKey, maxPages = 20 } = options;
21606
+ const { url, tier, maxPages = 20 } = options;
21580
21607
  console.log(`
21581
21608
  \u{1F52C} Starting AI Keyword Research (${tier} tier)...
21582
21609
  `);
@@ -21584,10 +21611,11 @@ async function runAIKeywordResearch(options) {
21584
21611
  if (tier === "free") {
21585
21612
  return runFreeKeywordResearch(crawlResult, options);
21586
21613
  } else {
21587
- if (!openaiApiKey) {
21588
- throw new Error("OpenAI API key required for paid tier keyword research");
21614
+ const llm = keywordLLM(options);
21615
+ if (!llm) {
21616
+ throw new Error("An AI provider (or an OpenAI API key) is required for paid tier keyword research");
21589
21617
  }
21590
- return runPaidKeywordResearch(crawlResult, options);
21618
+ return runPaidKeywordResearch(crawlResult, options, llm);
21591
21619
  }
21592
21620
  }
21593
21621
  async function runFreeKeywordResearch(crawlResult, options) {
@@ -21641,13 +21669,11 @@ async function runFreeKeywordResearch(crawlResult, options) {
21641
21669
  ]
21642
21670
  };
21643
21671
  }
21644
- async function runPaidKeywordResearch(crawlResult, options) {
21645
- const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
21672
+ async function runPaidKeywordResearch(crawlResult, options, llm) {
21673
+ const { userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
21646
21674
  const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false;
21647
21675
  console.log("\u{1F680} Running full AI analysis...");
21648
- let siteSummary = await summarizeSite(crawlResult, {
21649
- openaiApiKey
21650
- });
21676
+ let siteSummary = await summarizeSite(crawlResult, { llm });
21651
21677
  if (userContext) {
21652
21678
  siteSummary = applyUserContext(siteSummary, userContext);
21653
21679
  }
@@ -21656,7 +21682,7 @@ async function runPaidKeywordResearch(crawlResult, options) {
21656
21682
  }
21657
21683
  const documents = crawlResult.pages.map((p) => p.mainContent);
21658
21684
  const nlpAnalysis = await runNLPAnalysis(documents, {
21659
- openaiApiKey,
21685
+ llm,
21660
21686
  numClusters: 8,
21661
21687
  numTopics: 6
21662
21688
  });
@@ -21683,13 +21709,13 @@ async function runPaidKeywordResearch(crawlResult, options) {
21683
21709
  nlpAnalysis
21684
21710
  );
21685
21711
  let freeToolIdeas;
21686
- if (enableEnhancedToolIdeas && openaiApiKey) {
21712
+ if (enableEnhancedToolIdeas) {
21687
21713
  console.log("\u{1F52C} Running enhanced tool idea analysis with competitive research...");
21688
21714
  freeToolIdeas = await generateEnhancedFreeToolIdeas(
21689
21715
  opportunities,
21690
21716
  siteSummary,
21691
21717
  {
21692
- openaiApiKey,
21718
+ llm,
21693
21719
  ...competitiveSearchKeys
21694
21720
  }
21695
21721
  );
@@ -21787,7 +21813,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
21787
21813
  kw.volume >= 300 && // Slightly lower threshold
21788
21814
  (kw.keyword.includes("generator") || kw.keyword.includes("tool") || kw.keyword.includes("maker") || kw.keyword.includes("builder") || kw.keyword.includes("converter") || kw.keyword.includes("calculator") || kw.keyword.includes("checker") || kw.keyword.includes("validator") || kw.keyword.includes("formatter") || kw.keyword.includes("free") || kw.intent === "informational")
21789
21815
  );
21790
- const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
21816
+ const basicIdeas = toolKeywords.slice(0, options.llm.maxToolIdeas ?? 15).map((kw) => ({
21791
21817
  keyword: kw.keyword,
21792
21818
  toolName: generateToolName(kw.keyword),
21793
21819
  toolDescription: generateToolDescription(kw.keyword, summary),
@@ -21817,7 +21843,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
21817
21843
  basicIdeas,
21818
21844
  summary,
21819
21845
  getCompetitiveData,
21820
- { openaiApiKey: options.openaiApiKey }
21846
+ { llm: options.llm }
21821
21847
  );
21822
21848
  return enhancedIdeas.map((enhanced) => ({
21823
21849
  keyword: enhanced.keyword,
@@ -22148,10 +22174,9 @@ async function startWizardSession(url, options = {}) {
22148
22174
  console.log("\u{1F50D} Analyzing your site...");
22149
22175
  const crawlResult = await crawlSite(url, { maxPages: 10 });
22150
22176
  let siteSummary;
22151
- if (options.openaiApiKey) {
22152
- siteSummary = await summarizeSite(crawlResult, {
22153
- openaiApiKey: options.openaiApiKey
22154
- });
22177
+ const llm = keywordLLM(options);
22178
+ if (llm) {
22179
+ siteSummary = await summarizeSite(crawlResult, { llm });
22155
22180
  } else {
22156
22181
  siteSummary = {
22157
22182
  productName: crawlResult.domain,
@@ -35072,24 +35097,32 @@ var RankTracker = class {
35072
35097
  }).eq("id", keywordId);
35073
35098
  if (updateError) {
35074
35099
  console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
35100
+ return;
35101
+ }
35102
+ const { error: historyError } = await this.supabase.from("keyword_position_history").insert({
35103
+ keyword_id: keywordId,
35104
+ position: result.position,
35105
+ url: result.url,
35106
+ checked_at: result.checkedAt.toISOString()
35107
+ });
35108
+ if (historyError) {
35109
+ console.error(`Failed to record history for ${result.keyword}:`, historyError);
35075
35110
  }
35076
35111
  }
35077
35112
  /**
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
35113
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
35081
35114
  */
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();
35115
+ async getHistory(keywordId, days = 30) {
35116
+ const { data, error } = await this.supabase.from("keyword_position_history").select("position, url, checked_at").eq("keyword_id", keywordId).gte("checked_at", new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString()).order("checked_at", { ascending: false });
35084
35117
  if (error || !data) {
35085
35118
  return [];
35086
35119
  }
35087
- return [{
35088
- position: data.current_position,
35089
- url: data.target_url,
35120
+ return data.map((row) => ({
35121
+ position: row.position,
35122
+ url: row.url,
35090
35123
  serpFeatures: [],
35091
- recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
35092
- }];
35124
+ recordedAt: new Date(row.checked_at)
35125
+ }));
35093
35126
  }
35094
35127
  /**
35095
35128
  * Get keyword trends for a project
@@ -35594,14 +35627,17 @@ var SUPPORTED_FRAMEWORKS = [
35594
35627
  injectGSCVerification,
35595
35628
  interpolatePrompt,
35596
35629
  isGitRepo,
35630
+ keywordLLM,
35597
35631
  listFiles,
35598
35632
  loadAgent,
35599
35633
  loadAgentByName,
35600
35634
  mergePages,
35635
+ openAIKeywordLLM,
35601
35636
  optimizeForAI,
35602
35637
  optimizeReadme,
35603
35638
  parseGEOResponse,
35604
35639
  parseGSCResponse,
35640
+ parseJsonReply,
35605
35641
  parseSitemap,
35606
35642
  prioritizeKeywords,
35607
35643
  processWizardResponse,
package/dist/index.mjs CHANGED
@@ -16564,6 +16564,50 @@ async function extractSeedKeywords(url) {
16564
16564
  }
16565
16565
  }
16566
16566
 
16567
+ // src/keywords/llm.ts
16568
+ import OpenAI from "openai";
16569
+ function openAIKeywordLLM(apiKey, opts = {}) {
16570
+ const openai = new OpenAI({ apiKey });
16571
+ const openAIModel = opts.model;
16572
+ return {
16573
+ async complete(prompt, opts2 = {}) {
16574
+ const response = await openai.chat.completions.create({
16575
+ model: opts2.best ? "gpt-4o" : openAIModel ?? "gpt-4o-mini",
16576
+ messages: [
16577
+ ...opts2.system ? [{ role: "system", content: opts2.system }] : [],
16578
+ { role: "user", content: prompt }
16579
+ ],
16580
+ response_format: { type: "json_object" },
16581
+ temperature: opts2.temperature ?? 0.3,
16582
+ ...opts2.maxTokens ? { max_tokens: opts2.maxTokens } : {}
16583
+ });
16584
+ return response.choices[0]?.message?.content || "";
16585
+ },
16586
+ async embed(texts) {
16587
+ const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: texts });
16588
+ return response.data.map((item) => item.embedding);
16589
+ }
16590
+ };
16591
+ }
16592
+ function keywordLLM(options) {
16593
+ return options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey) : void 0);
16594
+ }
16595
+ function parseJsonReply(text) {
16596
+ const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
16597
+ const start = cleaned.indexOf("{");
16598
+ const end = cleaned.lastIndexOf("}");
16599
+ if (start === -1 || end <= start) return null;
16600
+ const candidate = cleaned.slice(start, end + 1);
16601
+ for (const attempt of [candidate, candidate.replace(/,\s*([}\]])/g, "$1")]) {
16602
+ try {
16603
+ const parsed = JSON.parse(attempt);
16604
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
16605
+ } catch {
16606
+ }
16607
+ }
16608
+ return null;
16609
+ }
16610
+
16567
16611
  // src/keywords/site-crawler.ts
16568
16612
  import * as cheerio40 from "cheerio";
16569
16613
  var EXCLUDED_PATHS = [
@@ -16773,7 +16817,11 @@ function extractKeyPhrases(crawlResult) {
16773
16817
  }
16774
16818
 
16775
16819
  // src/keywords/ai-summarizer.ts
16776
- import OpenAI from "openai";
16820
+ function summarizerLLM(options) {
16821
+ const llm = options.llm ?? (options.openaiApiKey ? openAIKeywordLLM(options.openaiApiKey, { model: options.model }) : void 0);
16822
+ if (!llm) throw new Error("An AI provider is required to summarize the site");
16823
+ return llm;
16824
+ }
16777
16825
  var SUMMARIZER_PROMPT = `You are an expert SEO analyst. Analyze the following website content and extract key information for keyword research.
16778
16826
 
16779
16827
  WEBSITE CONTENT:
@@ -16816,37 +16864,26 @@ IMPORTANT:
16816
16864
 
16817
16865
  Respond ONLY with the JSON object, no other text.`;
16818
16866
  async function summarizeSite(crawlResult, options) {
16819
- const openai = new OpenAI({ apiKey: options.openaiApiKey });
16820
- const model = options.model || "gpt-4o-mini";
16867
+ const llm = summarizerLLM(options);
16868
+ const maxContent = llm.maxContentChars ?? 15e3;
16821
16869
  let content = crawlResult.aggregatedContent;
16822
- if (content.length > 15e3) {
16823
- content = content.substring(0, 15e3) + "\n...[truncated]";
16870
+ if (content.length > maxContent) {
16871
+ content = content.substring(0, maxContent) + "\n...[truncated]";
16824
16872
  }
16825
16873
  const headings = crawlResult.uniqueHeadings.slice(0, 30).join("\n");
16826
16874
  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
16875
  console.log("\u{1F916} Analyzing site content with AI...");
16828
16876
  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
- ],
16877
+ const responseText = await llm.complete(prompt, {
16878
+ system: "You are an expert SEO analyst. Always respond with valid JSON only.",
16841
16879
  temperature: 0.3,
16842
- max_tokens: options.maxTokens || 2e3
16880
+ maxTokens: options.maxTokens || 2e3
16843
16881
  });
16844
- const responseText = response.choices[0]?.message?.content || "";
16845
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
16846
- if (!jsonMatch) {
16882
+ const parsed = parseJsonReply(responseText);
16883
+ if (!parsed) {
16847
16884
  throw new Error("No JSON found in response");
16848
16885
  }
16849
- const summary = JSON.parse(jsonMatch[0]);
16886
+ const summary = parsed;
16850
16887
  return {
16851
16888
  productName: summary.productName || "Unknown Product",
16852
16889
  productDescription: summary.productDescription || "No description available",
@@ -16908,7 +16945,7 @@ function createFallbackSummary(crawlResult) {
16908
16945
  };
16909
16946
  }
16910
16947
  async function enhanceSummaryWithCompetitors(summary, competitors, options) {
16911
- const openai = new OpenAI({ apiKey: options.openaiApiKey });
16948
+ const llm = summarizerLLM(options);
16912
16949
  const prompt = `Given this product summary:
16913
16950
  Product: ${summary.productName}
16914
16951
  Description: ${summary.productDescription}
@@ -16927,19 +16964,13 @@ Analyze the competitive positioning. If this product focuses on a niche segment
16927
16964
  "updatedUncertainties": ["uncertainty1", "uncertainty2"]
16928
16965
  }`;
16929
16966
  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
- ],
16967
+ const responseText = await llm.complete(prompt, {
16968
+ system: "Respond with valid JSON only.",
16936
16969
  temperature: 0.3,
16937
- max_tokens: 1e3
16970
+ maxTokens: 1e3
16938
16971
  });
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]);
16972
+ const analysis = parseJsonReply(responseText);
16973
+ if (analysis) {
16943
16974
  return {
16944
16975
  ...summary,
16945
16976
  competitivePositioning: analysis.competitivePositioning || "",
@@ -17026,7 +17057,6 @@ function generateUncertaintyQuestions(summary) {
17026
17057
  }
17027
17058
 
17028
17059
  // src/keywords/nlp-analysis.ts
17029
- import OpenAI2 from "openai";
17030
17060
  var STOP_WORDS2 = /* @__PURE__ */ new Set([
17031
17061
  "a",
17032
17062
  "an",
@@ -17311,19 +17341,18 @@ function createTermVectors(keywords, vocabulary) {
17311
17341
  return vector;
17312
17342
  });
17313
17343
  }
17344
+ async function embedBatch(embedder, batch) {
17345
+ if (typeof embedder === "function") return embedder(batch);
17346
+ const response = await embedder.embeddings.create({ model: "text-embedding-3-small", input: batch });
17347
+ return response.data.map((item) => item.embedding);
17348
+ }
17314
17349
  async function getEmbeddings(texts, openai) {
17315
17350
  const embeddings = [];
17316
17351
  const batchSize = 100;
17317
17352
  for (let i = 0; i < texts.length; i += batchSize) {
17318
17353
  const batch = texts.slice(i, i + batchSize);
17319
17354
  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
- }
17355
+ embeddings.push(...await embedBatch(openai, batch));
17327
17356
  } catch (error) {
17328
17357
  console.warn("Embedding API failed, using fallback BOW vectors");
17329
17358
  for (const text of batch) {
@@ -17478,12 +17507,12 @@ async function runNLPAnalysis(content, options = {}) {
17478
17507
  const entityPhrases = extractEntityPhrases(combinedText);
17479
17508
  let clusters = [];
17480
17509
  const keywordsToCluster = tfidfKeywords.slice(0, 50).map((k) => k.term);
17481
- if (options.openaiApiKey && keywordsToCluster.length > 0) {
17510
+ const embed = keywordLLM(options)?.embed;
17511
+ if (embed && keywordsToCluster.length > 0) {
17482
17512
  try {
17483
- const openai = new OpenAI2({ apiKey: options.openaiApiKey });
17484
17513
  clusters = await clusterKeywordsByEmbedding(
17485
17514
  keywordsToCluster,
17486
- openai,
17515
+ (texts) => embed(texts),
17487
17516
  options.numClusters
17488
17517
  );
17489
17518
  } catch (error) {
@@ -17826,9 +17855,13 @@ async function searchHackerNews(query, maxResults = 10) {
17826
17855
  }
17827
17856
 
17828
17857
  // src/keywords/llm-judge.ts
17829
- import OpenAI3 from "openai";
17858
+ function judgeLLM(options) {
17859
+ const llm = keywordLLM(options);
17860
+ if (!llm) throw new Error("An AI provider is required to judge tool ideas");
17861
+ return llm;
17862
+ }
17830
17863
  async function evaluateToolFeasibility(toolIdea, siteSummary, competitiveData, options) {
17831
- const openai = new OpenAI3({ apiKey: options.openaiApiKey });
17864
+ const llm = judgeLLM(options);
17832
17865
  const minFeasibility = options.minFeasibilityScore ?? 3;
17833
17866
  const minSpecificity = options.minSpecificityScore ?? 3;
17834
17867
  const minProductFit = options.minProductFitScore ?? 2;
@@ -17904,13 +17937,10 @@ Respond in JSON:
17904
17937
  "blockers": ["<issues that prevent building>"],
17905
17938
  "improvements": ["<suggestions to make it more specific/buildable>"]
17906
17939
  }`;
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 || "{}");
17940
+ const result = parseJsonReply(await llm.complete(prompt, { temperature: 0.3 })) ?? {};
17941
+ for (const key of ["feasibility", "specificity", "productFit", "marketOpportunity"]) {
17942
+ result[key] = Number(result[key]) || 0;
17943
+ }
17914
17944
  const overallScore = result.feasibility * 0.3 + result.specificity * 0.3 + result.productFit * 0.2 + result.marketOpportunity * 0.2;
17915
17945
  const passes = result.feasibility >= minFeasibility && result.specificity >= minSpecificity && result.productFit >= minProductFit;
17916
17946
  return {
@@ -17926,7 +17956,7 @@ Respond in JSON:
17926
17956
  };
17927
17957
  }
17928
17958
  async function enhanceToolIdea(toolIdea, siteSummary, competitiveData, options) {
17929
- const openai = new OpenAI3({ apiKey: options.openaiApiKey });
17959
+ const llm = judgeLLM(options);
17930
17960
  const feasibilityScore = await evaluateToolFeasibility(
17931
17961
  toolIdea,
17932
17962
  siteSummary,
@@ -17971,13 +18001,7 @@ ${competitiveData?.openSourceOptions.slice(0, 3).map((t) => `- OSS: ${t.name} ($
17971
18001
  "canUseExistingOSS": <true if an OSS library does most of the work>,
17972
18002
  "ossLibrary": "<name of OSS library to use, or null>"
17973
18003
  }`;
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 || "{}");
18004
+ const enhanced = parseJsonReply(await llm.complete(prompt, { temperature: 0.5, best: true })) ?? {};
17981
18005
  const priority = Math.round(
17982
18006
  feasibilityScore.overallScore * 20 + toolIdea.volume / 1e3 * 10 + (100 - toolIdea.difficulty) / 100 * 20
17983
18007
  );
@@ -18028,7 +18052,7 @@ async function evaluateAndEnhanceToolIdeas(toolIdeas, siteSummary, getCompetitiv
18028
18052
 
18029
18053
  // src/keywords/ai-keyword-engine.ts
18030
18054
  async function runAIKeywordResearch(options) {
18031
- const { url, tier, openaiApiKey, maxPages = 20 } = options;
18055
+ const { url, tier, maxPages = 20 } = options;
18032
18056
  console.log(`
18033
18057
  \u{1F52C} Starting AI Keyword Research (${tier} tier)...
18034
18058
  `);
@@ -18036,10 +18060,11 @@ async function runAIKeywordResearch(options) {
18036
18060
  if (tier === "free") {
18037
18061
  return runFreeKeywordResearch(crawlResult, options);
18038
18062
  } else {
18039
- if (!openaiApiKey) {
18040
- throw new Error("OpenAI API key required for paid tier keyword research");
18063
+ const llm = keywordLLM(options);
18064
+ if (!llm) {
18065
+ throw new Error("An AI provider (or an OpenAI API key) is required for paid tier keyword research");
18041
18066
  }
18042
- return runPaidKeywordResearch(crawlResult, options);
18067
+ return runPaidKeywordResearch(crawlResult, options, llm);
18043
18068
  }
18044
18069
  }
18045
18070
  async function runFreeKeywordResearch(crawlResult, options) {
@@ -18093,13 +18118,11 @@ async function runFreeKeywordResearch(crawlResult, options) {
18093
18118
  ]
18094
18119
  };
18095
18120
  }
18096
- async function runPaidKeywordResearch(crawlResult, options) {
18097
- const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
18121
+ async function runPaidKeywordResearch(crawlResult, options, llm) {
18122
+ const { userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
18098
18123
  const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false;
18099
18124
  console.log("\u{1F680} Running full AI analysis...");
18100
- let siteSummary = await summarizeSite(crawlResult, {
18101
- openaiApiKey
18102
- });
18125
+ let siteSummary = await summarizeSite(crawlResult, { llm });
18103
18126
  if (userContext) {
18104
18127
  siteSummary = applyUserContext(siteSummary, userContext);
18105
18128
  }
@@ -18108,7 +18131,7 @@ async function runPaidKeywordResearch(crawlResult, options) {
18108
18131
  }
18109
18132
  const documents = crawlResult.pages.map((p) => p.mainContent);
18110
18133
  const nlpAnalysis = await runNLPAnalysis(documents, {
18111
- openaiApiKey,
18134
+ llm,
18112
18135
  numClusters: 8,
18113
18136
  numTopics: 6
18114
18137
  });
@@ -18135,13 +18158,13 @@ async function runPaidKeywordResearch(crawlResult, options) {
18135
18158
  nlpAnalysis
18136
18159
  );
18137
18160
  let freeToolIdeas;
18138
- if (enableEnhancedToolIdeas && openaiApiKey) {
18161
+ if (enableEnhancedToolIdeas) {
18139
18162
  console.log("\u{1F52C} Running enhanced tool idea analysis with competitive research...");
18140
18163
  freeToolIdeas = await generateEnhancedFreeToolIdeas(
18141
18164
  opportunities,
18142
18165
  siteSummary,
18143
18166
  {
18144
- openaiApiKey,
18167
+ llm,
18145
18168
  ...competitiveSearchKeys
18146
18169
  }
18147
18170
  );
@@ -18239,7 +18262,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
18239
18262
  kw.volume >= 300 && // Slightly lower threshold
18240
18263
  (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
18264
  );
18242
- const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
18265
+ const basicIdeas = toolKeywords.slice(0, options.llm.maxToolIdeas ?? 15).map((kw) => ({
18243
18266
  keyword: kw.keyword,
18244
18267
  toolName: generateToolName(kw.keyword),
18245
18268
  toolDescription: generateToolDescription(kw.keyword, summary),
@@ -18269,7 +18292,7 @@ async function generateEnhancedFreeToolIdeas(opportunities, summary, options) {
18269
18292
  basicIdeas,
18270
18293
  summary,
18271
18294
  getCompetitiveData,
18272
- { openaiApiKey: options.openaiApiKey }
18295
+ { llm: options.llm }
18273
18296
  );
18274
18297
  return enhancedIdeas.map((enhanced) => ({
18275
18298
  keyword: enhanced.keyword,
@@ -18600,10 +18623,9 @@ async function startWizardSession(url, options = {}) {
18600
18623
  console.log("\u{1F50D} Analyzing your site...");
18601
18624
  const crawlResult = await crawlSite(url, { maxPages: 10 });
18602
18625
  let siteSummary;
18603
- if (options.openaiApiKey) {
18604
- siteSummary = await summarizeSite(crawlResult, {
18605
- openaiApiKey: options.openaiApiKey
18606
- });
18626
+ const llm = keywordLLM(options);
18627
+ if (llm) {
18628
+ siteSummary = await summarizeSite(crawlResult, { llm });
18607
18629
  } else {
18608
18630
  siteSummary = {
18609
18631
  productName: crawlResult.domain,
@@ -31517,24 +31539,32 @@ var RankTracker = class {
31517
31539
  }).eq("id", keywordId);
31518
31540
  if (updateError) {
31519
31541
  console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
31542
+ return;
31543
+ }
31544
+ const { error: historyError } = await this.supabase.from("keyword_position_history").insert({
31545
+ keyword_id: keywordId,
31546
+ position: result.position,
31547
+ url: result.url,
31548
+ checked_at: result.checkedAt.toISOString()
31549
+ });
31550
+ if (historyError) {
31551
+ console.error(`Failed to record history for ${result.keyword}:`, historyError);
31520
31552
  }
31521
31553
  }
31522
31554
  /**
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
31555
+ * Ranking history for a keyword: one entry per check in the last `days`, newest first.
31526
31556
  */
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();
31557
+ async getHistory(keywordId, days = 30) {
31558
+ 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
31559
  if (error || !data) {
31530
31560
  return [];
31531
31561
  }
31532
- return [{
31533
- position: data.current_position,
31534
- url: data.target_url,
31562
+ return data.map((row) => ({
31563
+ position: row.position,
31564
+ url: row.url,
31535
31565
  serpFeatures: [],
31536
- recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
31537
- }];
31566
+ recordedAt: new Date(row.checked_at)
31567
+ }));
31538
31568
  }
31539
31569
  /**
31540
31570
  * Get keyword trends for a project
@@ -32031,14 +32061,17 @@ export {
32031
32061
  injectGSCVerification,
32032
32062
  interpolatePrompt,
32033
32063
  isGitRepo,
32064
+ keywordLLM,
32034
32065
  listFiles,
32035
32066
  loadAgent,
32036
32067
  loadAgentByName,
32037
32068
  mergePages,
32069
+ openAIKeywordLLM,
32038
32070
  optimizeForAI,
32039
32071
  optimizeReadme,
32040
32072
  parseGEOResponse,
32041
32073
  parseGSCResponse,
32074
+ parseJsonReply,
32042
32075
  parseSitemap,
32043
32076
  prioritizeKeywords,
32044
32077
  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.24",
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",