@rankcli/agent-runtime 0.0.14 → 0.0.16

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 (199) hide show
  1. package/dist/index.d.mts +77 -393
  2. package/dist/index.d.ts +77 -393
  3. package/dist/index.js +417 -6443
  4. package/dist/index.mjs +296 -6313
  5. package/package.json +6 -1
  6. package/scripts/build-deno.ts +0 -134
  7. package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
  8. package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
  9. package/src/analyzers/geo-analyzer.test.ts +0 -310
  10. package/src/analyzers/geo-analyzer.ts +0 -814
  11. package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
  12. package/src/analyzers/image-optimization-analyzer.ts +0 -348
  13. package/src/analyzers/index.ts +0 -233
  14. package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
  15. package/src/analyzers/internal-linking-analyzer.ts +0 -419
  16. package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
  17. package/src/analyzers/mobile-seo-analyzer.ts +0 -455
  18. package/src/analyzers/security-headers-analyzer.test.ts +0 -115
  19. package/src/analyzers/security-headers-analyzer.ts +0 -318
  20. package/src/analyzers/structured-data-analyzer.test.ts +0 -210
  21. package/src/analyzers/structured-data-analyzer.ts +0 -590
  22. package/src/audit/ai/analyzer.ts +0 -347
  23. package/src/audit/ai/index.ts +0 -29
  24. package/src/audit/ai/prompts/content-analysis.ts +0 -271
  25. package/src/audit/ai/types.ts +0 -179
  26. package/src/audit/checks/additional-checks.ts +0 -439
  27. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  28. package/src/audit/checks/ai-content-structure.ts +0 -325
  29. package/src/audit/checks/ai-readiness.ts +0 -339
  30. package/src/audit/checks/anchor-text.ts +0 -179
  31. package/src/audit/checks/answer-conciseness.ts +0 -322
  32. package/src/audit/checks/asset-minification.ts +0 -270
  33. package/src/audit/checks/bing-optimization.ts +0 -206
  34. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  35. package/src/audit/checks/caching-headers.ts +0 -305
  36. package/src/audit/checks/canonical-advanced.ts +0 -150
  37. package/src/audit/checks/canonical-domain.ts +0 -196
  38. package/src/audit/checks/citation-quality.ts +0 -358
  39. package/src/audit/checks/client-rendering.ts +0 -542
  40. package/src/audit/checks/color-contrast.ts +0 -342
  41. package/src/audit/checks/content-freshness.ts +0 -170
  42. package/src/audit/checks/content-science.ts +0 -589
  43. package/src/audit/checks/conversion-elements.ts +0 -526
  44. package/src/audit/checks/crawlability.ts +0 -220
  45. package/src/audit/checks/directory-listing.ts +0 -172
  46. package/src/audit/checks/dom-analysis.ts +0 -191
  47. package/src/audit/checks/dom-size.ts +0 -246
  48. package/src/audit/checks/duplicate-content.ts +0 -194
  49. package/src/audit/checks/eeat-signals.ts +0 -990
  50. package/src/audit/checks/entity-seo.ts +0 -396
  51. package/src/audit/checks/featured-snippet.ts +0 -473
  52. package/src/audit/checks/freshness-signals.ts +0 -443
  53. package/src/audit/checks/funnel-intent.ts +0 -463
  54. package/src/audit/checks/hreflang.ts +0 -174
  55. package/src/audit/checks/html-compliance.ts +0 -302
  56. package/src/audit/checks/image-dimensions.ts +0 -167
  57. package/src/audit/checks/images.ts +0 -160
  58. package/src/audit/checks/indexnow.ts +0 -275
  59. package/src/audit/checks/interactive-tools.ts +0 -475
  60. package/src/audit/checks/internal-link-graph.ts +0 -436
  61. package/src/audit/checks/keyword-analysis.ts +0 -239
  62. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  63. package/src/audit/checks/keyword-placement.ts +0 -471
  64. package/src/audit/checks/links.ts +0 -273
  65. package/src/audit/checks/llms-txt.ts +0 -224
  66. package/src/audit/checks/local-seo.ts +0 -296
  67. package/src/audit/checks/mobile.ts +0 -167
  68. package/src/audit/checks/modern-images.ts +0 -226
  69. package/src/audit/checks/navboost-signals.ts +0 -395
  70. package/src/audit/checks/on-page.ts +0 -209
  71. package/src/audit/checks/page-resources.ts +0 -285
  72. package/src/audit/checks/pagination.ts +0 -180
  73. package/src/audit/checks/performance.ts +0 -153
  74. package/src/audit/checks/platform-presence.ts +0 -580
  75. package/src/audit/checks/redirect-analysis.ts +0 -153
  76. package/src/audit/checks/redirect-chain.ts +0 -389
  77. package/src/audit/checks/resource-hints.ts +0 -420
  78. package/src/audit/checks/responsive-css.ts +0 -247
  79. package/src/audit/checks/responsive-images.ts +0 -396
  80. package/src/audit/checks/review-ecosystem.ts +0 -415
  81. package/src/audit/checks/robots-validation.ts +0 -373
  82. package/src/audit/checks/security-headers.ts +0 -188
  83. package/src/audit/checks/security.ts +0 -144
  84. package/src/audit/checks/serp-preview.ts +0 -251
  85. package/src/audit/checks/site-maturity.ts +0 -453
  86. package/src/audit/checks/social-meta.test.ts +0 -275
  87. package/src/audit/checks/social-meta.ts +0 -134
  88. package/src/audit/checks/soft-404.ts +0 -151
  89. package/src/audit/checks/structured-data.ts +0 -238
  90. package/src/audit/checks/tech-detection.ts +0 -496
  91. package/src/audit/checks/topical-clusters.ts +0 -435
  92. package/src/audit/checks/tracker-bloat.ts +0 -462
  93. package/src/audit/checks/tracking-verification.test.ts +0 -371
  94. package/src/audit/checks/tracking-verification.ts +0 -636
  95. package/src/audit/checks/url-safety.ts +0 -682
  96. package/src/audit/deno-entry.ts +0 -66
  97. package/src/audit/discovery/index.ts +0 -15
  98. package/src/audit/discovery/link-crawler.ts +0 -233
  99. package/src/audit/discovery/repo-routes.ts +0 -347
  100. package/src/audit/engine.ts +0 -627
  101. package/src/audit/fixes/index.ts +0 -209
  102. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  103. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  104. package/src/audit/index.ts +0 -74
  105. package/src/audit/runner.test.ts +0 -299
  106. package/src/audit/runner.ts +0 -130
  107. package/src/audit/types.ts +0 -1963
  108. package/src/content/featured-snippet.ts +0 -367
  109. package/src/content/generator.test.ts +0 -534
  110. package/src/content/generator.ts +0 -501
  111. package/src/content/headline.ts +0 -317
  112. package/src/content/index.ts +0 -62
  113. package/src/content/intent.ts +0 -258
  114. package/src/content/keyword-density.ts +0 -349
  115. package/src/content/readability.ts +0 -262
  116. package/src/executor.ts +0 -336
  117. package/src/fixer/framework-fixes.test.ts +0 -489
  118. package/src/fixer/framework-fixes.ts +0 -5287
  119. package/src/fixer/index.ts +0 -6
  120. package/src/fixer/schemas.ts +0 -971
  121. package/src/fixer.ts +0 -642
  122. package/src/frameworks/detector.test.ts +0 -248
  123. package/src/frameworks/detector.ts +0 -899
  124. package/src/frameworks/index.ts +0 -68
  125. package/src/frameworks/recipes/angular.yaml +0 -171
  126. package/src/frameworks/recipes/astro.yaml +0 -206
  127. package/src/frameworks/recipes/django.yaml +0 -180
  128. package/src/frameworks/recipes/laravel.yaml +0 -137
  129. package/src/frameworks/recipes/nextjs.yaml +0 -268
  130. package/src/frameworks/recipes/nuxt.yaml +0 -175
  131. package/src/frameworks/recipes/rails.yaml +0 -188
  132. package/src/frameworks/recipes/react.yaml +0 -202
  133. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  134. package/src/frameworks/recipes/vue.yaml +0 -137
  135. package/src/frameworks/recipes/wordpress.yaml +0 -209
  136. package/src/frameworks/suggestion-engine.ts +0 -357
  137. package/src/geo/geo-content.test.ts +0 -305
  138. package/src/geo/geo-content.ts +0 -266
  139. package/src/geo/geo-history.test.ts +0 -473
  140. package/src/geo/geo-history.ts +0 -433
  141. package/src/geo/geo-tracker.test.ts +0 -359
  142. package/src/geo/geo-tracker.ts +0 -411
  143. package/src/geo/index.ts +0 -11
  144. package/src/geo/llm-citation-checker.ts +0 -188
  145. package/src/git/commit-helper.test.ts +0 -261
  146. package/src/git/commit-helper.ts +0 -329
  147. package/src/git/index.ts +0 -12
  148. package/src/git/pr-helper.test.ts +0 -284
  149. package/src/git/pr-helper.ts +0 -307
  150. package/src/index.ts +0 -76
  151. package/src/keywords/ai-keyword-engine.ts +0 -1062
  152. package/src/keywords/ai-summarizer.ts +0 -387
  153. package/src/keywords/ci-mode.ts +0 -555
  154. package/src/keywords/engine.ts +0 -359
  155. package/src/keywords/index.ts +0 -151
  156. package/src/keywords/llm-judge.ts +0 -357
  157. package/src/keywords/nlp-analysis.ts +0 -706
  158. package/src/keywords/prioritizer.ts +0 -295
  159. package/src/keywords/site-crawler.ts +0 -342
  160. package/src/keywords/sources/autocomplete.ts +0 -139
  161. package/src/keywords/sources/competitive-search.ts +0 -450
  162. package/src/keywords/sources/competitor-analysis.ts +0 -374
  163. package/src/keywords/sources/dataforseo.ts +0 -206
  164. package/src/keywords/sources/free-sources.ts +0 -294
  165. package/src/keywords/sources/gsc.ts +0 -123
  166. package/src/keywords/topic-grouping.ts +0 -327
  167. package/src/keywords/types.ts +0 -144
  168. package/src/keywords/wizard.ts +0 -457
  169. package/src/loader.ts +0 -40
  170. package/src/ranking/index.ts +0 -5
  171. package/src/ranking/serp-client.ts +0 -348
  172. package/src/ranking/tracker.ts +0 -380
  173. package/src/ranking/types.ts +0 -123
  174. package/src/reports/index.ts +0 -7
  175. package/src/reports/report-generator.test.ts +0 -293
  176. package/src/reports/report-generator.ts +0 -713
  177. package/src/scheduler/alerts.test.ts +0 -458
  178. package/src/scheduler/alerts.ts +0 -328
  179. package/src/scheduler/index.ts +0 -8
  180. package/src/scheduler/scheduled-audit.test.ts +0 -377
  181. package/src/scheduler/scheduled-audit.ts +0 -149
  182. package/src/test/integration-test.ts +0 -325
  183. package/src/tools/analyzer.ts +0 -373
  184. package/src/tools/crawl.ts +0 -293
  185. package/src/tools/files.ts +0 -301
  186. package/src/tools/h1-fixer.ts +0 -249
  187. package/src/tools/index.ts +0 -67
  188. package/src/tracking/github-action.ts +0 -326
  189. package/src/tracking/google-analytics.ts +0 -265
  190. package/src/tracking/index.ts +0 -45
  191. package/src/tracking/report-generator.ts +0 -386
  192. package/src/tracking/search-console.ts +0 -335
  193. package/src/types.ts +0 -148
  194. package/src/utils/http.ts +0 -302
  195. package/src/wasm-adapter.ts +0 -297
  196. package/src/wasm-entry.ts +0 -14
  197. package/tsconfig.json +0 -17
  198. package/tsup.wasm.config.ts +0 -26
  199. package/vitest.config.ts +0 -15
@@ -1,1062 +0,0 @@
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
- }