@rankcli/agent-runtime 0.0.12 → 0.0.14
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 +181 -1
- package/dist/index.d.ts +181 -1
- package/dist/index.js +578 -15
- package/dist/index.mjs +575 -15
- package/package.json +3 -2
- package/src/audit/checks/client-rendering.ts +10 -10
- package/src/audit/checks/security-headers.ts +18 -2
- package/src/audit/engine.ts +12 -5
- package/src/index.ts +3 -0
- package/src/ranking/index.ts +5 -0
- package/src/ranking/serp-client.ts +348 -0
- package/src/ranking/tracker.ts +380 -0
- package/src/ranking/types.ts +123 -0
package/dist/index.mjs
CHANGED
|
@@ -4552,7 +4552,13 @@ async function analyzeSecurityHeaders2(url) {
|
|
|
4552
4552
|
maxRedirects: 5
|
|
4553
4553
|
});
|
|
4554
4554
|
const headers = response.headers;
|
|
4555
|
-
|
|
4555
|
+
let isHttps = false;
|
|
4556
|
+
try {
|
|
4557
|
+
const parsedUrl = new URL(url);
|
|
4558
|
+
isHttps = parsedUrl.protocol === "https:";
|
|
4559
|
+
} catch {
|
|
4560
|
+
isHttps = url.toLowerCase().startsWith("https://");
|
|
4561
|
+
}
|
|
4556
4562
|
const getHeader = (name) => {
|
|
4557
4563
|
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
4558
4564
|
return key ? headers[key] : null;
|
|
@@ -4624,10 +4630,17 @@ async function analyzeSecurityHeaders2(url) {
|
|
|
4624
4630
|
}
|
|
4625
4631
|
};
|
|
4626
4632
|
} catch (error) {
|
|
4633
|
+
let isHttps = false;
|
|
4634
|
+
try {
|
|
4635
|
+
const parsedUrl = new URL(url);
|
|
4636
|
+
isHttps = parsedUrl.protocol === "https:";
|
|
4637
|
+
} catch {
|
|
4638
|
+
isHttps = url.toLowerCase().startsWith("https://");
|
|
4639
|
+
}
|
|
4627
4640
|
return {
|
|
4628
4641
|
issues,
|
|
4629
4642
|
data: {
|
|
4630
|
-
https:
|
|
4643
|
+
https: isHttps,
|
|
4631
4644
|
headers: {
|
|
4632
4645
|
hsts: null,
|
|
4633
4646
|
csp: null,
|
|
@@ -8661,17 +8674,17 @@ function generateRecommendations2(renderingMethod, hasContentInHTML, charCount,
|
|
|
8661
8674
|
);
|
|
8662
8675
|
if (framework === "React") {
|
|
8663
8676
|
recommendations.push(
|
|
8664
|
-
|
|
8677
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG - works with Vite, minimal config needed"
|
|
8665
8678
|
);
|
|
8666
8679
|
recommendations.push(
|
|
8667
|
-
"Alternative:
|
|
8680
|
+
"Alternative: Migrate to Next.js or Remix for built-in SSR/SSG support"
|
|
8668
8681
|
);
|
|
8669
8682
|
} else if (framework === "Vue") {
|
|
8670
8683
|
recommendations.push(
|
|
8671
|
-
"Quick fix:
|
|
8684
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG - works with Vite, minimal config needed"
|
|
8672
8685
|
);
|
|
8673
8686
|
recommendations.push(
|
|
8674
|
-
"Alternative:
|
|
8687
|
+
"Alternative: Migrate to Nuxt for built-in SSR/SSG support"
|
|
8675
8688
|
);
|
|
8676
8689
|
} else if (framework === "Angular Universal") {
|
|
8677
8690
|
recommendations.push(
|
|
@@ -8679,7 +8692,7 @@ function generateRecommendations2(renderingMethod, hasContentInHTML, charCount,
|
|
|
8679
8692
|
);
|
|
8680
8693
|
} else {
|
|
8681
8694
|
recommendations.push(
|
|
8682
|
-
"Quick fix: Use
|
|
8695
|
+
"Quick fix: Use Vike (vike.dev) for SSR/SSG with any Vite-based framework"
|
|
8683
8696
|
);
|
|
8684
8697
|
}
|
|
8685
8698
|
recommendations.push(
|
|
@@ -8703,7 +8716,7 @@ function analyzeClientRendering(html, url) {
|
|
|
8703
8716
|
const issues = [];
|
|
8704
8717
|
const analysis = analyzeRendering(html);
|
|
8705
8718
|
if (analysis.renderingMethod === "csr" && analysis.confidence !== "low") {
|
|
8706
|
-
const howToFixByFramework = analysis.frameworkDetected === "React" ?
|
|
8719
|
+
const howToFixByFramework = analysis.frameworkDetected === "React" ? "Use Vike (vike.dev) to add SSR/SSG to your Vite React app: npm install vike vike-react, then follow the setup guide. Alternatively, migrate to Next.js or Remix." : analysis.frameworkDetected === "Vue" ? "Use Vike (vike.dev) to add SSR/SSG to your Vite Vue app: npm install vike vike-vue, then follow the setup guide. Alternatively, migrate to Nuxt." : "Use Vike (vike.dev) for SSR/SSG with Vite-based frameworks, or use a framework with built-in SSR support (Next.js, Nuxt, Remix, SvelteKit).";
|
|
8707
8720
|
issues.push({
|
|
8708
8721
|
code: "CLIENT_SIDE_RENDERING",
|
|
8709
8722
|
severity: "error",
|
|
@@ -8744,7 +8757,7 @@ function analyzeClientRendering(html, url) {
|
|
|
8744
8757
|
title: `${analysis.frameworkDetected} detected without SSR markers`,
|
|
8745
8758
|
description: `This appears to be a ${analysis.frameworkDetected} SPA without server-side rendering enabled.`,
|
|
8746
8759
|
impact: "Single Page Applications without SSR have slower time-to-content for search crawlers.",
|
|
8747
|
-
howToFix: analysis.frameworkDetected === "React" ? "
|
|
8760
|
+
howToFix: analysis.frameworkDetected === "React" ? "Add SSR/SSG using Vike (vike.dev): npm install vike vike-react. Alternative: migrate to Next.js or Remix." : "Add SSR/SSG using Vike (vike.dev): npm install vike vike-vue. Alternative: migrate to Nuxt.",
|
|
8748
8761
|
affectedUrls: [url],
|
|
8749
8762
|
details: {
|
|
8750
8763
|
framework: analysis.frameworkDetected,
|
|
@@ -16659,20 +16672,26 @@ async function runFullAudit(options) {
|
|
|
16659
16672
|
timeout: 3e4,
|
|
16660
16673
|
validateStatus: () => true
|
|
16661
16674
|
}).catch((err) => {
|
|
16675
|
+
console.error("Main page fetch failed:", err instanceof Error ? { message: err.message, cause: err.cause } : err);
|
|
16662
16676
|
return { error: err, data: "", headers: {} };
|
|
16663
16677
|
})
|
|
16664
16678
|
]);
|
|
16665
16679
|
allIssues.push(...crawlabilityResult);
|
|
16666
16680
|
if ("error" in fetchResult) {
|
|
16681
|
+
const err = fetchResult.error;
|
|
16682
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
16683
|
+
const errorCause = err instanceof Error && err.cause ? ` (${String(err.cause)})` : "";
|
|
16667
16684
|
allIssues.push({
|
|
16668
16685
|
code: "FETCH_ERROR",
|
|
16669
16686
|
severity: "error",
|
|
16670
|
-
category: "
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16687
|
+
category: "indexability",
|
|
16688
|
+
// Not crawlability - robots/sitemap checks may have succeeded
|
|
16689
|
+
title: "Page fetch failed",
|
|
16690
|
+
description: `Could not load page content: ${errorMsg}${errorCause}. Only robots.txt and sitemap checks were performed.`,
|
|
16691
|
+
impact: "Cannot perform full SEO audit without page HTML content.",
|
|
16692
|
+
howToFix: "Verify the URL is accessible, the server is responding, and there are no firewall/geo blocks.",
|
|
16693
|
+
affectedUrls: [url],
|
|
16694
|
+
details: { error: errorMsg, cause: errorCause || void 0 }
|
|
16676
16695
|
});
|
|
16677
16696
|
return createReport(url, domain, allIssues, pages);
|
|
16678
16697
|
}
|
|
@@ -33755,6 +33774,544 @@ function getAIVisibilitySummary(results) {
|
|
|
33755
33774
|
};
|
|
33756
33775
|
}
|
|
33757
33776
|
|
|
33777
|
+
// src/ranking/types.ts
|
|
33778
|
+
var TIER_LIMITS = {
|
|
33779
|
+
free: {
|
|
33780
|
+
maxKeywords: 10,
|
|
33781
|
+
checksPerDay: 1,
|
|
33782
|
+
serpFeatures: false,
|
|
33783
|
+
competitorTracking: false,
|
|
33784
|
+
historyDays: 7
|
|
33785
|
+
},
|
|
33786
|
+
solo: {
|
|
33787
|
+
maxKeywords: 100,
|
|
33788
|
+
checksPerDay: 1,
|
|
33789
|
+
serpFeatures: true,
|
|
33790
|
+
competitorTracking: false,
|
|
33791
|
+
historyDays: 30
|
|
33792
|
+
},
|
|
33793
|
+
pro: {
|
|
33794
|
+
maxKeywords: 500,
|
|
33795
|
+
checksPerDay: 2,
|
|
33796
|
+
serpFeatures: true,
|
|
33797
|
+
competitorTracking: true,
|
|
33798
|
+
historyDays: 90
|
|
33799
|
+
},
|
|
33800
|
+
agency: {
|
|
33801
|
+
maxKeywords: 2e3,
|
|
33802
|
+
checksPerDay: 4,
|
|
33803
|
+
serpFeatures: true,
|
|
33804
|
+
competitorTracking: true,
|
|
33805
|
+
historyDays: 365
|
|
33806
|
+
}
|
|
33807
|
+
};
|
|
33808
|
+
|
|
33809
|
+
// src/ranking/serp-client.ts
|
|
33810
|
+
var SerpClient = class {
|
|
33811
|
+
config;
|
|
33812
|
+
constructor(config) {
|
|
33813
|
+
this.config = config;
|
|
33814
|
+
}
|
|
33815
|
+
/**
|
|
33816
|
+
* Check ranking for a single keyword
|
|
33817
|
+
*/
|
|
33818
|
+
async checkRank(options) {
|
|
33819
|
+
const results = [];
|
|
33820
|
+
for (const keyword of options.keywords) {
|
|
33821
|
+
try {
|
|
33822
|
+
const result = await this.checkSingleKeyword({
|
|
33823
|
+
...options,
|
|
33824
|
+
keyword
|
|
33825
|
+
});
|
|
33826
|
+
results.push(result);
|
|
33827
|
+
} catch (error) {
|
|
33828
|
+
console.error(`Error checking rank for "${keyword}":`, error);
|
|
33829
|
+
results.push({
|
|
33830
|
+
keyword,
|
|
33831
|
+
position: null,
|
|
33832
|
+
url: null,
|
|
33833
|
+
serpFeatures: [],
|
|
33834
|
+
topResults: [],
|
|
33835
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
33836
|
+
});
|
|
33837
|
+
}
|
|
33838
|
+
}
|
|
33839
|
+
return results;
|
|
33840
|
+
}
|
|
33841
|
+
async checkSingleKeyword(options) {
|
|
33842
|
+
switch (this.config.provider) {
|
|
33843
|
+
case "valueserp":
|
|
33844
|
+
return this.checkViaValueSerp(options);
|
|
33845
|
+
case "serpapi":
|
|
33846
|
+
return this.checkViaSerpApi(options);
|
|
33847
|
+
case "direct":
|
|
33848
|
+
default:
|
|
33849
|
+
return this.checkViaDirect(options);
|
|
33850
|
+
}
|
|
33851
|
+
}
|
|
33852
|
+
/**
|
|
33853
|
+
* ValueSERP API implementation
|
|
33854
|
+
* Docs: https://www.valueserp.com/docs
|
|
33855
|
+
*/
|
|
33856
|
+
async checkViaValueSerp(options) {
|
|
33857
|
+
if (!this.config.apiKey) {
|
|
33858
|
+
throw new Error("ValueSERP API key required");
|
|
33859
|
+
}
|
|
33860
|
+
const params = new URLSearchParams({
|
|
33861
|
+
api_key: this.config.apiKey,
|
|
33862
|
+
q: options.keyword,
|
|
33863
|
+
location: options.country || "United States",
|
|
33864
|
+
google_domain: options.country === "US" ? "google.com" : `google.${options.country?.toLowerCase() || "com"}`,
|
|
33865
|
+
gl: options.country || "us",
|
|
33866
|
+
hl: options.language || "en",
|
|
33867
|
+
device: options.device || "desktop",
|
|
33868
|
+
num: "100"
|
|
33869
|
+
// Get top 100 results
|
|
33870
|
+
});
|
|
33871
|
+
const response = await fetch(`https://api.valueserp.com/search?${params}`);
|
|
33872
|
+
if (!response.ok) {
|
|
33873
|
+
throw new Error(`ValueSERP API error: ${response.status}`);
|
|
33874
|
+
}
|
|
33875
|
+
const data = await response.json();
|
|
33876
|
+
return this.parseValueSerpResponse(data, options.domain, options.keyword);
|
|
33877
|
+
}
|
|
33878
|
+
/**
|
|
33879
|
+
* SerpAPI implementation (alternative)
|
|
33880
|
+
* Docs: https://serpapi.com/search-api
|
|
33881
|
+
*/
|
|
33882
|
+
async checkViaSerpApi(options) {
|
|
33883
|
+
if (!this.config.apiKey) {
|
|
33884
|
+
throw new Error("SerpAPI key required");
|
|
33885
|
+
}
|
|
33886
|
+
const params = new URLSearchParams({
|
|
33887
|
+
api_key: this.config.apiKey,
|
|
33888
|
+
q: options.keyword,
|
|
33889
|
+
location: options.country === "US" ? "United States" : options.country || "United States",
|
|
33890
|
+
gl: options.country?.toLowerCase() || "us",
|
|
33891
|
+
hl: options.language || "en",
|
|
33892
|
+
device: options.device || "desktop",
|
|
33893
|
+
num: "100"
|
|
33894
|
+
});
|
|
33895
|
+
const response = await fetch(`https://serpapi.com/search?${params}`);
|
|
33896
|
+
if (!response.ok) {
|
|
33897
|
+
throw new Error(`SerpAPI error: ${response.status}`);
|
|
33898
|
+
}
|
|
33899
|
+
const data = await response.json();
|
|
33900
|
+
return this.parseSerpApiResponse(data, options.domain, options.keyword);
|
|
33901
|
+
}
|
|
33902
|
+
/**
|
|
33903
|
+
* Direct scraping fallback (rate-limited, use with caution)
|
|
33904
|
+
*/
|
|
33905
|
+
async checkViaDirect(options) {
|
|
33906
|
+
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(options.keyword)}&num=100`;
|
|
33907
|
+
const headers = {
|
|
33908
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
33909
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
33910
|
+
"Accept-Language": options.language || "en-US,en;q=0.9"
|
|
33911
|
+
};
|
|
33912
|
+
const response = await fetch(searchUrl, { headers });
|
|
33913
|
+
if (!response.ok) {
|
|
33914
|
+
throw new Error(`Direct search failed: ${response.status}`);
|
|
33915
|
+
}
|
|
33916
|
+
const html = await response.text();
|
|
33917
|
+
return this.parseDirectSearchResults(html, options.domain, options.keyword);
|
|
33918
|
+
}
|
|
33919
|
+
parseValueSerpResponse(data, domain, keyword) {
|
|
33920
|
+
const topResults = [];
|
|
33921
|
+
const serpFeatures = [];
|
|
33922
|
+
let position = null;
|
|
33923
|
+
let url = null;
|
|
33924
|
+
if (data.organic_results) {
|
|
33925
|
+
for (let i = 0; i < data.organic_results.length; i++) {
|
|
33926
|
+
const result = data.organic_results[i];
|
|
33927
|
+
const resultDomain = this.extractDomain(result.link);
|
|
33928
|
+
topResults.push({
|
|
33929
|
+
position: i + 1,
|
|
33930
|
+
url: result.link,
|
|
33931
|
+
domain: resultDomain,
|
|
33932
|
+
title: result.title
|
|
33933
|
+
});
|
|
33934
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
33935
|
+
position = i + 1;
|
|
33936
|
+
url = result.link;
|
|
33937
|
+
}
|
|
33938
|
+
}
|
|
33939
|
+
}
|
|
33940
|
+
if (data.answer_box) {
|
|
33941
|
+
serpFeatures.push({
|
|
33942
|
+
type: "featured_snippet",
|
|
33943
|
+
position: 0,
|
|
33944
|
+
hasOwnSite: this.domainMatches(this.extractDomain(data.answer_box.link || ""), domain)
|
|
33945
|
+
});
|
|
33946
|
+
}
|
|
33947
|
+
if (data.people_also_ask) {
|
|
33948
|
+
serpFeatures.push({
|
|
33949
|
+
type: "people_also_ask",
|
|
33950
|
+
hasOwnSite: data.people_also_ask.some(
|
|
33951
|
+
(paa) => this.domainMatches(this.extractDomain(paa.link || ""), domain)
|
|
33952
|
+
)
|
|
33953
|
+
});
|
|
33954
|
+
}
|
|
33955
|
+
if (data.local_results) {
|
|
33956
|
+
serpFeatures.push({ type: "local_pack" });
|
|
33957
|
+
}
|
|
33958
|
+
if (data.knowledge_graph) {
|
|
33959
|
+
serpFeatures.push({ type: "knowledge_panel" });
|
|
33960
|
+
}
|
|
33961
|
+
return {
|
|
33962
|
+
keyword,
|
|
33963
|
+
position,
|
|
33964
|
+
url,
|
|
33965
|
+
serpFeatures,
|
|
33966
|
+
topResults: topResults.slice(0, 10),
|
|
33967
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
33968
|
+
};
|
|
33969
|
+
}
|
|
33970
|
+
parseSerpApiResponse(data, domain, keyword) {
|
|
33971
|
+
const topResults = [];
|
|
33972
|
+
const serpFeatures = [];
|
|
33973
|
+
let position = null;
|
|
33974
|
+
let url = null;
|
|
33975
|
+
if (data.organic_results) {
|
|
33976
|
+
for (let i = 0; i < data.organic_results.length; i++) {
|
|
33977
|
+
const result = data.organic_results[i];
|
|
33978
|
+
const resultDomain = this.extractDomain(result.link);
|
|
33979
|
+
topResults.push({
|
|
33980
|
+
position: result.position || i + 1,
|
|
33981
|
+
url: result.link,
|
|
33982
|
+
domain: resultDomain,
|
|
33983
|
+
title: result.title
|
|
33984
|
+
});
|
|
33985
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
33986
|
+
position = result.position || i + 1;
|
|
33987
|
+
url = result.link;
|
|
33988
|
+
}
|
|
33989
|
+
}
|
|
33990
|
+
}
|
|
33991
|
+
if (data.answer_box) {
|
|
33992
|
+
serpFeatures.push({
|
|
33993
|
+
type: "featured_snippet",
|
|
33994
|
+
position: 0
|
|
33995
|
+
});
|
|
33996
|
+
}
|
|
33997
|
+
if (data.related_questions) {
|
|
33998
|
+
serpFeatures.push({ type: "people_also_ask" });
|
|
33999
|
+
}
|
|
34000
|
+
return {
|
|
34001
|
+
keyword,
|
|
34002
|
+
position,
|
|
34003
|
+
url,
|
|
34004
|
+
serpFeatures,
|
|
34005
|
+
topResults: topResults.slice(0, 10),
|
|
34006
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34007
|
+
};
|
|
34008
|
+
}
|
|
34009
|
+
parseDirectSearchResults(html, domain, keyword) {
|
|
34010
|
+
const topResults = [];
|
|
34011
|
+
let position = null;
|
|
34012
|
+
let url = null;
|
|
34013
|
+
const linkRegex = /<a[^>]+href="\/url\?q=([^"&]+)/g;
|
|
34014
|
+
let match;
|
|
34015
|
+
let index = 0;
|
|
34016
|
+
while ((match = linkRegex.exec(html)) !== null && index < 100) {
|
|
34017
|
+
try {
|
|
34018
|
+
const decodedUrl = decodeURIComponent(match[1]);
|
|
34019
|
+
const resultDomain = this.extractDomain(decodedUrl);
|
|
34020
|
+
if (resultDomain.includes("google.com") || resultDomain.includes("gstatic.com")) {
|
|
34021
|
+
continue;
|
|
34022
|
+
}
|
|
34023
|
+
index++;
|
|
34024
|
+
topResults.push({
|
|
34025
|
+
position: index,
|
|
34026
|
+
url: decodedUrl,
|
|
34027
|
+
domain: resultDomain
|
|
34028
|
+
});
|
|
34029
|
+
if (this.domainMatches(resultDomain, domain) && position === null) {
|
|
34030
|
+
position = index;
|
|
34031
|
+
url = decodedUrl;
|
|
34032
|
+
}
|
|
34033
|
+
} catch {
|
|
34034
|
+
}
|
|
34035
|
+
}
|
|
34036
|
+
return {
|
|
34037
|
+
keyword,
|
|
34038
|
+
position,
|
|
34039
|
+
url,
|
|
34040
|
+
serpFeatures: [],
|
|
34041
|
+
// Direct scraping doesn't easily extract SERP features
|
|
34042
|
+
topResults: topResults.slice(0, 10),
|
|
34043
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
34044
|
+
};
|
|
34045
|
+
}
|
|
34046
|
+
extractDomain(url) {
|
|
34047
|
+
try {
|
|
34048
|
+
const parsed = new URL(url);
|
|
34049
|
+
return parsed.hostname.replace(/^www\./, "");
|
|
34050
|
+
} catch {
|
|
34051
|
+
return "";
|
|
34052
|
+
}
|
|
34053
|
+
}
|
|
34054
|
+
domainMatches(resultDomain, targetDomain) {
|
|
34055
|
+
const normalizedResult = resultDomain.toLowerCase().replace(/^www\./, "");
|
|
34056
|
+
const normalizedTarget = targetDomain.toLowerCase().replace(/^www\./, "");
|
|
34057
|
+
return normalizedResult === normalizedTarget || normalizedResult.endsWith(`.${normalizedTarget}`);
|
|
34058
|
+
}
|
|
34059
|
+
};
|
|
34060
|
+
|
|
34061
|
+
// src/ranking/tracker.ts
|
|
34062
|
+
var RankTracker = class {
|
|
34063
|
+
supabase;
|
|
34064
|
+
serpClient;
|
|
34065
|
+
constructor(config) {
|
|
34066
|
+
this.supabase = config.supabase;
|
|
34067
|
+
this.serpClient = new SerpClient(config.serpConfig);
|
|
34068
|
+
}
|
|
34069
|
+
/**
|
|
34070
|
+
* Add keywords to track for a project
|
|
34071
|
+
*/
|
|
34072
|
+
async addKeywords(projectId, keywords, options) {
|
|
34073
|
+
const keywordRecords = keywords.map((keyword) => ({
|
|
34074
|
+
project_id: projectId,
|
|
34075
|
+
keyword: keyword.toLowerCase().trim(),
|
|
34076
|
+
search_engine: options?.searchEngine || "google",
|
|
34077
|
+
country: options?.country || "US",
|
|
34078
|
+
language: options?.language || "en",
|
|
34079
|
+
track_url: options?.trackUrl,
|
|
34080
|
+
is_active: true
|
|
34081
|
+
}));
|
|
34082
|
+
const { data, error } = await this.supabase.from("keywords").upsert(keywordRecords, {
|
|
34083
|
+
onConflict: "project_id,keyword",
|
|
34084
|
+
ignoreDuplicates: false
|
|
34085
|
+
}).select();
|
|
34086
|
+
if (error) {
|
|
34087
|
+
throw new Error(`Failed to add keywords: ${error.message}`);
|
|
34088
|
+
}
|
|
34089
|
+
return (data || []).map(this.mapKeyword);
|
|
34090
|
+
}
|
|
34091
|
+
/**
|
|
34092
|
+
* Remove keywords from tracking
|
|
34093
|
+
*/
|
|
34094
|
+
async removeKeywords(projectId, keywords) {
|
|
34095
|
+
const normalizedKeywords = keywords.map((k) => k.toLowerCase().trim());
|
|
34096
|
+
const { error } = await this.supabase.from("keywords").update({ is_active: false }).eq("project_id", projectId).in("keyword", normalizedKeywords);
|
|
34097
|
+
if (error) {
|
|
34098
|
+
throw new Error(`Failed to remove keywords: ${error.message}`);
|
|
34099
|
+
}
|
|
34100
|
+
}
|
|
34101
|
+
/**
|
|
34102
|
+
* Get all tracked keywords for a project
|
|
34103
|
+
*/
|
|
34104
|
+
async getKeywords(projectId, includeInactive = false) {
|
|
34105
|
+
let query = this.supabase.from("keywords").select("*").eq("project_id", projectId);
|
|
34106
|
+
if (!includeInactive) {
|
|
34107
|
+
query = query.eq("is_active", true);
|
|
34108
|
+
}
|
|
34109
|
+
const { data, error } = await query;
|
|
34110
|
+
if (error) {
|
|
34111
|
+
throw new Error(`Failed to get keywords: ${error.message}`);
|
|
34112
|
+
}
|
|
34113
|
+
return (data || []).map(this.mapKeyword);
|
|
34114
|
+
}
|
|
34115
|
+
/**
|
|
34116
|
+
* Check rankings for all active keywords in a project
|
|
34117
|
+
*/
|
|
34118
|
+
async checkRankings(projectId, domain) {
|
|
34119
|
+
const keywords = await this.getKeywords(projectId);
|
|
34120
|
+
if (keywords.length === 0) {
|
|
34121
|
+
return [];
|
|
34122
|
+
}
|
|
34123
|
+
const groups = this.groupKeywords(keywords);
|
|
34124
|
+
const results = [];
|
|
34125
|
+
for (const group of groups) {
|
|
34126
|
+
const checkResults = await this.serpClient.checkRank({
|
|
34127
|
+
keywords: group.keywords.map((k) => k.keyword),
|
|
34128
|
+
domain,
|
|
34129
|
+
searchEngine: group.searchEngine,
|
|
34130
|
+
country: group.country,
|
|
34131
|
+
language: group.language
|
|
34132
|
+
});
|
|
34133
|
+
for (const result of checkResults) {
|
|
34134
|
+
const keyword = group.keywords.find((k) => k.keyword === result.keyword);
|
|
34135
|
+
if (!keyword) continue;
|
|
34136
|
+
await this.saveRanking(keyword.id, result);
|
|
34137
|
+
results.push({
|
|
34138
|
+
keywordId: keyword.id,
|
|
34139
|
+
keyword: result.keyword,
|
|
34140
|
+
position: result.position,
|
|
34141
|
+
url: result.url,
|
|
34142
|
+
serpFeatures: result.serpFeatures,
|
|
34143
|
+
competitorUrls: result.topResults,
|
|
34144
|
+
checkedAt: result.checkedAt
|
|
34145
|
+
});
|
|
34146
|
+
}
|
|
34147
|
+
}
|
|
34148
|
+
await this.supabase.from("projects").update({ last_rank_check_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", projectId);
|
|
34149
|
+
return results;
|
|
34150
|
+
}
|
|
34151
|
+
/**
|
|
34152
|
+
* Save a ranking result to the database
|
|
34153
|
+
*/
|
|
34154
|
+
async saveRanking(keywordId, result) {
|
|
34155
|
+
const { data: currentKeyword } = await this.supabase.from("keywords").select("current_position, best_position").eq("id", keywordId).single();
|
|
34156
|
+
const previousPosition = currentKeyword?.current_position;
|
|
34157
|
+
const bestPosition = currentKeyword?.best_position;
|
|
34158
|
+
const newBestPosition = result.position !== null && (bestPosition === null || result.position < bestPosition) ? result.position : bestPosition;
|
|
34159
|
+
const { error: updateError } = await this.supabase.from("keywords").update({
|
|
34160
|
+
previous_position: previousPosition,
|
|
34161
|
+
current_position: result.position,
|
|
34162
|
+
best_position: newBestPosition,
|
|
34163
|
+
last_checked: result.checkedAt.toISOString()
|
|
34164
|
+
}).eq("id", keywordId);
|
|
34165
|
+
if (updateError) {
|
|
34166
|
+
console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
|
|
34167
|
+
}
|
|
34168
|
+
}
|
|
34169
|
+
/**
|
|
34170
|
+
* Get ranking history for a keyword
|
|
34171
|
+
* Note: Full history requires keyword_ranking_history table with keyword_ranking_id
|
|
34172
|
+
* For now, returns current state as single history entry
|
|
34173
|
+
*/
|
|
34174
|
+
async getHistory(keywordId, _days = 30) {
|
|
34175
|
+
const { data, error } = await this.supabase.from("keywords").select("current_position, target_url, last_checked").eq("id", keywordId).single();
|
|
34176
|
+
if (error || !data) {
|
|
34177
|
+
return [];
|
|
34178
|
+
}
|
|
34179
|
+
return [{
|
|
34180
|
+
position: data.current_position,
|
|
34181
|
+
url: data.target_url,
|
|
34182
|
+
serpFeatures: [],
|
|
34183
|
+
recordedAt: data.last_checked ? new Date(data.last_checked) : /* @__PURE__ */ new Date()
|
|
34184
|
+
}];
|
|
34185
|
+
}
|
|
34186
|
+
/**
|
|
34187
|
+
* Get keyword trends for a project
|
|
34188
|
+
*/
|
|
34189
|
+
async getTrends(projectId) {
|
|
34190
|
+
const { data, error } = await this.supabase.from("keyword_rank_trends").select("*").eq("project_id", projectId);
|
|
34191
|
+
if (error) {
|
|
34192
|
+
return this.calculateTrends(projectId);
|
|
34193
|
+
}
|
|
34194
|
+
return (data || []).map((row) => ({
|
|
34195
|
+
keywordId: row.keyword_id,
|
|
34196
|
+
keyword: row.keyword,
|
|
34197
|
+
currentPosition: row.current_position,
|
|
34198
|
+
bestPosition: row.best_position,
|
|
34199
|
+
positionChange: row.position_change || 0,
|
|
34200
|
+
avgPosition: row.avg_position || 0,
|
|
34201
|
+
dataPoints: row.data_points || 0
|
|
34202
|
+
}));
|
|
34203
|
+
}
|
|
34204
|
+
/**
|
|
34205
|
+
* Manual trend calculation fallback
|
|
34206
|
+
*/
|
|
34207
|
+
async calculateTrends(projectId) {
|
|
34208
|
+
const keywords = await this.getKeywords(projectId);
|
|
34209
|
+
const trends = [];
|
|
34210
|
+
for (const keyword of keywords) {
|
|
34211
|
+
const history = await this.getHistory(keyword.id, 30);
|
|
34212
|
+
if (history.length === 0) {
|
|
34213
|
+
trends.push({
|
|
34214
|
+
keywordId: keyword.id,
|
|
34215
|
+
keyword: keyword.keyword,
|
|
34216
|
+
currentPosition: keyword.currentPosition,
|
|
34217
|
+
bestPosition: keyword.bestPosition,
|
|
34218
|
+
positionChange: 0,
|
|
34219
|
+
avgPosition: keyword.currentPosition || 0,
|
|
34220
|
+
dataPoints: 0
|
|
34221
|
+
});
|
|
34222
|
+
continue;
|
|
34223
|
+
}
|
|
34224
|
+
const positions = history.filter((h) => h.position !== null).map((h) => h.position);
|
|
34225
|
+
const avgPosition = positions.length > 0 ? positions.reduce((a, b) => a + b, 0) / positions.length : 0;
|
|
34226
|
+
const positionChange = keyword.previousPosition && keyword.currentPosition ? keyword.previousPosition - keyword.currentPosition : 0;
|
|
34227
|
+
trends.push({
|
|
34228
|
+
keywordId: keyword.id,
|
|
34229
|
+
keyword: keyword.keyword,
|
|
34230
|
+
currentPosition: keyword.currentPosition,
|
|
34231
|
+
bestPosition: keyword.bestPosition,
|
|
34232
|
+
positionChange,
|
|
34233
|
+
avgPosition,
|
|
34234
|
+
dataPoints: history.length
|
|
34235
|
+
});
|
|
34236
|
+
}
|
|
34237
|
+
return trends;
|
|
34238
|
+
}
|
|
34239
|
+
/**
|
|
34240
|
+
* Export ranking data as CSV
|
|
34241
|
+
*/
|
|
34242
|
+
async exportCSV(projectId, days = 30) {
|
|
34243
|
+
const keywords = await this.getKeywords(projectId);
|
|
34244
|
+
const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
|
|
34245
|
+
for (const keyword of keywords) {
|
|
34246
|
+
const history = await this.getHistory(keyword.id, days);
|
|
34247
|
+
const trend = this.calculatePositionTrend(history);
|
|
34248
|
+
rows.push([
|
|
34249
|
+
`"${keyword.keyword}"`,
|
|
34250
|
+
keyword.currentPosition?.toString() || "N/A",
|
|
34251
|
+
keyword.bestPosition?.toString() || "N/A",
|
|
34252
|
+
keyword.lastChecked?.toISOString() || "Never",
|
|
34253
|
+
trend
|
|
34254
|
+
].join(","));
|
|
34255
|
+
}
|
|
34256
|
+
return rows.join("\n");
|
|
34257
|
+
}
|
|
34258
|
+
/**
|
|
34259
|
+
* Calculate position trend from history
|
|
34260
|
+
*/
|
|
34261
|
+
calculatePositionTrend(history) {
|
|
34262
|
+
if (history.length < 2) return "\u2192";
|
|
34263
|
+
const recent = history[0]?.position;
|
|
34264
|
+
const older = history[history.length - 1]?.position;
|
|
34265
|
+
if (recent === null || older === null) return "\u2192";
|
|
34266
|
+
if (recent < older) return "\u2191";
|
|
34267
|
+
if (recent > older) return "\u2193";
|
|
34268
|
+
return "\u2192";
|
|
34269
|
+
}
|
|
34270
|
+
/**
|
|
34271
|
+
* Group keywords by search engine and country
|
|
34272
|
+
*/
|
|
34273
|
+
groupKeywords(keywords) {
|
|
34274
|
+
const groups = /* @__PURE__ */ new Map();
|
|
34275
|
+
for (const keyword of keywords) {
|
|
34276
|
+
const key = `${keyword.searchEngine}:${keyword.country}:${keyword.language}`;
|
|
34277
|
+
if (!groups.has(key)) {
|
|
34278
|
+
groups.set(key, []);
|
|
34279
|
+
}
|
|
34280
|
+
groups.get(key).push(keyword);
|
|
34281
|
+
}
|
|
34282
|
+
return Array.from(groups.entries()).map(([key, keywords2]) => {
|
|
34283
|
+
const [searchEngine, country, language] = key.split(":");
|
|
34284
|
+
return {
|
|
34285
|
+
searchEngine,
|
|
34286
|
+
country,
|
|
34287
|
+
language,
|
|
34288
|
+
keywords: keywords2
|
|
34289
|
+
};
|
|
34290
|
+
});
|
|
34291
|
+
}
|
|
34292
|
+
/**
|
|
34293
|
+
* Map database row to TrackedKeyword
|
|
34294
|
+
*/
|
|
34295
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34296
|
+
mapKeyword(row) {
|
|
34297
|
+
return {
|
|
34298
|
+
id: row.id,
|
|
34299
|
+
projectId: row.project_id,
|
|
34300
|
+
keyword: row.keyword,
|
|
34301
|
+
searchEngine: row.search_engine || "google",
|
|
34302
|
+
country: row.country || "US",
|
|
34303
|
+
language: row.language || "en",
|
|
34304
|
+
currentPosition: row.current_position,
|
|
34305
|
+
previousPosition: row.previous_position,
|
|
34306
|
+
bestPosition: row.best_position,
|
|
34307
|
+
trackUrl: row.track_url,
|
|
34308
|
+
isActive: row.is_active,
|
|
34309
|
+
lastChecked: row.last_checked ? new Date(row.last_checked) : null,
|
|
34310
|
+
createdAt: new Date(row.created_at)
|
|
34311
|
+
};
|
|
34312
|
+
}
|
|
34313
|
+
};
|
|
34314
|
+
|
|
33758
34315
|
// src/analyzers/index.ts
|
|
33759
34316
|
var analyzers_exports = {};
|
|
33760
34317
|
__export(analyzers_exports, {
|
|
@@ -34937,9 +35494,12 @@ export {
|
|
|
34937
35494
|
LOCATION_CODES,
|
|
34938
35495
|
OG_IMAGE_SPECS,
|
|
34939
35496
|
PRIORITY_WEIGHTS,
|
|
35497
|
+
RankTracker,
|
|
34940
35498
|
SEO_SCOPES,
|
|
34941
35499
|
SITE_PROFILE_QUESTIONS,
|
|
34942
35500
|
Schemas,
|
|
35501
|
+
SerpClient,
|
|
35502
|
+
TIER_LIMITS,
|
|
34943
35503
|
addTrackingResult,
|
|
34944
35504
|
analyzeAnchorText,
|
|
34945
35505
|
analyzeCanonicalAdvanced,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankcli/agent-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
|
|
5
5
|
"homepage": "https://rankcli.dev",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
30
|
-
"
|
|
30
|
+
"@supabase/supabase-js": "^2.98.0",
|
|
31
|
+
"cheerio": "1.0.0-rc.12",
|
|
31
32
|
"openai": "^6.16.0",
|
|
32
33
|
"yaml": "^2.3.0"
|
|
33
34
|
},
|