@rankcli/agent-runtime 0.0.15 → 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 (202) hide show
  1. package/dist/index.d.mts +43 -393
  2. package/dist/index.d.ts +43 -393
  3. package/dist/index.js +306 -6526
  4. package/dist/index.mjs +202 -6412
  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/agent-experience.ts +0 -108
  28. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  29. package/src/audit/checks/ai-content-structure.ts +0 -325
  30. package/src/audit/checks/ai-readiness.ts +0 -406
  31. package/src/audit/checks/anchor-text.ts +0 -179
  32. package/src/audit/checks/answer-conciseness.ts +0 -322
  33. package/src/audit/checks/asset-minification.ts +0 -270
  34. package/src/audit/checks/bing-optimization.ts +0 -206
  35. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  36. package/src/audit/checks/caching-headers.ts +0 -305
  37. package/src/audit/checks/canonical-advanced.ts +0 -150
  38. package/src/audit/checks/canonical-domain.ts +0 -196
  39. package/src/audit/checks/citation-quality.ts +0 -358
  40. package/src/audit/checks/client-rendering.ts +0 -542
  41. package/src/audit/checks/color-contrast.ts +0 -342
  42. package/src/audit/checks/content-freshness.ts +0 -170
  43. package/src/audit/checks/content-science.ts +0 -589
  44. package/src/audit/checks/conversion-elements.ts +0 -526
  45. package/src/audit/checks/crawlability.ts +0 -220
  46. package/src/audit/checks/directory-listing.ts +0 -172
  47. package/src/audit/checks/dom-analysis.ts +0 -191
  48. package/src/audit/checks/dom-size.ts +0 -246
  49. package/src/audit/checks/duplicate-content.ts +0 -194
  50. package/src/audit/checks/eeat-signals.ts +0 -990
  51. package/src/audit/checks/entity-seo.ts +0 -396
  52. package/src/audit/checks/featured-snippet.ts +0 -473
  53. package/src/audit/checks/freshness-signals.ts +0 -443
  54. package/src/audit/checks/funnel-intent.ts +0 -463
  55. package/src/audit/checks/hreflang.ts +0 -174
  56. package/src/audit/checks/html-compliance.ts +0 -302
  57. package/src/audit/checks/image-dimensions.ts +0 -167
  58. package/src/audit/checks/images.ts +0 -160
  59. package/src/audit/checks/indexnow.ts +0 -275
  60. package/src/audit/checks/interactive-tools.ts +0 -475
  61. package/src/audit/checks/internal-link-graph.ts +0 -436
  62. package/src/audit/checks/keyword-analysis.ts +0 -239
  63. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  64. package/src/audit/checks/keyword-placement.ts +0 -471
  65. package/src/audit/checks/links.ts +0 -273
  66. package/src/audit/checks/llms-txt.ts +0 -224
  67. package/src/audit/checks/local-seo.ts +0 -296
  68. package/src/audit/checks/mobile.ts +0 -167
  69. package/src/audit/checks/modern-images.ts +0 -226
  70. package/src/audit/checks/navboost-signals.ts +0 -395
  71. package/src/audit/checks/on-page.ts +0 -209
  72. package/src/audit/checks/page-resources.ts +0 -285
  73. package/src/audit/checks/pagination.ts +0 -180
  74. package/src/audit/checks/performance.ts +0 -153
  75. package/src/audit/checks/platform-presence.ts +0 -580
  76. package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
  77. package/src/audit/checks/rag-chunk-readiness.ts +0 -163
  78. package/src/audit/checks/redirect-analysis.ts +0 -153
  79. package/src/audit/checks/redirect-chain.ts +0 -389
  80. package/src/audit/checks/resource-hints.ts +0 -420
  81. package/src/audit/checks/responsive-css.ts +0 -247
  82. package/src/audit/checks/responsive-images.ts +0 -396
  83. package/src/audit/checks/review-ecosystem.ts +0 -415
  84. package/src/audit/checks/robots-validation.ts +0 -373
  85. package/src/audit/checks/security-headers.ts +0 -188
  86. package/src/audit/checks/security.ts +0 -144
  87. package/src/audit/checks/serp-preview.ts +0 -251
  88. package/src/audit/checks/site-maturity.ts +0 -453
  89. package/src/audit/checks/social-meta.test.ts +0 -275
  90. package/src/audit/checks/social-meta.ts +0 -134
  91. package/src/audit/checks/soft-404.ts +0 -151
  92. package/src/audit/checks/structured-data.ts +0 -238
  93. package/src/audit/checks/tech-detection.ts +0 -496
  94. package/src/audit/checks/topical-clusters.ts +0 -435
  95. package/src/audit/checks/tracker-bloat.ts +0 -462
  96. package/src/audit/checks/tracking-verification.test.ts +0 -371
  97. package/src/audit/checks/tracking-verification.ts +0 -636
  98. package/src/audit/checks/url-safety.ts +0 -682
  99. package/src/audit/deno-entry.ts +0 -66
  100. package/src/audit/discovery/index.ts +0 -15
  101. package/src/audit/discovery/link-crawler.ts +0 -233
  102. package/src/audit/discovery/repo-routes.ts +0 -347
  103. package/src/audit/engine.ts +0 -633
  104. package/src/audit/fixes/index.ts +0 -209
  105. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  106. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  107. package/src/audit/index.ts +0 -74
  108. package/src/audit/runner.test.ts +0 -299
  109. package/src/audit/runner.ts +0 -130
  110. package/src/audit/types.ts +0 -1987
  111. package/src/content/featured-snippet.ts +0 -367
  112. package/src/content/generator.test.ts +0 -534
  113. package/src/content/generator.ts +0 -501
  114. package/src/content/headline.ts +0 -317
  115. package/src/content/index.ts +0 -62
  116. package/src/content/intent.ts +0 -258
  117. package/src/content/keyword-density.ts +0 -349
  118. package/src/content/readability.ts +0 -262
  119. package/src/executor.ts +0 -336
  120. package/src/fixer/framework-fixes.test.ts +0 -489
  121. package/src/fixer/framework-fixes.ts +0 -5287
  122. package/src/fixer/index.ts +0 -6
  123. package/src/fixer/schemas.ts +0 -971
  124. package/src/fixer.ts +0 -642
  125. package/src/frameworks/detector.test.ts +0 -248
  126. package/src/frameworks/detector.ts +0 -899
  127. package/src/frameworks/index.ts +0 -68
  128. package/src/frameworks/recipes/angular.yaml +0 -171
  129. package/src/frameworks/recipes/astro.yaml +0 -206
  130. package/src/frameworks/recipes/django.yaml +0 -180
  131. package/src/frameworks/recipes/laravel.yaml +0 -137
  132. package/src/frameworks/recipes/nextjs.yaml +0 -268
  133. package/src/frameworks/recipes/nuxt.yaml +0 -175
  134. package/src/frameworks/recipes/rails.yaml +0 -188
  135. package/src/frameworks/recipes/react.yaml +0 -202
  136. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  137. package/src/frameworks/recipes/vue.yaml +0 -137
  138. package/src/frameworks/recipes/wordpress.yaml +0 -209
  139. package/src/frameworks/suggestion-engine.ts +0 -357
  140. package/src/geo/geo-content.test.ts +0 -305
  141. package/src/geo/geo-content.ts +0 -266
  142. package/src/geo/geo-history.test.ts +0 -473
  143. package/src/geo/geo-history.ts +0 -433
  144. package/src/geo/geo-tracker.test.ts +0 -359
  145. package/src/geo/geo-tracker.ts +0 -411
  146. package/src/geo/index.ts +0 -11
  147. package/src/geo/llm-citation-checker.ts +0 -188
  148. package/src/git/commit-helper.test.ts +0 -261
  149. package/src/git/commit-helper.ts +0 -329
  150. package/src/git/index.ts +0 -12
  151. package/src/git/pr-helper.test.ts +0 -284
  152. package/src/git/pr-helper.ts +0 -307
  153. package/src/index.ts +0 -76
  154. package/src/keywords/ai-keyword-engine.ts +0 -1062
  155. package/src/keywords/ai-summarizer.ts +0 -387
  156. package/src/keywords/ci-mode.ts +0 -555
  157. package/src/keywords/engine.ts +0 -359
  158. package/src/keywords/index.ts +0 -151
  159. package/src/keywords/llm-judge.ts +0 -357
  160. package/src/keywords/nlp-analysis.ts +0 -706
  161. package/src/keywords/prioritizer.ts +0 -295
  162. package/src/keywords/site-crawler.ts +0 -342
  163. package/src/keywords/sources/autocomplete.ts +0 -139
  164. package/src/keywords/sources/competitive-search.ts +0 -450
  165. package/src/keywords/sources/competitor-analysis.ts +0 -374
  166. package/src/keywords/sources/dataforseo.ts +0 -206
  167. package/src/keywords/sources/free-sources.ts +0 -294
  168. package/src/keywords/sources/gsc.ts +0 -123
  169. package/src/keywords/topic-grouping.ts +0 -327
  170. package/src/keywords/types.ts +0 -144
  171. package/src/keywords/wizard.ts +0 -457
  172. package/src/loader.ts +0 -40
  173. package/src/ranking/index.ts +0 -5
  174. package/src/ranking/serp-client.ts +0 -348
  175. package/src/ranking/tracker.ts +0 -380
  176. package/src/ranking/types.ts +0 -123
  177. package/src/reports/index.ts +0 -7
  178. package/src/reports/report-generator.test.ts +0 -293
  179. package/src/reports/report-generator.ts +0 -713
  180. package/src/scheduler/alerts.test.ts +0 -458
  181. package/src/scheduler/alerts.ts +0 -328
  182. package/src/scheduler/index.ts +0 -8
  183. package/src/scheduler/scheduled-audit.test.ts +0 -377
  184. package/src/scheduler/scheduled-audit.ts +0 -149
  185. package/src/test/integration-test.ts +0 -325
  186. package/src/tools/analyzer.ts +0 -373
  187. package/src/tools/crawl.ts +0 -293
  188. package/src/tools/files.ts +0 -301
  189. package/src/tools/h1-fixer.ts +0 -249
  190. package/src/tools/index.ts +0 -67
  191. package/src/tracking/github-action.ts +0 -326
  192. package/src/tracking/google-analytics.ts +0 -265
  193. package/src/tracking/index.ts +0 -45
  194. package/src/tracking/report-generator.ts +0 -386
  195. package/src/tracking/search-console.ts +0 -335
  196. package/src/types.ts +0 -148
  197. package/src/utils/http.ts +0 -302
  198. package/src/wasm-adapter.ts +0 -297
  199. package/src/wasm-entry.ts +0 -14
  200. package/tsconfig.json +0 -17
  201. package/tsup.wasm.config.ts +0 -26
  202. package/vitest.config.ts +0 -15
