@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.
- package/dist/index.d.mts +77 -393
- package/dist/index.d.ts +77 -393
- package/dist/index.js +417 -6443
- package/dist/index.mjs +296 -6313
- package/package.json +6 -1
- package/scripts/build-deno.ts +0 -134
- package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
- package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
- package/src/analyzers/geo-analyzer.test.ts +0 -310
- package/src/analyzers/geo-analyzer.ts +0 -814
- package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
- package/src/analyzers/image-optimization-analyzer.ts +0 -348
- package/src/analyzers/index.ts +0 -233
- package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
- package/src/analyzers/internal-linking-analyzer.ts +0 -419
- package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
- package/src/analyzers/mobile-seo-analyzer.ts +0 -455
- package/src/analyzers/security-headers-analyzer.test.ts +0 -115
- package/src/analyzers/security-headers-analyzer.ts +0 -318
- package/src/analyzers/structured-data-analyzer.test.ts +0 -210
- package/src/analyzers/structured-data-analyzer.ts +0 -590
- package/src/audit/ai/analyzer.ts +0 -347
- package/src/audit/ai/index.ts +0 -29
- package/src/audit/ai/prompts/content-analysis.ts +0 -271
- package/src/audit/ai/types.ts +0 -179
- package/src/audit/checks/additional-checks.ts +0 -439
- package/src/audit/checks/ai-citation-worthiness.ts +0 -399
- package/src/audit/checks/ai-content-structure.ts +0 -325
- package/src/audit/checks/ai-readiness.ts +0 -339
- package/src/audit/checks/anchor-text.ts +0 -179
- package/src/audit/checks/answer-conciseness.ts +0 -322
- package/src/audit/checks/asset-minification.ts +0 -270
- package/src/audit/checks/bing-optimization.ts +0 -206
- package/src/audit/checks/brand-mention-optimization.ts +0 -349
- package/src/audit/checks/caching-headers.ts +0 -305
- package/src/audit/checks/canonical-advanced.ts +0 -150
- package/src/audit/checks/canonical-domain.ts +0 -196
- package/src/audit/checks/citation-quality.ts +0 -358
- package/src/audit/checks/client-rendering.ts +0 -542
- package/src/audit/checks/color-contrast.ts +0 -342
- package/src/audit/checks/content-freshness.ts +0 -170
- package/src/audit/checks/content-science.ts +0 -589
- package/src/audit/checks/conversion-elements.ts +0 -526
- package/src/audit/checks/crawlability.ts +0 -220
- package/src/audit/checks/directory-listing.ts +0 -172
- package/src/audit/checks/dom-analysis.ts +0 -191
- package/src/audit/checks/dom-size.ts +0 -246
- package/src/audit/checks/duplicate-content.ts +0 -194
- package/src/audit/checks/eeat-signals.ts +0 -990
- package/src/audit/checks/entity-seo.ts +0 -396
- package/src/audit/checks/featured-snippet.ts +0 -473
- package/src/audit/checks/freshness-signals.ts +0 -443
- package/src/audit/checks/funnel-intent.ts +0 -463
- package/src/audit/checks/hreflang.ts +0 -174
- package/src/audit/checks/html-compliance.ts +0 -302
- package/src/audit/checks/image-dimensions.ts +0 -167
- package/src/audit/checks/images.ts +0 -160
- package/src/audit/checks/indexnow.ts +0 -275
- package/src/audit/checks/interactive-tools.ts +0 -475
- package/src/audit/checks/internal-link-graph.ts +0 -436
- package/src/audit/checks/keyword-analysis.ts +0 -239
- package/src/audit/checks/keyword-cannibalization.ts +0 -385
- package/src/audit/checks/keyword-placement.ts +0 -471
- package/src/audit/checks/links.ts +0 -273
- package/src/audit/checks/llms-txt.ts +0 -224
- package/src/audit/checks/local-seo.ts +0 -296
- package/src/audit/checks/mobile.ts +0 -167
- package/src/audit/checks/modern-images.ts +0 -226
- package/src/audit/checks/navboost-signals.ts +0 -395
- package/src/audit/checks/on-page.ts +0 -209
- package/src/audit/checks/page-resources.ts +0 -285
- package/src/audit/checks/pagination.ts +0 -180
- package/src/audit/checks/performance.ts +0 -153
- package/src/audit/checks/platform-presence.ts +0 -580
- package/src/audit/checks/redirect-analysis.ts +0 -153
- package/src/audit/checks/redirect-chain.ts +0 -389
- package/src/audit/checks/resource-hints.ts +0 -420
- package/src/audit/checks/responsive-css.ts +0 -247
- package/src/audit/checks/responsive-images.ts +0 -396
- package/src/audit/checks/review-ecosystem.ts +0 -415
- package/src/audit/checks/robots-validation.ts +0 -373
- package/src/audit/checks/security-headers.ts +0 -188
- package/src/audit/checks/security.ts +0 -144
- package/src/audit/checks/serp-preview.ts +0 -251
- package/src/audit/checks/site-maturity.ts +0 -453
- package/src/audit/checks/social-meta.test.ts +0 -275
- package/src/audit/checks/social-meta.ts +0 -134
- package/src/audit/checks/soft-404.ts +0 -151
- package/src/audit/checks/structured-data.ts +0 -238
- package/src/audit/checks/tech-detection.ts +0 -496
- package/src/audit/checks/topical-clusters.ts +0 -435
- package/src/audit/checks/tracker-bloat.ts +0 -462
- package/src/audit/checks/tracking-verification.test.ts +0 -371
- package/src/audit/checks/tracking-verification.ts +0 -636
- package/src/audit/checks/url-safety.ts +0 -682
- package/src/audit/deno-entry.ts +0 -66
- package/src/audit/discovery/index.ts +0 -15
- package/src/audit/discovery/link-crawler.ts +0 -233
- package/src/audit/discovery/repo-routes.ts +0 -347
- package/src/audit/engine.ts +0 -627
- package/src/audit/fixes/index.ts +0 -209
- package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
- package/src/audit/fixes/social-meta-fixes.ts +0 -463
- package/src/audit/index.ts +0 -74
- package/src/audit/runner.test.ts +0 -299
- package/src/audit/runner.ts +0 -130
- package/src/audit/types.ts +0 -1963
- package/src/content/featured-snippet.ts +0 -367
- package/src/content/generator.test.ts +0 -534
- package/src/content/generator.ts +0 -501
- package/src/content/headline.ts +0 -317
- package/src/content/index.ts +0 -62
- package/src/content/intent.ts +0 -258
- package/src/content/keyword-density.ts +0 -349
- package/src/content/readability.ts +0 -262
- package/src/executor.ts +0 -336
- package/src/fixer/framework-fixes.test.ts +0 -489
- package/src/fixer/framework-fixes.ts +0 -5287
- package/src/fixer/index.ts +0 -6
- package/src/fixer/schemas.ts +0 -971
- package/src/fixer.ts +0 -642
- package/src/frameworks/detector.test.ts +0 -248
- package/src/frameworks/detector.ts +0 -899
- package/src/frameworks/index.ts +0 -68
- package/src/frameworks/recipes/angular.yaml +0 -171
- package/src/frameworks/recipes/astro.yaml +0 -206
- package/src/frameworks/recipes/django.yaml +0 -180
- package/src/frameworks/recipes/laravel.yaml +0 -137
- package/src/frameworks/recipes/nextjs.yaml +0 -268
- package/src/frameworks/recipes/nuxt.yaml +0 -175
- package/src/frameworks/recipes/rails.yaml +0 -188
- package/src/frameworks/recipes/react.yaml +0 -202
- package/src/frameworks/recipes/sveltekit.yaml +0 -154
- package/src/frameworks/recipes/vue.yaml +0 -137
- package/src/frameworks/recipes/wordpress.yaml +0 -209
- package/src/frameworks/suggestion-engine.ts +0 -357
- package/src/geo/geo-content.test.ts +0 -305
- package/src/geo/geo-content.ts +0 -266
- package/src/geo/geo-history.test.ts +0 -473
- package/src/geo/geo-history.ts +0 -433
- package/src/geo/geo-tracker.test.ts +0 -359
- package/src/geo/geo-tracker.ts +0 -411
- package/src/geo/index.ts +0 -11
- package/src/geo/llm-citation-checker.ts +0 -188
- package/src/git/commit-helper.test.ts +0 -261
- package/src/git/commit-helper.ts +0 -329
- package/src/git/index.ts +0 -12
- package/src/git/pr-helper.test.ts +0 -284
- package/src/git/pr-helper.ts +0 -307
- package/src/index.ts +0 -76
- package/src/keywords/ai-keyword-engine.ts +0 -1062
- package/src/keywords/ai-summarizer.ts +0 -387
- package/src/keywords/ci-mode.ts +0 -555
- package/src/keywords/engine.ts +0 -359
- package/src/keywords/index.ts +0 -151
- package/src/keywords/llm-judge.ts +0 -357
- package/src/keywords/nlp-analysis.ts +0 -706
- package/src/keywords/prioritizer.ts +0 -295
- package/src/keywords/site-crawler.ts +0 -342
- package/src/keywords/sources/autocomplete.ts +0 -139
- package/src/keywords/sources/competitive-search.ts +0 -450
- package/src/keywords/sources/competitor-analysis.ts +0 -374
- package/src/keywords/sources/dataforseo.ts +0 -206
- package/src/keywords/sources/free-sources.ts +0 -294
- package/src/keywords/sources/gsc.ts +0 -123
- package/src/keywords/topic-grouping.ts +0 -327
- package/src/keywords/types.ts +0 -144
- package/src/keywords/wizard.ts +0 -457
- package/src/loader.ts +0 -40
- package/src/ranking/index.ts +0 -5
- package/src/ranking/serp-client.ts +0 -348
- package/src/ranking/tracker.ts +0 -380
- package/src/ranking/types.ts +0 -123
- package/src/reports/index.ts +0 -7
- package/src/reports/report-generator.test.ts +0 -293
- package/src/reports/report-generator.ts +0 -713
- package/src/scheduler/alerts.test.ts +0 -458
- package/src/scheduler/alerts.ts +0 -328
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/scheduled-audit.test.ts +0 -377
- package/src/scheduler/scheduled-audit.ts +0 -149
- package/src/test/integration-test.ts +0 -325
- package/src/tools/analyzer.ts +0 -373
- package/src/tools/crawl.ts +0 -293
- package/src/tools/files.ts +0 -301
- package/src/tools/h1-fixer.ts +0 -249
- package/src/tools/index.ts +0 -67
- package/src/tracking/github-action.ts +0 -326
- package/src/tracking/google-analytics.ts +0 -265
- package/src/tracking/index.ts +0 -45
- package/src/tracking/report-generator.ts +0 -386
- package/src/tracking/search-console.ts +0 -335
- package/src/types.ts +0 -148
- package/src/utils/http.ts +0 -302
- package/src/wasm-adapter.ts +0 -297
- package/src/wasm-entry.ts +0 -14
- package/tsconfig.json +0 -17
- package/tsup.wasm.config.ts +0 -26
- package/vitest.config.ts +0 -15
package/src/geo/geo-tracker.ts
DELETED
|
@@ -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
|
-
}
|