@rankcli/agent-runtime 0.0.15 → 0.0.17

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