@rankcli/agent-runtime 0.0.1

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.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * AI-Powered Keyword Research Engine
3
+ *
4
+ * Tiered access:
5
+ * - FREE: Basic keyword extraction + score teaser
6
+ * - PAID: Full NLP, clustering, embeddings, competitive analysis
7
+ *
8
+ * Based on Bhanu's "Engineering as Marketing" strategy:
9
+ * 1. Find low KD keywords (< 10)
10
+ * 2. High volume (1000+)
11
+ * 3. Relevant to product
12
+ * 4. Easy to build free tools for
13
+ */
14
+
15
+ import OpenAI from 'openai';
16
+ import { crawlSite, extractKeyPhrases, type SiteCrawlResult } from './site-crawler.js';
17
+ import { summarizeSite, generateUncertaintyQuestions, type SiteSummary } from './ai-summarizer.js';
18
+ import {
19
+ runNLPAnalysis,
20
+ calculateTFIDF,
21
+ extractNgrams,
22
+ tokenize,
23
+ type NLPAnalysisResult,
24
+ } from './nlp-analysis.js';
25
+ import { getExpandedSuggestions, enrichKeywordsWithEstimates } from './sources/autocomplete.js';
26
+ import { getAllFreeKeywordIdeas, applyEstimates } from './sources/free-sources.js';
27
+ import type { KeywordData, SiteProfile } from './types.js';
28
+ import { prioritizeKeywords } from './prioritizer.js';
29
+ import {
30
+ searchCompetitors,
31
+ searchFormatConverters,
32
+ searchHackerNews,
33
+ type CompetitiveSearchResult,
34
+ type CompetitorTool,
35
+ type CompetitiveSearchOptions,
36
+ } from './sources/competitive-search.js';
37
+ import {
38
+ evaluateAndEnhanceToolIdeas,
39
+ type EnhancedToolIdea,
40
+ type LLMJudgeOptions,
41
+ } from './llm-judge.js';
42
+
43
+ export type PlanTier = 'free' | 'solo' | 'pro' | 'agency';
44
+
45
+ export interface AIKeywordResearchOptions {
46
+ url: string;
47
+ tier: PlanTier;
48
+ openaiApiKey?: string;
49
+ maxPages?: number;
50
+ userContext?: {
51
+ productDescription?: string;
52
+ targetAudience?: string;
53
+ competitors?: string[];
54
+ mainProblem?: string;
55
+ differentiator?: string;
56
+ };
57
+ wizardResponses?: Array<{ id: string; answer: string }>;
58
+ ciMode?: boolean;
59
+ /** API keys for competitive search (optional but recommended) */
60
+ competitiveSearchKeys?: {
61
+ braveApiKey?: string;
62
+ serperApiKey?: string;
63
+ githubToken?: string;
64
+ };
65
+ /** Enable enhanced tool ideas with LLM judge (default: true for paid tiers) */
66
+ enableEnhancedToolIdeas?: boolean;
67
+ }
68
+
69
+ export interface FreeKeywordResult {
70
+ tier: 'free';
71
+ /** Basic keyword suggestions (limited) */
72
+ keywords: Array<{
73
+ keyword: string;
74
+ estimatedVolume: 'low' | 'medium' | 'high';
75
+ estimatedDifficulty: 'low' | 'medium' | 'high';
76
+ relevance: number;
77
+ }>;
78
+ /** Overall keyword opportunity score (teaser) */
79
+ opportunityScore: number;
80
+ /** Teaser message */
81
+ teaserMessage: string;
82
+ /** What they'd get with upgrade */
83
+ upgradeFeatures: string[];
84
+ /** Basic site understanding */
85
+ siteCategory: string;
86
+ /** Detected keywords count */
87
+ totalKeywordsFound: number;
88
+ }
89
+
90
+ export interface PaidKeywordResult {
91
+ tier: PlanTier;
92
+ /** Site summary from AI */
93
+ siteSummary: SiteSummary;
94
+ /** Full keyword opportunities */
95
+ opportunities: KeywordOpportunity[];
96
+ /** NLP analysis results */
97
+ nlpAnalysis: NLPAnalysisResult;
98
+ /** Keyword clusters */
99
+ clusters: Array<{
100
+ name: string;
101
+ keywords: string[];
102
+ totalVolume: number;
103
+ avgDifficulty: number;
104
+ }>;
105
+ /** Recommended free tools to build */
106
+ freeToolIdeas: FreeToolIdea[];
107
+ /** Competitive positioning */
108
+ competitiveInsights?: CompetitiveInsight[];
109
+ /** Uncertainty assessment */
110
+ uncertainty: UncertaintyAssessment;
111
+ /** Prioritized recommendations */
112
+ recommendations: KeywordRecommendation[];
113
+ }
114
+
115
+ export interface KeywordOpportunity {
116
+ keyword: string;
117
+ volume: number;
118
+ difficulty: number;
119
+ relevance: number;
120
+ opportunityScore: number;
121
+ intent: 'informational' | 'commercial' | 'transactional' | 'navigational';
122
+ suggestedContentType: 'free-tool' | 'landing-page' | 'blog-post' | 'comparison' | 'guide';
123
+ suggestedCTA: string;
124
+ rationale: string;
125
+ cluster?: string;
126
+ }
127
+
128
+ export interface FreeToolIdea {
129
+ keyword: string;
130
+ toolName: string;
131
+ toolDescription: string;
132
+ estimatedEffort: 'low' | 'medium' | 'high';
133
+ ctaConnection: string;
134
+ volume: number;
135
+ difficulty: number;
136
+ priority: number;
137
+ /** Enhanced fields from LLM judge */
138
+ inputFormat?: string;
139
+ outputFormat?: string;
140
+ productTieIn?: string;
141
+ ctaText?: string;
142
+ deepLinkPattern?: string;
143
+ competitors?: {
144
+ hasFreeAlternative: boolean;
145
+ topCompetitor?: string;
146
+ ourAdvantage: string;
147
+ };
148
+ implementationHints?: {
149
+ suggestedLibraries: string[];
150
+ estimatedComplexity: 'trivial' | 'simple' | 'moderate' | 'complex';
151
+ canUseExistingOSS: boolean;
152
+ ossLibrary?: string;
153
+ };
154
+ feasibilityScore?: {
155
+ feasibility: number;
156
+ specificity: number;
157
+ productFit: number;
158
+ marketOpportunity: number;
159
+ overallScore: number;
160
+ passes: boolean;
161
+ reasoning: string;
162
+ };
163
+ }
164
+
165
+ export interface CompetitiveInsight {
166
+ competitor: string;
167
+ theirFocus: 'broad' | 'niche';
168
+ ourAdvantage?: string;
169
+ keywordsTheyRankFor: string[];
170
+ keywordsWeCanTarget: string[];
171
+ }
172
+
173
+ export interface UncertaintyAssessment {
174
+ overallConfidence: number;
175
+ siteUnderstanding: number;
176
+ audienceClarity: number;
177
+ competitiveClarity: number;
178
+ canAutomate: boolean;
179
+ questionsToAsk: Array<{
180
+ id: string;
181
+ question: string;
182
+ impact: number;
183
+ options?: string[];
184
+ }>;
185
+ explanation: string;
186
+ }
187
+
188
+ export interface KeywordRecommendation {
189
+ priority: number;
190
+ keyword: string;
191
+ action: 'build-tool' | 'create-page' | 'write-content' | 'skip' | 'needs-input';
192
+ effort: 'low' | 'medium' | 'high';
193
+ impact: 'low' | 'medium' | 'high';
194
+ rationale: string;
195
+ }
196
+
197
+ /**
198
+ * Run keyword research based on tier
199
+ */
200
+ export async function runAIKeywordResearch(
201
+ options: AIKeywordResearchOptions
202
+ ): Promise<FreeKeywordResult | PaidKeywordResult> {
203
+ const { url, tier, openaiApiKey, maxPages = 20 } = options;
204
+
205
+ console.log(`\n🔬 Starting AI Keyword Research (${tier} tier)...\n`);
206
+
207
+ // Step 1: Crawl the site
208
+ const crawlResult = await crawlSite(url, { maxPages });
209
+
210
+ // Route based on tier
211
+ if (tier === 'free') {
212
+ return runFreeKeywordResearch(crawlResult, options);
213
+ } else {
214
+ if (!openaiApiKey) {
215
+ throw new Error('OpenAI API key required for paid tier keyword research');
216
+ }
217
+ return runPaidKeywordResearch(crawlResult, options);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * FREE TIER: Basic keyword extraction with teaser
223
+ */
224
+ async function runFreeKeywordResearch(
225
+ crawlResult: SiteCrawlResult,
226
+ options: AIKeywordResearchOptions
227
+ ): Promise<FreeKeywordResult> {
228
+ console.log('📊 Running free tier analysis...');
229
+
230
+ // Basic TF-IDF analysis (no AI)
231
+ const documents = crawlResult.pages.map((p) => p.mainContent);
232
+ const tfidfResults = calculateTFIDF(documents);
233
+
234
+ // Extract key phrases
235
+ const keyPhrases = extractKeyPhrases(crawlResult);
236
+
237
+ // Get basic autocomplete suggestions for top terms
238
+ const topTerms = tfidfResults.slice(0, 5).map((r) => r.term);
239
+ let autocompleteKeywords: KeywordData[] = [];
240
+
241
+ for (const term of topTerms) {
242
+ try {
243
+ const suggestions = await getExpandedSuggestions(term, {});
244
+ autocompleteKeywords.push(...suggestions.slice(0, 5));
245
+ } catch {
246
+ // Ignore errors
247
+ }
248
+ }
249
+
250
+ // Apply volume estimates
251
+ autocompleteKeywords = applyEstimates(enrichKeywordsWithEstimates(autocompleteKeywords));
252
+
253
+ // Create limited results (teaser)
254
+ const limitedKeywords = autocompleteKeywords.slice(0, 10).map((kw) => {
255
+ const estimatedVolume: 'low' | 'medium' | 'high' = kw.searchVolume > 5000 ? 'high' : kw.searchVolume > 1000 ? 'medium' : 'low';
256
+ const estimatedDifficulty: 'low' | 'medium' | 'high' = kw.keywordDifficulty > 50 ? 'high' : kw.keywordDifficulty > 20 ? 'medium' : 'low';
257
+ return {
258
+ keyword: kw.keyword,
259
+ estimatedVolume,
260
+ estimatedDifficulty,
261
+ relevance: 0.5 + Math.random() * 0.5, // Simplified relevance
262
+ };
263
+ });
264
+
265
+ // Calculate opportunity score
266
+ const opportunityScore = Math.min(
267
+ 100,
268
+ Math.round(
269
+ (limitedKeywords.filter((k) => k.estimatedDifficulty === 'low').length / limitedKeywords.length) * 50 +
270
+ (limitedKeywords.filter((k) => k.estimatedVolume !== 'low').length / limitedKeywords.length) * 50
271
+ )
272
+ );
273
+
274
+ // Detect site category
275
+ const siteCategory = detectSiteCategory(crawlResult, tfidfResults);
276
+
277
+ return {
278
+ tier: 'free',
279
+ keywords: limitedKeywords,
280
+ opportunityScore,
281
+ totalKeywordsFound: autocompleteKeywords.length + keyPhrases.length,
282
+ siteCategory,
283
+ teaserMessage: `We found ${autocompleteKeywords.length + keyPhrases.length} potential keywords for ${crawlResult.domain}. Upgrade to see full analysis with AI-powered insights, keyword clustering, and competitive analysis.`,
284
+ upgradeFeatures: [
285
+ '🧠 AI-powered site analysis',
286
+ '📊 Full keyword data (volume, difficulty, trends)',
287
+ '🎯 Semantic keyword clustering',
288
+ '🛠️ Free tool ideas with CTA suggestions',
289
+ '🏆 Competitive positioning analysis',
290
+ '📈 Prioritized action recommendations',
291
+ '🤖 Uncertainty wizard for better results',
292
+ ],
293
+ };
294
+ }
295
+
296
+ /**
297
+ * PAID TIER: Full AI-powered analysis
298
+ */
299
+ async function runPaidKeywordResearch(
300
+ crawlResult: SiteCrawlResult,
301
+ options: AIKeywordResearchOptions
302
+ ): Promise<PaidKeywordResult> {
303
+ const { openaiApiKey, userContext, wizardResponses, ciMode, competitiveSearchKeys } = options;
304
+ const enableEnhancedToolIdeas = options.enableEnhancedToolIdeas !== false; // Default true
305
+
306
+ console.log('🚀 Running full AI analysis...');
307
+
308
+ // Step 1: AI Site Summary
309
+ let siteSummary = await summarizeSite(crawlResult, {
310
+ openaiApiKey: openaiApiKey!,
311
+ });
312
+
313
+ // Apply user context to improve summary
314
+ if (userContext) {
315
+ siteSummary = applyUserContext(siteSummary, userContext);
316
+ }
317
+
318
+ // Apply wizard responses
319
+ if (wizardResponses) {
320
+ siteSummary = applyWizardResponses(siteSummary, wizardResponses);
321
+ }
322
+
323
+ // Step 2: Full NLP Analysis
324
+ const documents = crawlResult.pages.map((p) => p.mainContent);
325
+ const nlpAnalysis = await runNLPAnalysis(documents, {
326
+ openaiApiKey,
327
+ numClusters: 8,
328
+ numTopics: 6,
329
+ });
330
+
331
+ // Step 3: Generate seed keywords from multiple sources
332
+ const seedKeywords = generateSeedKeywords(siteSummary, nlpAnalysis, crawlResult);
333
+
334
+ // Step 4: Expand keywords with autocomplete and volume data
335
+ console.log('🔍 Expanding keyword opportunities...');
336
+ let allKeywords: KeywordData[] = [];
337
+
338
+ for (const seed of seedKeywords.slice(0, 15)) {
339
+ try {
340
+ const suggestions = await getExpandedSuggestions(seed, {});
341
+ allKeywords.push(...suggestions.slice(0, 20));
342
+
343
+ const freeIdeas = await getAllFreeKeywordIdeas(seed, {
344
+ includeQuestions: true,
345
+ includeModifiers: true,
346
+ });
347
+ allKeywords.push(...freeIdeas);
348
+ } catch {
349
+ // Continue with other seeds
350
+ }
351
+ }
352
+
353
+ // Enrich with volume estimates
354
+ allKeywords = applyEstimates(enrichKeywordsWithEstimates(allKeywords));
355
+
356
+ // Deduplicate
357
+ const uniqueKeywords = deduplicateKeywords(allKeywords);
358
+
359
+ // Step 5: Score and prioritize
360
+ const opportunities = scoreKeywordOpportunities(
361
+ uniqueKeywords,
362
+ siteSummary,
363
+ nlpAnalysis
364
+ );
365
+
366
+ // Step 6: Generate free tool ideas with competitive research and LLM judge
367
+ let freeToolIdeas: FreeToolIdea[];
368
+
369
+ if (enableEnhancedToolIdeas && openaiApiKey) {
370
+ console.log('🔬 Running enhanced tool idea analysis with competitive research...');
371
+ freeToolIdeas = await generateEnhancedFreeToolIdeas(
372
+ opportunities,
373
+ siteSummary,
374
+ {
375
+ openaiApiKey,
376
+ ...competitiveSearchKeys,
377
+ }
378
+ );
379
+ } else {
380
+ // Fallback to basic tool idea generation
381
+ freeToolIdeas = generateBasicFreeToolIdeas(opportunities, siteSummary);
382
+ }
383
+
384
+ // Step 7: Create keyword clusters
385
+ const clusters = createKeywordClusters(opportunities, nlpAnalysis);
386
+
387
+ // Step 8: Assess uncertainty
388
+ const uncertainty = assessUncertainty(siteSummary, opportunities, ciMode);
389
+
390
+ // Step 9: Generate recommendations
391
+ const recommendations = generateRecommendations(
392
+ opportunities,
393
+ freeToolIdeas,
394
+ uncertainty
395
+ );
396
+
397
+ return {
398
+ tier: options.tier,
399
+ siteSummary,
400
+ opportunities: opportunities.slice(0, 100),
401
+ nlpAnalysis,
402
+ clusters,
403
+ freeToolIdeas: freeToolIdeas.slice(0, 20),
404
+ uncertainty,
405
+ recommendations: recommendations.slice(0, 15),
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Generate seed keywords from AI analysis
411
+ */
412
+ function generateSeedKeywords(
413
+ summary: SiteSummary,
414
+ nlp: NLPAnalysisResult,
415
+ crawl: SiteCrawlResult
416
+ ): string[] {
417
+ const seeds = new Set<string>();
418
+
419
+ // From AI summary
420
+ summary.suggestedSeedKeywords.forEach((k) => seeds.add(k.toLowerCase()));
421
+
422
+ // From key features
423
+ summary.keyFeatures.forEach((f) => {
424
+ const words = tokenize(f);
425
+ if (words.length <= 3) {
426
+ seeds.add(words.join(' '));
427
+ }
428
+ });
429
+
430
+ // From problems solved
431
+ summary.problemsSolved.forEach((p) => {
432
+ const words = tokenize(p);
433
+ if (words.length <= 4) {
434
+ seeds.add(words.join(' '));
435
+ }
436
+ });
437
+
438
+ // From TF-IDF
439
+ nlp.tfidfKeywords.slice(0, 15).forEach((k) => seeds.add(k.term));
440
+
441
+ // From bigrams
442
+ nlp.ngrams.bigrams.slice(0, 10).forEach((g) => seeds.add(g.phrase));
443
+
444
+ // From entity phrases
445
+ nlp.entityPhrases.slice(0, 10).forEach((e) => seeds.add(e.toLowerCase()));
446
+
447
+ // Product name variations
448
+ const productName = summary.productName.toLowerCase();
449
+ seeds.add(productName);
450
+ seeds.add(`${productName} alternative`);
451
+ seeds.add(`${productName} vs`);
452
+ seeds.add(`best ${summary.industry.toLowerCase()}`);
453
+
454
+ // Industry + modifiers
455
+ const industry = summary.industry.toLowerCase();
456
+ seeds.add(`${industry} tool`);
457
+ seeds.add(`${industry} software`);
458
+ seeds.add(`free ${industry}`);
459
+ seeds.add(`${industry} generator`);
460
+ seeds.add(`${industry} ai`);
461
+
462
+ return Array.from(seeds).filter((s) => s.length > 2 && s.length < 50);
463
+ }
464
+
465
+ /**
466
+ * Score keyword opportunities
467
+ */
468
+ function scoreKeywordOpportunities(
469
+ keywords: KeywordData[],
470
+ summary: SiteSummary,
471
+ nlp: NLPAnalysisResult
472
+ ): KeywordOpportunity[] {
473
+ const opportunities: KeywordOpportunity[] = [];
474
+
475
+ // Get relevance terms from summary
476
+ const relevanceTerms = new Set([
477
+ ...summary.suggestedSeedKeywords.map((k) => k.toLowerCase()),
478
+ ...summary.keyFeatures.flatMap(tokenize),
479
+ ...summary.problemsSolved.flatMap(tokenize),
480
+ summary.industry.toLowerCase(),
481
+ summary.productName.toLowerCase(),
482
+ ]);
483
+
484
+ for (const kw of keywords) {
485
+ // Calculate relevance
486
+ const kwTokens = tokenize(kw.keyword);
487
+ const relevance = kwTokens.filter((t) => relevanceTerms.has(t)).length / Math.max(kwTokens.length, 1);
488
+
489
+ // Skip very low relevance
490
+ if (relevance < 0.1 && kw.searchVolume < 500) continue;
491
+
492
+ // Calculate opportunity score
493
+ // Higher volume + lower difficulty + higher relevance = better opportunity
494
+ const volumeScore = Math.min(1, Math.log10(kw.searchVolume + 1) / 4);
495
+ const difficultyScore = 1 - kw.keywordDifficulty / 100;
496
+ const opportunityScore = Math.round(
497
+ (volumeScore * 0.3 + difficultyScore * 0.4 + relevance * 0.3) * 100
498
+ );
499
+
500
+ // Determine intent
501
+ const intent = determineIntent(kw.keyword);
502
+
503
+ // Suggest content type
504
+ const suggestedContentType = suggestContentType(kw.keyword, kw.keywordDifficulty, intent);
505
+
506
+ // Generate CTA
507
+ const suggestedCTA = generateCTA(kw.keyword, summary);
508
+
509
+ // Generate rationale
510
+ const rationale = generateRationale(kw, relevance, summary);
511
+
512
+ opportunities.push({
513
+ keyword: kw.keyword,
514
+ volume: kw.searchVolume,
515
+ difficulty: kw.keywordDifficulty,
516
+ relevance: Math.round(relevance * 100),
517
+ opportunityScore,
518
+ intent,
519
+ suggestedContentType,
520
+ suggestedCTA,
521
+ rationale,
522
+ });
523
+ }
524
+
525
+ // Sort by opportunity score
526
+ return opportunities.sort((a, b) => b.opportunityScore - a.opportunityScore);
527
+ }
528
+
529
+ /**
530
+ * Generate free tool ideas with competitive research and LLM judge (enhanced version)
531
+ */
532
+ async function generateEnhancedFreeToolIdeas(
533
+ opportunities: KeywordOpportunity[],
534
+ summary: SiteSummary,
535
+ options: {
536
+ openaiApiKey: string;
537
+ braveApiKey?: string;
538
+ serperApiKey?: string;
539
+ githubToken?: string;
540
+ }
541
+ ): Promise<FreeToolIdea[]> {
542
+ // Filter for tool-worthy keywords (low difficulty, decent volume)
543
+ const toolKeywords = opportunities.filter(
544
+ (kw) =>
545
+ kw.difficulty < 30 && // Slightly more lenient
546
+ kw.volume >= 300 && // Slightly lower threshold
547
+ (kw.keyword.includes('generator') ||
548
+ kw.keyword.includes('tool') ||
549
+ kw.keyword.includes('maker') ||
550
+ kw.keyword.includes('builder') ||
551
+ kw.keyword.includes('converter') ||
552
+ kw.keyword.includes('calculator') ||
553
+ kw.keyword.includes('checker') ||
554
+ kw.keyword.includes('validator') ||
555
+ kw.keyword.includes('formatter') ||
556
+ kw.keyword.includes('free') ||
557
+ kw.intent === 'informational')
558
+ );
559
+
560
+ // Prepare basic tool ideas for enhancement
561
+ const basicIdeas = toolKeywords.slice(0, 15).map((kw) => ({
562
+ keyword: kw.keyword,
563
+ toolName: generateToolName(kw.keyword),
564
+ toolDescription: generateToolDescription(kw.keyword, summary),
565
+ volume: kw.volume,
566
+ difficulty: kw.difficulty,
567
+ }));
568
+
569
+ // Create competitive search function with caching
570
+ const competitiveCache = new Map<string, CompetitiveSearchResult>();
571
+
572
+ const getCompetitiveData = async (keyword: string): Promise<CompetitiveSearchResult | null> => {
573
+ // Check cache
574
+ if (competitiveCache.has(keyword)) {
575
+ return competitiveCache.get(keyword)!;
576
+ }
577
+
578
+ try {
579
+ const result = await searchCompetitors(keyword, {
580
+ braveApiKey: options.braveApiKey,
581
+ serperApiKey: options.serperApiKey,
582
+ githubToken: options.githubToken,
583
+ maxResults: 5,
584
+ });
585
+ competitiveCache.set(keyword, result);
586
+ return result;
587
+ } catch (error) {
588
+ console.warn(`Competitive search failed for "${keyword}":`, error);
589
+ return null;
590
+ }
591
+ };
592
+
593
+ // Evaluate and enhance tool ideas using LLM judge
594
+ const enhancedIdeas = await evaluateAndEnhanceToolIdeas(
595
+ basicIdeas,
596
+ summary,
597
+ getCompetitiveData,
598
+ { openaiApiKey: options.openaiApiKey }
599
+ );
600
+
601
+ // Convert enhanced ideas to FreeToolIdea format
602
+ return enhancedIdeas.map((enhanced) => ({
603
+ keyword: enhanced.keyword,
604
+ toolName: enhanced.toolName,
605
+ toolDescription: enhanced.description,
606
+ estimatedEffort: complexityToEffort(enhanced.implementationHints.estimatedComplexity),
607
+ ctaConnection: enhanced.ctaText,
608
+ volume: enhanced.volume,
609
+ difficulty: enhanced.difficulty,
610
+ priority: enhanced.priority,
611
+ inputFormat: enhanced.inputFormat,
612
+ outputFormat: enhanced.outputFormat,
613
+ productTieIn: enhanced.productTieIn,
614
+ ctaText: enhanced.ctaText,
615
+ deepLinkPattern: enhanced.deepLinkPattern,
616
+ competitors: enhanced.competitors,
617
+ implementationHints: enhanced.implementationHints,
618
+ feasibilityScore: {
619
+ feasibility: enhanced.feasibilityScore.feasibility,
620
+ specificity: enhanced.feasibilityScore.specificity,
621
+ productFit: enhanced.feasibilityScore.productFit,
622
+ marketOpportunity: enhanced.feasibilityScore.marketOpportunity,
623
+ overallScore: enhanced.feasibilityScore.overallScore,
624
+ passes: enhanced.feasibilityScore.passes,
625
+ reasoning: enhanced.feasibilityScore.reasoning,
626
+ },
627
+ }));
628
+ }
629
+
630
+ /**
631
+ * Convert complexity to effort level
632
+ */
633
+ function complexityToEffort(complexity: 'trivial' | 'simple' | 'moderate' | 'complex'): 'low' | 'medium' | 'high' {
634
+ switch (complexity) {
635
+ case 'trivial':
636
+ case 'simple':
637
+ return 'low';
638
+ case 'moderate':
639
+ return 'medium';
640
+ case 'complex':
641
+ return 'high';
642
+ default:
643
+ return 'medium';
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Generate free tool ideas (basic version - fallback)
649
+ */
650
+ function generateBasicFreeToolIdeas(
651
+ opportunities: KeywordOpportunity[],
652
+ summary: SiteSummary
653
+ ): FreeToolIdea[] {
654
+ const toolIdeas: FreeToolIdea[] = [];
655
+
656
+ // Filter for tool-worthy keywords (low difficulty, decent volume)
657
+ const toolKeywords = opportunities.filter(
658
+ (kw) =>
659
+ kw.difficulty < 20 &&
660
+ kw.volume >= 500 &&
661
+ (kw.keyword.includes('generator') ||
662
+ kw.keyword.includes('tool') ||
663
+ kw.keyword.includes('maker') ||
664
+ kw.keyword.includes('builder') ||
665
+ kw.keyword.includes('converter') ||
666
+ kw.keyword.includes('calculator') ||
667
+ kw.keyword.includes('checker') ||
668
+ kw.keyword.includes('free') ||
669
+ kw.intent === 'informational')
670
+ );
671
+
672
+ for (const kw of toolKeywords.slice(0, 30)) {
673
+ const toolName = generateToolName(kw.keyword);
674
+ const toolDescription = generateToolDescription(kw.keyword, summary);
675
+ const effort = estimateToolEffort(kw.keyword);
676
+ const ctaConnection = generateToolCTA(kw.keyword, summary);
677
+
678
+ // Calculate priority
679
+ const priority = Math.round(
680
+ (kw.volume / 10000) * 30 + // Volume contribution
681
+ ((100 - kw.difficulty) / 100) * 40 + // Difficulty contribution
682
+ (kw.relevance / 100) * 30 // Relevance contribution
683
+ );
684
+
685
+ toolIdeas.push({
686
+ keyword: kw.keyword,
687
+ toolName,
688
+ toolDescription,
689
+ estimatedEffort: effort,
690
+ ctaConnection,
691
+ volume: kw.volume,
692
+ difficulty: kw.difficulty,
693
+ priority,
694
+ });
695
+ }
696
+
697
+ return toolIdeas.sort((a, b) => b.priority - a.priority);
698
+ }
699
+
700
+ /**
701
+ * Create keyword clusters
702
+ */
703
+ function createKeywordClusters(
704
+ opportunities: KeywordOpportunity[],
705
+ nlp: NLPAnalysisResult
706
+ ): Array<{
707
+ name: string;
708
+ keywords: string[];
709
+ totalVolume: number;
710
+ avgDifficulty: number;
711
+ }> {
712
+ // Use NLP clusters if available
713
+ if (nlp.clusters.length > 0) {
714
+ return nlp.clusters.map((cluster) => {
715
+ const clusterOpps = opportunities.filter((o) =>
716
+ cluster.keywords.some(
717
+ (k) => o.keyword.includes(k) || k.includes(o.keyword.split(' ')[0])
718
+ )
719
+ );
720
+
721
+ return {
722
+ name: cluster.name,
723
+ keywords: clusterOpps.map((o) => o.keyword),
724
+ totalVolume: clusterOpps.reduce((sum, o) => sum + o.volume, 0),
725
+ avgDifficulty:
726
+ clusterOpps.length > 0
727
+ ? Math.round(clusterOpps.reduce((sum, o) => sum + o.difficulty, 0) / clusterOpps.length)
728
+ : 0,
729
+ };
730
+ });
731
+ }
732
+
733
+ // Fallback: group by common terms
734
+ const clusters: Map<string, KeywordOpportunity[]> = new Map();
735
+
736
+ for (const opp of opportunities) {
737
+ const mainTerm = opp.keyword.split(' ')[0];
738
+ if (!clusters.has(mainTerm)) {
739
+ clusters.set(mainTerm, []);
740
+ }
741
+ clusters.get(mainTerm)!.push(opp);
742
+ }
743
+
744
+ return Array.from(clusters.entries())
745
+ .filter(([_, opps]) => opps.length >= 2)
746
+ .map(([name, opps]) => ({
747
+ name,
748
+ keywords: opps.map((o) => o.keyword),
749
+ totalVolume: opps.reduce((sum, o) => sum + o.volume, 0),
750
+ avgDifficulty: Math.round(opps.reduce((sum, o) => sum + o.difficulty, 0) / opps.length),
751
+ }))
752
+ .sort((a, b) => b.totalVolume - a.totalVolume)
753
+ .slice(0, 10);
754
+ }
755
+
756
+ /**
757
+ * Assess uncertainty for CI mode
758
+ */
759
+ function assessUncertainty(
760
+ summary: SiteSummary,
761
+ opportunities: KeywordOpportunity[],
762
+ ciMode?: boolean
763
+ ): UncertaintyAssessment {
764
+ const siteUnderstanding = summary.confidence;
765
+ const audienceClarity = summary.targetAudience.includes('Unknown') ? 0.3 : 0.8;
766
+ const competitiveClarity = 0.5; // Would be higher with competitor analysis
767
+
768
+ // Average relevance of top keywords
769
+ const topOpps = opportunities.slice(0, 20);
770
+ const avgRelevance = topOpps.length > 0
771
+ ? topOpps.reduce((sum, o) => sum + o.relevance, 0) / topOpps.length / 100
772
+ : 0.5;
773
+
774
+ const overallConfidence =
775
+ (siteUnderstanding + audienceClarity + competitiveClarity + avgRelevance) / 4;
776
+
777
+ // In CI mode, require higher confidence
778
+ const threshold = ciMode ? 0.7 : 0.5;
779
+ const canAutomate = overallConfidence >= threshold;
780
+
781
+ // Generate questions to reduce uncertainty
782
+ const questions = generateUncertaintyQuestions(summary);
783
+
784
+ let explanation: string;
785
+ if (canAutomate) {
786
+ explanation = `Confidence is ${Math.round(overallConfidence * 100)}%. Keyword recommendations can be automated.`;
787
+ } else {
788
+ explanation = `Confidence is only ${Math.round(overallConfidence * 100)}%. ${
789
+ ciMode
790
+ ? 'CI mode requires 70% confidence. Please run the keyword wizard to provide more context.'
791
+ : 'Consider running the keyword wizard for better results.'
792
+ }`;
793
+ }
794
+
795
+ return {
796
+ overallConfidence,
797
+ siteUnderstanding,
798
+ audienceClarity,
799
+ competitiveClarity,
800
+ canAutomate,
801
+ questionsToAsk: questions,
802
+ explanation,
803
+ };
804
+ }
805
+
806
+ /**
807
+ * Generate prioritized recommendations
808
+ */
809
+ function generateRecommendations(
810
+ opportunities: KeywordOpportunity[],
811
+ toolIdeas: FreeToolIdea[],
812
+ uncertainty: UncertaintyAssessment
813
+ ): KeywordRecommendation[] {
814
+ const recommendations: KeywordRecommendation[] = [];
815
+ let priority = 1;
816
+
817
+ // If uncertainty is high, first recommendation is to run wizard
818
+ if (!uncertainty.canAutomate) {
819
+ recommendations.push({
820
+ priority: priority++,
821
+ keyword: '',
822
+ action: 'needs-input',
823
+ effort: 'low',
824
+ impact: 'high',
825
+ rationale: `Run the keyword wizard to answer ${uncertainty.questionsToAsk.length} questions and improve recommendation accuracy.`,
826
+ });
827
+ }
828
+
829
+ // Add top free tool ideas
830
+ for (const tool of toolIdeas.slice(0, 5)) {
831
+ recommendations.push({
832
+ priority: priority++,
833
+ keyword: tool.keyword,
834
+ action: 'build-tool',
835
+ effort: tool.estimatedEffort,
836
+ impact: tool.volume > 2000 ? 'high' : tool.volume > 500 ? 'medium' : 'low',
837
+ rationale: `Build "${tool.toolName}": ${tool.volume} monthly searches, KD ${tool.difficulty}. ${tool.ctaConnection}`,
838
+ });
839
+ }
840
+
841
+ // Add top content opportunities
842
+ const contentOpps = opportunities
843
+ .filter((o) => o.suggestedContentType !== 'free-tool' && o.difficulty < 30)
844
+ .slice(0, 5);
845
+
846
+ for (const opp of contentOpps) {
847
+ recommendations.push({
848
+ priority: priority++,
849
+ keyword: opp.keyword,
850
+ action: opp.suggestedContentType === 'landing-page' ? 'create-page' : 'write-content',
851
+ effort: opp.suggestedContentType === 'guide' ? 'high' : 'medium',
852
+ impact: opp.volume > 2000 ? 'high' : opp.volume > 500 ? 'medium' : 'low',
853
+ rationale: opp.rationale,
854
+ });
855
+ }
856
+
857
+ return recommendations;
858
+ }
859
+
860
+ // Helper functions
861
+
862
+ function detectSiteCategory(
863
+ crawl: SiteCrawlResult,
864
+ tfidf: Array<{ term: string; tfidf: number }>
865
+ ): string {
866
+ const topTerms = tfidf.slice(0, 20).map((t) => t.term);
867
+ const text = topTerms.join(' ');
868
+
869
+ if (text.includes('saas') || text.includes('software') || text.includes('app')) {
870
+ return 'SaaS/Software';
871
+ }
872
+ if (text.includes('shop') || text.includes('buy') || text.includes('product')) {
873
+ return 'E-commerce';
874
+ }
875
+ if (text.includes('blog') || text.includes('article') || text.includes('news')) {
876
+ return 'Content/Media';
877
+ }
878
+ if (text.includes('agency') || text.includes('service') || text.includes('consult')) {
879
+ return 'Agency/Services';
880
+ }
881
+
882
+ return 'General';
883
+ }
884
+
885
+ function deduplicateKeywords(keywords: KeywordData[]): KeywordData[] {
886
+ const seen = new Map<string, KeywordData>();
887
+ for (const kw of keywords) {
888
+ const key = kw.keyword.toLowerCase().trim();
889
+ const existing = seen.get(key);
890
+ if (!existing || kw.searchVolume > existing.searchVolume) {
891
+ seen.set(key, kw);
892
+ }
893
+ }
894
+ return Array.from(seen.values());
895
+ }
896
+
897
+ function determineIntent(keyword: string): 'informational' | 'commercial' | 'transactional' | 'navigational' {
898
+ const kw = keyword.toLowerCase();
899
+ if (kw.includes('how to') || kw.includes('what is') || kw.includes('guide') || kw.includes('tutorial')) {
900
+ return 'informational';
901
+ }
902
+ if (kw.includes('buy') || kw.includes('price') || kw.includes('cost') || kw.includes('purchase')) {
903
+ return 'transactional';
904
+ }
905
+ if (kw.includes('best') || kw.includes('top') || kw.includes('review') || kw.includes('vs')) {
906
+ return 'commercial';
907
+ }
908
+ return 'informational';
909
+ }
910
+
911
+ function suggestContentType(
912
+ keyword: string,
913
+ difficulty: number,
914
+ intent: string
915
+ ): 'free-tool' | 'landing-page' | 'blog-post' | 'comparison' | 'guide' {
916
+ const kw = keyword.toLowerCase();
917
+
918
+ if (kw.includes('generator') || kw.includes('tool') || kw.includes('maker') || kw.includes('calculator')) {
919
+ return 'free-tool';
920
+ }
921
+ if (kw.includes('vs') || kw.includes('alternative') || kw.includes('comparison')) {
922
+ return 'comparison';
923
+ }
924
+ if (intent === 'commercial' && difficulty < 30) {
925
+ return 'landing-page';
926
+ }
927
+ if (kw.includes('guide') || kw.includes('how to') || kw.includes('tutorial')) {
928
+ return 'guide';
929
+ }
930
+ return 'blog-post';
931
+ }
932
+
933
+ function generateCTA(keyword: string, summary: SiteSummary): string {
934
+ const product = summary.productName;
935
+ const kw = keyword.toLowerCase();
936
+
937
+ if (kw.includes('generator') || kw.includes('tool')) {
938
+ return `Try ${product} for more advanced ${summary.industry.toLowerCase()} features →`;
939
+ }
940
+ if (kw.includes('free')) {
941
+ return `${product} offers a free tier with even more capabilities →`;
942
+ }
943
+ return `Discover how ${product} can help with ${kw.split(' ').slice(0, 3).join(' ')} →`;
944
+ }
945
+
946
+ function generateRationale(kw: KeywordData, relevance: number, summary: SiteSummary): string {
947
+ const parts: string[] = [];
948
+
949
+ if (kw.keywordDifficulty < 20) {
950
+ parts.push('Low competition - easy to rank');
951
+ } else if (kw.keywordDifficulty < 40) {
952
+ parts.push('Moderate competition');
953
+ }
954
+
955
+ if (kw.searchVolume > 5000) {
956
+ parts.push('High search volume');
957
+ } else if (kw.searchVolume > 1000) {
958
+ parts.push('Decent search volume');
959
+ }
960
+
961
+ if (relevance > 0.7) {
962
+ parts.push('Highly relevant to your product');
963
+ } else if (relevance > 0.4) {
964
+ parts.push('Related to your industry');
965
+ }
966
+
967
+ return parts.join('. ') + '.';
968
+ }
969
+
970
+ function generateToolName(keyword: string): string {
971
+ const kw = keyword.toLowerCase();
972
+ const words = kw.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1));
973
+
974
+ if (kw.includes('generator')) {
975
+ return words.join(' ');
976
+ }
977
+ if (kw.includes('checker') || kw.includes('validator')) {
978
+ return words.join(' ');
979
+ }
980
+
981
+ return `Free ${words.join(' ')} Tool`;
982
+ }
983
+
984
+ function generateToolDescription(keyword: string, summary: SiteSummary): string {
985
+ return `A free ${keyword} tool. Use it to ${keyword.replace('generator', 'generate').replace('checker', 'check')}. Try ${summary.productName} for more advanced features.`;
986
+ }
987
+
988
+ function estimateToolEffort(keyword: string): 'low' | 'medium' | 'high' {
989
+ const kw = keyword.toLowerCase();
990
+
991
+ if (kw.includes('calculator') || kw.includes('converter') || kw.includes('counter')) {
992
+ return 'low';
993
+ }
994
+ if (kw.includes('generator') || kw.includes('maker')) {
995
+ return 'medium';
996
+ }
997
+ if (kw.includes('analyzer') || kw.includes('checker') || kw.includes('builder')) {
998
+ return 'high';
999
+ }
1000
+
1001
+ return 'medium';
1002
+ }
1003
+
1004
+ function generateToolCTA(keyword: string, summary: SiteSummary): string {
1005
+ return `"Like this free tool? ${summary.productName} can do much more - ${summary.valueProposition || `try ${summary.productName} free`}"`;
1006
+ }
1007
+
1008
+ function applyUserContext(summary: SiteSummary, context: AIKeywordResearchOptions['userContext']): SiteSummary {
1009
+ if (!context) return summary;
1010
+
1011
+ return {
1012
+ ...summary,
1013
+ productDescription: context.productDescription || summary.productDescription,
1014
+ targetAudience: context.targetAudience || summary.targetAudience,
1015
+ problemsSolved: context.mainProblem
1016
+ ? [context.mainProblem, ...summary.problemsSolved]
1017
+ : summary.problemsSolved,
1018
+ nicheFocus: context.differentiator || summary.nicheFocus,
1019
+ confidence: Math.min(1, summary.confidence + 0.2), // Boost confidence with user input
1020
+ uncertainties: summary.uncertainties.filter(
1021
+ (u) =>
1022
+ !(context.targetAudience && u.includes('audience')) &&
1023
+ !(context.mainProblem && u.includes('problem'))
1024
+ ),
1025
+ };
1026
+ }
1027
+
1028
+ function applyWizardResponses(
1029
+ summary: SiteSummary,
1030
+ responses: Array<{ id: string; answer: string }>
1031
+ ): SiteSummary {
1032
+ let updated = { ...summary };
1033
+ let confidenceBoost = 0;
1034
+
1035
+ for (const response of responses) {
1036
+ switch (response.id) {
1037
+ case 'target_audience':
1038
+ updated.targetAudience = response.answer;
1039
+ confidenceBoost += 0.15;
1040
+ break;
1041
+ case 'industry':
1042
+ updated.industry = response.answer;
1043
+ confidenceBoost += 0.1;
1044
+ break;
1045
+ case 'main_problem':
1046
+ updated.problemsSolved = [response.answer, ...updated.problemsSolved];
1047
+ confidenceBoost += 0.15;
1048
+ break;
1049
+ case 'differentiator':
1050
+ updated.nicheFocus = response.answer;
1051
+ confidenceBoost += 0.1;
1052
+ break;
1053
+ }
1054
+ }
1055
+
1056
+ updated.confidence = Math.min(1, updated.confidence + confidenceBoost);
1057
+ updated.uncertainties = updated.uncertainties.filter(
1058
+ (u) => !responses.some((r) => u.toLowerCase().includes(r.id.replace('_', ' ')))
1059
+ );
1060
+
1061
+ return updated;
1062
+ }