@@ -1,411 +0,0 @@
1
- /**
2
- * GEO (Generative Engine Optimization) Tracker
3
- *
4
- * Tracks brand visibility in LLM responses to understand how AI models
5
- * recommend or mention your brand for relevant queries.
6
- */
7
-
8
- export type MentionType = 'brand' | 'domain' | 'alternative';
9
- export type LLMProvider = 'openai' | 'anthropic' | 'google' | 'perplexity';
10
- export type Sentiment = 'positive' | 'negative' | 'neutral';
11
-
12
- export interface BrandConfig {
13
- brandName: string;
14
- domains: string[];
15
- alternativeNames?: string[];
16
- }
17
-
18
- export interface Mention {
19
- type: MentionType;
20
- text: string;
21
- position: number;
22
- context: string;
23
- }
24
-
25
- export interface GEOQuery {
26
- keyword: string;
27
- brand: BrandConfig;
28
- providers: LLMProvider[];
29
- }
30
-
31
- export interface GEOResult {
32
- provider: LLMProvider;
33
- keyword: string;
34
- mentioned: boolean;
35
- position: number | null;
36
- sentiment: Sentiment | null;
37
- score: number;
38
- timestamp: string;
39
- contextSnippet?: string;
40
- error?: string;
41
- }
42
-
43
- export interface ParsedResponse {
44
- mentioned: boolean;
45
- position: number | null;
46
- sentiment: Sentiment | null;
47
- contextSnippet: string | null;
48
- }
49
-
50
- export interface TrackingOptions {
51
- fetch?: typeof fetch;
52
- }
53
-
54
- /**
55
- * Detects brand mentions in text
56
- */
57
- export function detectMentions(text: string, config: BrandConfig): Mention[] {
58
- const mentions: Mention[] = [];
59
- const lowerText = text.toLowerCase();
60
-
61
- // Track found positions to avoid duplicates
62
- const foundPositions = new Set<number>();
63
-
64
- // Check for exact brand name (case-insensitive)
65
- const brandLower = config.brandName.toLowerCase();
66
- let searchIndex = 0;
67
- while (true) {
68
- const idx = lowerText.indexOf(brandLower, searchIndex);
69
- if (idx === -1) break;
70
-
71
- if (!foundPositions.has(idx)) {
72
- foundPositions.add(idx);
73
- const position = calculatePosition(text, idx);
74
- mentions.push({
75
- type: 'brand',
76
- text: text.substring(idx, idx + config.brandName.length),
77
- position,
78
- context: extractContext(text, idx),
79
- });
80
- }
81
- searchIndex = idx + 1;
82
- }
83
-
84
- // Check for domain mentions
85
- for (const domain of config.domains) {
86
- const domainLower = domain.toLowerCase();
87
- searchIndex = 0;
88
- while (true) {
89
- const idx = lowerText.indexOf(domainLower, searchIndex);
90
- if (idx === -1) break;
91
-
92
- if (!foundPositions.has(idx)) {
93
- foundPositions.add(idx);
94
- const position = calculatePosition(text, idx);
95
- mentions.push({
96
- type: 'domain',
97
- text: text.substring(idx, idx + domain.length),
98
- position,
99
- context: extractContext(text, idx),
100
- });
101
- }
102
- searchIndex = idx + 1;
103
- }
104
- }
105
-
106
- // Check for alternative names
107
- if (config.alternativeNames) {
108
- for (const altName of config.alternativeNames) {
109
- const altLower = altName.toLowerCase();
110
- searchIndex = 0;
111
- while (true) {
112
- const idx = lowerText.indexOf(altLower, searchIndex);
113
- if (idx === -1) break;
114
-
115
- if (!foundPositions.has(idx)) {
116
- foundPositions.add(idx);
117
- const position = calculatePosition(text, idx);
118
- mentions.push({
119
- type: 'alternative',
120
- text: text.substring(idx, idx + altName.length),
121
- position,
122
- context: extractContext(text, idx),
123
- });
124
- }
125
- searchIndex = idx + 1;
126
- }
127
- }
128
- }
129
-
130
- // Sort by position in text
131
- mentions.sort((a, b) => a.position - b.position);
132
-
133
- return mentions;
134
- }
135
-
136
- /**
137
- * Calculate position (1-indexed) based on list context or sentence order
138
- */
139
- function calculatePosition(text: string, charIndex: number): number {
140
- // Check if we're in a numbered list
141
- const beforeText = text.substring(0, charIndex);
142
- const lines = beforeText.split('\n');
143
- const currentLineStart = beforeText.lastIndexOf('\n') + 1;
144
- const currentLine = text.substring(currentLineStart).split('\n')[0];
145
-
146
- // Check for numbered list pattern (e.g., "1.", "2.", etc.)
147
- const listMatch = currentLine.match(/^(\d+)\./);
148
- if (listMatch) {
149
- return parseInt(listMatch[1], 10);
150
- }
151
-
152
- // Count sentences/items before this mention
153
- const sentences = beforeText.split(/[.!?]\s+/);
154
- return Math.max(1, sentences.length);
155
- }
156
-
157
- /**
158
- * Extract surrounding context for a mention
159
- */
160
- function extractContext(text: string, charIndex: number): string {
161
- const contextRadius = 100;
162
- const start = Math.max(0, charIndex - contextRadius);
163
- const end = Math.min(text.length, charIndex + contextRadius);
164
- return text.substring(start, end).trim();
165
- }
166
-
167
- /**
168
- * Score a mention based on position, type, and context
169
- */
170
- export function scoreMention(mention: Mention | null): number {
171
- if (!mention) return 0;
172
-
173
- let score = 0;
174
-
175
- // Position scoring (first position = highest score)
176
- // Position 1 = 50 points, position 2 = 40, position 3 = 30, etc.
177
- score += Math.max(0, 60 - mention.position * 10);
178
-
179
- // Type scoring
180
- if (mention.type === 'brand') {
181
- score += 30;
182
- } else if (mention.type === 'alternative') {
183
- score += 20;
184
- } else if (mention.type === 'domain') {
185
- score += 15;
186
- }
187
-
188
- // Context scoring - boost for recommendation language
189
- const recommendWords = ['recommend', 'best', 'excellent', 'great', 'suggest', 'top choice', 'top pick'];
190
- const contextLower = mention.context.toLowerCase();
191
- for (const word of recommendWords) {
192
- // Use word boundary matching to avoid false positives (e.g., "top" in "Autopilot")
193
- const wordRegex = new RegExp(`\\b${word}\\b`, 'i');
194
- if (wordRegex.test(contextLower)) {
195
- score += 10;
196
- break;
197
- }
198
- }
199
-
200
- return score;
201
- }
202
-
203
- /**
204
- * Parse an LLM response to extract mention details
205
- */
206
- export function parseGEOResponse(
207
- response: string,
208
- config: BrandConfig
209
- ): ParsedResponse {
210
- const mentions = detectMentions(response, config);
211
-
212
- if (mentions.length === 0) {
213
- return {
214
- mentioned: false,
215
- position: null,
216
- sentiment: null,
217
- contextSnippet: null,
218
- };
219
- }
220
-
221
- // Get the first (most prominent) mention
222
- const primaryMention = mentions[0];
223
-
224
- // Determine sentiment from context
225
- const sentiment = analyzeSentiment(primaryMention.context);
226
-
227
- // Extract context snippet (max 200 chars)
228
- let contextSnippet = primaryMention.context;
229
- if (contextSnippet.length > 200) {
230
- contextSnippet = contextSnippet.substring(0, 197) + '...';
231
- }
232
-
233
- return {
234
- mentioned: true,
235
- position: primaryMention.position,
236
- sentiment,
237
- contextSnippet,
238
- };
239
- }
240
-
241
- /**
242
- * Simple sentiment analysis based on keyword matching
243
- */
244
- function analyzeSentiment(text: string): Sentiment {
245
- const lowerText = text.toLowerCase();
246
-
247
- const positiveWords = [
248
- 'excellent',
249
- 'great',
250
- 'best',
251
- 'recommend',
252
- 'top',
253
- 'amazing',
254
- 'fantastic',
255
- 'highly',
256
- 'love',
257
- 'perfect',
258
- 'outstanding',
259
- ];
260
-
261
- const negativeWords = [
262
- 'limitation',
263
- 'limitations',
264
- 'limited',
265
- 'not suitable',
266
- 'not be suitable',
267
- 'may not',
268
- 'poor',
269
- 'bad',
270
- 'avoid',
271
- 'issue',
272
- 'problem',
273
- 'difficult',
274
- 'complicated',
275
- 'expensive',
276
- 'lack',
277
- 'lacking',
278
- 'drawback',
279
- 'downside',
280
- ];
281
-
282
- let positiveScore = 0;
283
- let negativeScore = 0;
284
-
285
- for (const word of positiveWords) {
286
- if (lowerText.includes(word)) positiveScore++;
287
- }
288
-
289
- for (const word of negativeWords) {
290
- if (lowerText.includes(word)) negativeScore++;
291
- }
292
-
293
- if (positiveScore > negativeScore) return 'positive';
294
- if (negativeScore > positiveScore) return 'negative';
295
- return 'neutral';
296
- }
297
-
298
- /**
299
- * Get the API endpoint for a provider
300
- */
301
- function getProviderEndpoint(provider: LLMProvider): string {
302
- switch (provider) {
303
- case 'openai':
304
- return 'https://api.openai.com/v1/chat/completions';
305
- case 'anthropic':
306
- return 'https://api.anthropic.com/v1/messages';
307
- case 'google':
308
- return 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent';
309
- case 'perplexity':
310
- return 'https://api.perplexity.ai/chat/completions';
311
- default:
312
- throw new Error(`Unknown provider: ${provider}`);
313
- }
314
- }
315
-
316
- /**
317
- * Query a single LLM provider
318
- */
319
- async function queryProvider(
320
- provider: LLMProvider,
321
- keyword: string,
322
- fetchFn: typeof fetch
323
- ): Promise<string> {
324
- const endpoint = getProviderEndpoint(provider);
325
-
326
- const response = await fetchFn(endpoint, {
327
- method: 'POST',
328
- headers: {
329
- 'Content-Type': 'application/json',
330
- },
331
- body: JSON.stringify({
332
- model: provider === 'openai' ? 'gpt-4' : undefined,
333
- messages: [
334
- {
335
- role: 'user',
336
- content: keyword,
337
- },
338
- ],
339
- }),
340
- });
341
-
342
- if (!response.ok) {
343
- throw new Error(`API error: ${response.status}`);
344
- }
345
-
346
- const data = await response.json();
347
-
348
- // Handle OpenAI/Perplexity response format
349
- if (data.choices && data.choices[0]?.message?.content) {
350
- return data.choices[0].message.content;
351
- }
352
-
353
- // Handle Anthropic response format
354
- if (data.content && data.content[0]?.text) {
355
- return data.content[0].text;
356
- }
357
-
358
- // Handle Google response format
359
- if (data.candidates && data.candidates[0]?.content?.parts?.[0]?.text) {
360
- return data.candidates[0].content.parts[0].text;
361
- }
362
-
363
- throw new Error('Unexpected response format');
364
- }
365
-
366
- /**
367
- * Track LLM visibility for a brand across multiple providers
368
- */
369
- export async function trackLLMVisibility(
370
- query: GEOQuery,
371
- options: TrackingOptions = {}
372
- ): Promise<GEOResult[]> {
373
- const fetchFn = options.fetch || fetch;
374
- const timestamp = new Date().toISOString();
375
-
376
- // Query all providers in parallel
377
- const results = await Promise.all(
378
- query.providers.map(async (provider): Promise<GEOResult> => {
379
- try {
380
- const response = await queryProvider(provider, query.keyword, fetchFn);
381
- const parsed = parseGEOResponse(response, query.brand);
382
- const mentions = detectMentions(response, query.brand);
383
- const score = mentions.length > 0 ? scoreMention(mentions[0]) : 0;
384
-
385
- return {
386
- provider,
387
- keyword: query.keyword,
388
- mentioned: parsed.mentioned,
389
- position: parsed.position,
390
- sentiment: parsed.sentiment,
391
- score,
392
- timestamp,
393
- contextSnippet: parsed.contextSnippet || undefined,
394
- };
395
- } catch (error) {
396
- return {
397
- provider,
398
- keyword: query.keyword,
399
- mentioned: false,
400
- position: null,
401
- sentiment: null,
402
- score: 0,
403
- timestamp,
404
- error: error instanceof Error ? error.message : 'Unknown error',
405
- };
406
- }
407
- })
408
- );
409
-
410
- return results;
411
- }
package/src/geo/index.ts DELETED
@@ -1,11 +0,0 @@
1
- /**
2
- * GEO (Generative Engine Optimization) Module
3
- *
4
- * Track brand visibility in LLM responses across providers like
5
- * OpenAI, Anthropic, Google, and Perplexity.
6
- */
7
-
8
- export * from './geo-tracker.js';
9
- export * from './geo-history.js';
10
- export * from './geo-content.js';
11
- export * from './llm-citation-checker.js';
@@ -1,188 +0,0 @@
1
- /**
2
- * LLM Citation Checker
3
- *
4
- * Simplified interface for checking if a brand is mentioned/cited
5
- * by major LLM providers when asked recommendation queries.
6
- */
7
-
8
- import {
9
- trackLLMVisibility,
10
- parseGEOResponse,
11
- type BrandConfig,
12
- type GEOResult,
13
- type LLMProvider,
14
- type TrackingOptions,
15
- } from './geo-tracker.js';
16
-
17
- export interface LLMCitationResult {
18
- provider: LLMProvider;
19
- providerName: string;
20
- mentioned: boolean;
21
- position: number | null;
22
- sentiment: 'positive' | 'neutral' | 'negative' | null;
23
- context: string | null;
24
- score: number;
25
- error?: string;
26
- }
27
-
28
- export interface CitationCheckOptions extends TrackingOptions {
29
- providers?: LLMProvider[];
30
- queries?: string[];
31
- }
32
-
33
- const PROVIDER_NAMES: Record<LLMProvider, string> = {
34
- openai: 'ChatGPT',
35
- anthropic: 'Claude',
36
- google: 'Gemini',
37
- perplexity: 'Perplexity',
38
- };
39
-
40
- const DEFAULT_PROVIDERS: LLMProvider[] = ['openai', 'anthropic', 'google', 'perplexity'];
41
-
42
- /**
43
- * Generate recommendation queries for a brand
44
- */
45
- export function generateRecommendationQueries(brand: string, industry?: string): string[] {
46
- const queries = [
47
- `What are the best ${brand.toLowerCase()} alternatives?`,
48
- `Can you recommend tools similar to ${brand}?`,
49
- `What do you think about ${brand}?`,
50
- ];
51
-
52
- if (industry) {
53
- queries.push(`What are the best ${industry} tools?`);
54
- queries.push(`Recommend a good ${industry} solution`);
55
- }
56
-
57
- return queries;
58
- }
59
-
60
- /**
61
- * Check if a brand is cited/mentioned across LLM providers
62
- */
63
- export async function checkLLMCitations(
64
- brand: string,
65
- domain: string,
66
- options: CitationCheckOptions = {}
67
- ): Promise<LLMCitationResult[]> {
68
- const {
69
- providers = DEFAULT_PROVIDERS,
70
- queries = generateRecommendationQueries(brand),
71
- ...trackingOptions
72
- } = options;
73
-
74
- const brandConfig: BrandConfig = {
75
- brandName: brand,
76
- domains: [domain],
77
- alternativeNames: [brand.toLowerCase(), brand.toUpperCase()],
78
- };
79
-
80
- // Use the first query for the check
81
- const query = queries[0];
82
-
83
- const results = await trackLLMVisibility(
84
- {
85
- keyword: query,
86
- brand: brandConfig,
87
- providers,
88
- },
89
- trackingOptions
90
- );
91
-
92
- return results.map((result) => ({
93
- provider: result.provider,
94
- providerName: PROVIDER_NAMES[result.provider],
95
- mentioned: result.mentioned,
96
- position: result.position,
97
- sentiment: result.sentiment,
98
- context: result.contextSnippet || null,
99
- score: result.score,
100
- error: result.error,
101
- }));
102
- }
103
-
104
- /**
105
- * Calculate overall AI visibility score from citation results
106
- */
107
- export function calculateAIVisibilityScore(results: LLMCitationResult[]): number {
108
- if (results.length === 0) return 0;
109
-
110
- const mentionedCount = results.filter((r) => r.mentioned && !r.error).length;
111
- const validResults = results.filter((r) => !r.error);
112
-
113
- if (validResults.length === 0) return 0;
114
-
115
- // Base score: 25 points per provider mention (out of 100 for 4 providers)
116
- let score = (mentionedCount / validResults.length) * 100;
117
-
118
- // Bonus for good positions (top 3)
119
- const topPositions = results.filter(
120
- (r) => r.mentioned && r.position !== null && r.position <= 3
121
- );
122
- if (topPositions.length > 0) {
123
- score = Math.min(100, score + 10);
124
- }
125
-
126
- // Bonus for positive sentiment
127
- const positiveResults = results.filter((r) => r.sentiment === 'positive');
128
- if (positiveResults.length > mentionedCount / 2) {
129
- score = Math.min(100, score + 5);
130
- }
131
-
132
- return Math.round(score);
133
- }
134
-
135
- /**
136
- * Get a summary of AI visibility across providers
137
- */
138
- export function getAIVisibilitySummary(results: LLMCitationResult[]): {
139
- score: number;
140
- mentionedIn: string[];
141
- notMentionedIn: string[];
142
- bestPosition: number | null;
143
- overallSentiment: 'positive' | 'neutral' | 'negative' | 'mixed' | null;
144
- } {
145
- const score = calculateAIVisibilityScore(results);
146
-
147
- const mentionedIn = results
148
- .filter((r) => r.mentioned)
149
- .map((r) => r.providerName);
150
-
151
- const notMentionedIn = results
152
- .filter((r) => !r.mentioned && !r.error)
153
- .map((r) => r.providerName);
154
-
155
- const positions = results
156
- .filter((r) => r.position !== null)
157
- .map((r) => r.position as number);
158
- const bestPosition = positions.length > 0 ? Math.min(...positions) : null;
159
-
160
- // Calculate overall sentiment
161
- const sentiments = results
162
- .filter((r) => r.sentiment !== null)
163
- .map((r) => r.sentiment);
164
-
165
- let overallSentiment: 'positive' | 'neutral' | 'negative' | 'mixed' | null = null;
166
- if (sentiments.length > 0) {
167
- const positiveCount = sentiments.filter((s) => s === 'positive').length;
168
- const negativeCount = sentiments.filter((s) => s === 'negative').length;
169
-
170
- if (positiveCount > 0 && negativeCount > 0) {
171
- overallSentiment = 'mixed';
172
- } else if (positiveCount > negativeCount) {
173
- overallSentiment = 'positive';
174
- } else if (negativeCount > positiveCount) {
175
- overallSentiment = 'negative';
176
- } else {
177
- overallSentiment = 'neutral';
178
- }
179
- }
180
-
181
- return {
182
- score,
183
- mentionedIn,
184
- notMentionedIn,
185
- bestPosition,
186
- overallSentiment,
187
- };
188
- }