superkit-mcp-server 1.0.1 → 1.0.2
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/ARCHITECTURE.md +2 -3
- package/README.md +1 -0
- package/build/index.js +75 -0
- package/build/tools/autoPreview.js +99 -0
- package/build/tools/checklist.js +120 -0
- package/build/tools/sessionManager.js +107 -0
- package/build/tools/validators/__tests__/apiSchema.test.js +77 -0
- package/build/tools/validators/__tests__/convertRules.test.js +38 -0
- package/build/tools/validators/__tests__/frontendDesign.test.js +55 -0
- package/build/tools/validators/__tests__/geoChecker.test.js +45 -0
- package/build/tools/validators/__tests__/i18nChecker.test.js +32 -0
- package/build/tools/validators/__tests__/lintRunner.test.js +65 -0
- package/build/tools/validators/__tests__/mobileAudit.test.js +40 -0
- package/build/tools/validators/__tests__/playwrightRunner.test.js +55 -0
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +49 -0
- package/build/tools/validators/__tests__/securityScan.test.js +42 -0
- package/build/tools/validators/__tests__/seoChecker.test.js +44 -0
- package/build/tools/validators/__tests__/testRunner.test.js +49 -0
- package/build/tools/validators/__tests__/typeCoverage.test.js +62 -0
- package/build/tools/validators/accessibilityChecker.js +124 -0
- package/build/tools/validators/apiValidator.js +140 -0
- package/build/tools/validators/convertRules.js +170 -0
- package/build/tools/validators/geoChecker.js +176 -0
- package/build/tools/validators/i18nChecker.js +205 -0
- package/build/tools/validators/lighthouseAudit.js +50 -0
- package/build/tools/validators/lintRunner.js +106 -0
- package/build/tools/validators/mobileAudit.js +190 -0
- package/build/tools/validators/playwrightRunner.js +101 -0
- package/build/tools/validators/reactPerformanceChecker.js +199 -0
- package/build/tools/validators/schemaValidator.js +105 -0
- package/build/tools/validators/securityScan.js +215 -0
- package/build/tools/validators/seoChecker.js +122 -0
- package/build/tools/validators/testRunner.js +111 -0
- package/build/tools/validators/typeCoverage.js +150 -0
- package/build/tools/validators/uxAudit.js +222 -0
- package/build/tools/verifyAll.js +159 -0
- package/package.json +5 -3
- package/skills/tech/api-patterns/SKILL.md +1 -1
- package/skills/tech/clean-code/SKILL.md +14 -14
- package/skills/tech/doc.md +3 -3
- package/skills/tech/frontend-design/SKILL.md +1 -1
- package/skills/tech/geo-fundamentals/SKILL.md +1 -1
- package/skills/tech/i18n-localization/SKILL.md +1 -1
- package/skills/tech/lint-and-validate/SKILL.md +2 -2
- package/skills/tech/mobile-design/SKILL.md +1 -1
- package/skills/tech/nextjs-react-expert/SKILL.md +1 -1
- package/skills/tech/parallel-agents/SKILL.md +3 -3
- package/skills/tech/performance-profiling/SKILL.md +1 -1
- package/skills/tech/vulnerability-scanner/SKILL.md +1 -1
- package/skills/tech/webapp-testing/SKILL.md +3 -3
- package/workflows/review-compound.md +1 -1
- package/skills/tech/api-patterns/scripts/api_validator.py +0 -211
- package/skills/tech/database-design/scripts/schema_validator.py +0 -172
- package/skills/tech/frontend-design/scripts/accessibility_checker.py +0 -183
- package/skills/tech/frontend-design/scripts/ux_audit.py +0 -722
- package/skills/tech/geo-fundamentals/scripts/geo_checker.py +0 -289
- package/skills/tech/i18n-localization/scripts/i18n_checker.py +0 -241
- package/skills/tech/lint-and-validate/scripts/lint_runner.py +0 -184
- package/skills/tech/lint-and-validate/scripts/type_coverage.py +0 -173
- package/skills/tech/mobile-design/scripts/mobile_audit.py +0 -670
- package/skills/tech/nextjs-react-expert/scripts/convert_rules.py +0 -222
- package/skills/tech/nextjs-react-expert/scripts/react_performance_checker.py +0 -252
- package/skills/tech/performance-profiling/scripts/lighthouse_audit.py +0 -76
- package/skills/tech/seo-fundamentals/scripts/seo_checker.py +0 -219
- package/skills/tech/testing-patterns/scripts/test_runner.py +0 -219
- package/skills/tech/vulnerability-scanner/scripts/security_scan.py +0 -458
- package/skills/tech/webapp-testing/scripts/playwright_runner.py +0 -173
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export async function findApiFiles(projectPath) {
|
|
4
|
+
let files = [];
|
|
5
|
+
// very basic matching
|
|
6
|
+
async function search(dir) {
|
|
7
|
+
try {
|
|
8
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
9
|
+
for (const item of items) {
|
|
10
|
+
if (['node_modules', '.git', 'dist', 'build', '__pycache__'].includes(item.name))
|
|
11
|
+
continue;
|
|
12
|
+
const fullPath = path.join(dir, item.name);
|
|
13
|
+
if (item.isDirectory()) {
|
|
14
|
+
await search(fullPath);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const name = item.name.toLowerCase();
|
|
18
|
+
if ((name.includes('api') || fullPath.includes('routes') || fullPath.includes('controllers') || fullPath.includes('endpoints')) &&
|
|
19
|
+
(name.endsWith('.ts') || name.endsWith('.js') || name.endsWith('.py'))) {
|
|
20
|
+
files.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
if (name.includes('openapi') || name.includes('swagger')) {
|
|
23
|
+
files.push(fullPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
await search(projectPath);
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
export async function checkOpenApiSpec(filePath) {
|
|
34
|
+
const result = { file: filePath, passed: [], issues: [], type: 'openapi' };
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
37
|
+
if (filePath.endsWith('.json')) {
|
|
38
|
+
const spec = JSON.parse(content);
|
|
39
|
+
if (spec.openapi || spec.swagger)
|
|
40
|
+
result.passed.push("[OK] OpenAPI version defined");
|
|
41
|
+
if (spec.info?.title)
|
|
42
|
+
result.passed.push("[OK] API title defined");
|
|
43
|
+
if (spec.info?.version)
|
|
44
|
+
result.passed.push("[OK] API version defined");
|
|
45
|
+
if (!spec.info?.description)
|
|
46
|
+
result.issues.push("[!] API description missing");
|
|
47
|
+
if (spec.paths) {
|
|
48
|
+
result.passed.push(`[OK] ${Object.keys(spec.paths).length} endpoints defined`);
|
|
49
|
+
for (const [p, methods] of Object.entries(spec.paths)) {
|
|
50
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
51
|
+
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
|
|
52
|
+
if (!details.responses)
|
|
53
|
+
result.issues.push(`[X] ${method.toUpperCase()} ${p}: No responses defined`);
|
|
54
|
+
if (!details.summary && !details.description)
|
|
55
|
+
result.issues.push(`[!] ${method.toUpperCase()} ${p}: No description`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Basic YAML
|
|
63
|
+
if (content.includes('openapi:') || content.includes('swagger:'))
|
|
64
|
+
result.passed.push("[OK] OpenAPI/Swagger version defined");
|
|
65
|
+
else
|
|
66
|
+
result.issues.push("[X] No OpenAPI version found");
|
|
67
|
+
if (content.includes('paths:'))
|
|
68
|
+
result.passed.push("[OK] Paths section exists");
|
|
69
|
+
else
|
|
70
|
+
result.issues.push("[X] No paths defined");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
result.issues.push(`[X] Parse error: ${e.message}`);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
export async function checkApiCode(filePath) {
|
|
79
|
+
const result = { file: filePath, passed: [], issues: [], type: 'code' };
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
82
|
+
// error handling
|
|
83
|
+
if (/try\s*{|try:|\.catch\(|except\s+|catch\s*\(/.test(content))
|
|
84
|
+
result.passed.push("[OK] Error handling present");
|
|
85
|
+
else
|
|
86
|
+
result.issues.push("[X] No error handling found");
|
|
87
|
+
// status codes
|
|
88
|
+
if (/status\s*\(\s*\d{3}\s*\)|statusCode\s*[=:]\s*\d{3}|HttpStatus\.|status_code\s*=\s*\d{3}/.test(content))
|
|
89
|
+
result.passed.push("[OK] HTTP status codes used");
|
|
90
|
+
else
|
|
91
|
+
result.issues.push("[!] No explicit HTTP status codes");
|
|
92
|
+
// validation
|
|
93
|
+
if (/validate|schema|zod|joi|yup|pydantic|@Body\(|@Query\(/i.test(content))
|
|
94
|
+
result.passed.push("[OK] Input validation present");
|
|
95
|
+
else
|
|
96
|
+
result.issues.push("[!] No input validation detected");
|
|
97
|
+
// auth
|
|
98
|
+
if (/auth|jwt|bearer|token|middleware|guard|@Authenticated/i.test(content))
|
|
99
|
+
result.passed.push("[OK] Authentication/authorization detected");
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
result.issues.push(`[X] Read error: ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
export async function runApiValidator(projectPath = ".") {
|
|
107
|
+
const root = path.resolve(projectPath);
|
|
108
|
+
const files = await findApiFiles(root);
|
|
109
|
+
let report = `============================================================\n`;
|
|
110
|
+
report += ` API VALIDATOR - Endpoint Best Practices Check\n`;
|
|
111
|
+
report += `============================================================\n`;
|
|
112
|
+
if (files.length === 0) {
|
|
113
|
+
return { passed: true, report: report + "[!] No API files found.\n" };
|
|
114
|
+
}
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const file of files.slice(0, 15)) {
|
|
117
|
+
if (file.toLowerCase().includes('openapi') || file.toLowerCase().includes('swagger')) {
|
|
118
|
+
results.push(await checkOpenApiSpec(file));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
results.push(await checkApiCode(file));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let totalIssues = 0, totalPassed = 0;
|
|
125
|
+
for (const r of results) {
|
|
126
|
+
report += `\n[FILE] ${path.basename(r.file)} [${r.type}]\n`;
|
|
127
|
+
for (const p of r.passed) {
|
|
128
|
+
report += ` ${p}\n`;
|
|
129
|
+
totalPassed++;
|
|
130
|
+
}
|
|
131
|
+
for (const i of r.issues) {
|
|
132
|
+
report += ` ${i}\n`;
|
|
133
|
+
if (i.startsWith("[X]"))
|
|
134
|
+
totalIssues++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
report += `\n============================================================\n`;
|
|
138
|
+
report += `[RESULTS] ${totalPassed} passed, ${totalIssues} critical issues\n`;
|
|
139
|
+
return { passed: totalIssues === 0, report };
|
|
140
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const SECTIONS = {
|
|
4
|
+
'async': {
|
|
5
|
+
'number': 1,
|
|
6
|
+
'title': 'Eliminating Waterfalls',
|
|
7
|
+
'impact': 'CRITICAL',
|
|
8
|
+
'description': 'Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.'
|
|
9
|
+
},
|
|
10
|
+
'bundle': {
|
|
11
|
+
'number': 2,
|
|
12
|
+
'title': 'Bundle Size Optimization',
|
|
13
|
+
'impact': 'CRITICAL',
|
|
14
|
+
'description': 'Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.'
|
|
15
|
+
},
|
|
16
|
+
'server': {
|
|
17
|
+
'number': 3,
|
|
18
|
+
'title': 'Server-Side Performance',
|
|
19
|
+
'impact': 'HIGH',
|
|
20
|
+
'description': 'Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.'
|
|
21
|
+
},
|
|
22
|
+
'client': {
|
|
23
|
+
'number': 4,
|
|
24
|
+
'title': 'Client-Side Data Fetching',
|
|
25
|
+
'impact': 'MEDIUM-HIGH',
|
|
26
|
+
'description': 'Automatic deduplication and efficient data fetching patterns reduce redundant network requests.'
|
|
27
|
+
},
|
|
28
|
+
'rerender': {
|
|
29
|
+
'number': 5,
|
|
30
|
+
'title': 'Re-render Optimization',
|
|
31
|
+
'impact': 'MEDIUM',
|
|
32
|
+
'description': 'Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.'
|
|
33
|
+
},
|
|
34
|
+
'rendering': {
|
|
35
|
+
'number': 6,
|
|
36
|
+
'title': 'Rendering Performance',
|
|
37
|
+
'impact': 'MEDIUM',
|
|
38
|
+
'description': 'Optimizing the rendering process reduces the work the browser needs to do.'
|
|
39
|
+
},
|
|
40
|
+
'js': {
|
|
41
|
+
'number': 7,
|
|
42
|
+
'title': 'JavaScript Performance',
|
|
43
|
+
'impact': 'LOW-MEDIUM',
|
|
44
|
+
'description': 'Micro-optimizations for hot paths can add up to meaningful improvements.'
|
|
45
|
+
},
|
|
46
|
+
'advanced': {
|
|
47
|
+
'number': 8,
|
|
48
|
+
'title': 'Advanced Patterns',
|
|
49
|
+
'impact': 'VARIABLE',
|
|
50
|
+
'description': 'Advanced patterns for specific cases that require careful implementation.'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function parseFrontmatter(content) {
|
|
54
|
+
if (!content.startsWith('---'))
|
|
55
|
+
return { frontmatter: {}, body: content };
|
|
56
|
+
const parts = content.split('---');
|
|
57
|
+
if (parts.length < 3)
|
|
58
|
+
return { frontmatter: {}, body: content };
|
|
59
|
+
const frontmatterStr = parts[1].trim();
|
|
60
|
+
const body = parts.slice(2).join('---').trim();
|
|
61
|
+
const frontmatter = {};
|
|
62
|
+
for (const line of frontmatterStr.split('\n')) {
|
|
63
|
+
const colonIdx = line.indexOf(':');
|
|
64
|
+
if (colonIdx !== -1) {
|
|
65
|
+
const key = line.substring(0, colonIdx).trim();
|
|
66
|
+
const value = line.substring(colonIdx + 1).trim();
|
|
67
|
+
frontmatter[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { frontmatter, body };
|
|
71
|
+
}
|
|
72
|
+
async function parseRuleFile(filepath) {
|
|
73
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
74
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
75
|
+
const filename = path.basename(filepath);
|
|
76
|
+
const prefix = path.parse(filename).name.split('-')[0];
|
|
77
|
+
return {
|
|
78
|
+
filename,
|
|
79
|
+
prefix,
|
|
80
|
+
title: frontmatter['title'] || path.parse(filename).name,
|
|
81
|
+
impact: frontmatter['impact'] || '',
|
|
82
|
+
impactDescription: frontmatter['impactDescription'] || '',
|
|
83
|
+
tags: frontmatter['tags'] || '',
|
|
84
|
+
body,
|
|
85
|
+
frontmatter
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function groupRulesBySection(rulesDir) {
|
|
89
|
+
const grouped = {};
|
|
90
|
+
for (const key of Object.keys(SECTIONS))
|
|
91
|
+
grouped[key] = [];
|
|
92
|
+
try {
|
|
93
|
+
const files = await fs.readdir(rulesDir);
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
if (file.startsWith('_') || !file.endsWith('.md'))
|
|
96
|
+
continue;
|
|
97
|
+
const rule = await parseRuleFile(path.join(rulesDir, file));
|
|
98
|
+
if (grouped[rule.prefix]) {
|
|
99
|
+
grouped[rule.prefix].push(rule);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.warn(`[WARNING] Unknown prefix '${rule.prefix}' in file: ${file}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
throw new Error(`Failed to read rules directory: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
return grouped;
|
|
110
|
+
}
|
|
111
|
+
async function generateSectionFile(sectionPrefix, rules, outputDir, getOutputOnly = false) {
|
|
112
|
+
if (!rules || rules.length === 0)
|
|
113
|
+
return "";
|
|
114
|
+
const sectionMeta = SECTIONS[sectionPrefix];
|
|
115
|
+
const sectionNum = sectionMeta.number;
|
|
116
|
+
const sectionTitle = sectionMeta.title;
|
|
117
|
+
rules.sort((a, b) => a.title.localeCompare(b.title));
|
|
118
|
+
let content = `# ${sectionNum}. ${sectionTitle}\n\n> **Impact:** ${sectionMeta.impact}\n> **Focus:** ${sectionMeta.description}\n\n---\n\n## Overview\n\nThis section contains **${rules.length} rules** focused on ${sectionTitle.toLowerCase()}.\n\n`;
|
|
119
|
+
for (let i = 0; i < rules.length; i++) {
|
|
120
|
+
const rule = rules[i];
|
|
121
|
+
const ruleId = `${sectionNum}.${i + 1}`;
|
|
122
|
+
content += `---\n\n## Rule ${ruleId}: ${rule.title}\n\n`;
|
|
123
|
+
if (rule.impact)
|
|
124
|
+
content += `**Impact:** ${rule.impact} \n`;
|
|
125
|
+
if (rule.tags)
|
|
126
|
+
content += `**Tags:** ${rule.tags} \n`;
|
|
127
|
+
content += `\n${rule.body}\n\n`;
|
|
128
|
+
}
|
|
129
|
+
if (getOutputOnly)
|
|
130
|
+
return content;
|
|
131
|
+
const outputFilename = `${sectionNum}-${sectionPrefix}-${sectionTitle.toLowerCase().replace(/\s+/g, '-')}.md`;
|
|
132
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
133
|
+
await fs.writeFile(path.join(outputDir, outputFilename), content, 'utf-8');
|
|
134
|
+
return `[OK] Generated: ${outputFilename} (${rules.length} rules)\n`;
|
|
135
|
+
}
|
|
136
|
+
export async function runConvertRules(projectPath = ".") {
|
|
137
|
+
let report = `============================================================\n`;
|
|
138
|
+
report += `CONVERSION SCRIPT: React Best Practices -> .agent Format\n`;
|
|
139
|
+
report += `============================================================\n`;
|
|
140
|
+
const baseDir = path.resolve(projectPath);
|
|
141
|
+
// Mimic the python logic for paths if run from anywhere, or adapt to superkit structure
|
|
142
|
+
const rulesDir = path.join(baseDir, "skills", "react-best-practices", "rules");
|
|
143
|
+
const outputDir = path.join(baseDir, ".agent", "skills", "react-best-practices");
|
|
144
|
+
report += `[*] Reading rules from: ${rulesDir}\n[*] Output to: ${outputDir}\n\n`;
|
|
145
|
+
try {
|
|
146
|
+
const stat = await fs.stat(rulesDir);
|
|
147
|
+
if (!stat.isDirectory())
|
|
148
|
+
throw new Error("Not a directory");
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { passed: false, report: report + `[ERROR] Rules directory not found: ${rulesDir}\n` };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const groupedRules = await groupRulesBySection(rulesDir);
|
|
155
|
+
let totalRules = 0;
|
|
156
|
+
for (const rules of Object.values(groupedRules))
|
|
157
|
+
totalRules += rules.length;
|
|
158
|
+
report += `[*] Found ${totalRules} total rules\n\n[*] Generating section files...\n`;
|
|
159
|
+
for (const prefix of Object.keys(SECTIONS)) {
|
|
160
|
+
const out = await generateSectionFile(prefix, groupedRules[prefix], outputDir);
|
|
161
|
+
if (out)
|
|
162
|
+
report += out;
|
|
163
|
+
}
|
|
164
|
+
report += `\n[SUCCESS] Conversion complete!\n[*] Generated 8 section files from ${totalRules} rules\n`;
|
|
165
|
+
return { passed: true, report };
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return { passed: false, report: report + `[ERROR] ${e.message}\n` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const SKIP_DIRS = new Set([
|
|
4
|
+
'node_modules', '.next', 'dist', 'build', '.git', '.github',
|
|
5
|
+
'__pycache__', '.vscode', '.idea', 'coverage', 'test', 'tests',
|
|
6
|
+
'__tests__', 'spec', 'docs', 'documentation'
|
|
7
|
+
]);
|
|
8
|
+
const SKIP_FILES = new Set([
|
|
9
|
+
'jest.config', 'webpack.config', 'vite.config', 'tsconfig',
|
|
10
|
+
'package.json', 'package-lock', 'yarn.lock', '.eslintrc',
|
|
11
|
+
'tailwind.config', 'postcss.config', 'next.config'
|
|
12
|
+
]);
|
|
13
|
+
function isPageFile(filePath) {
|
|
14
|
+
const name = path.basename(filePath).toLowerCase();
|
|
15
|
+
for (const skip of SKIP_FILES) {
|
|
16
|
+
if (name.includes(skip))
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (name.endsWith('.test') || name.endsWith('.spec') || name.startsWith('test_') || name.startsWith('spec_')) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const pageIndicators = ['page', 'index', 'home', 'about', 'contact', 'blog', 'post', 'article', 'product', 'service', 'landing'];
|
|
23
|
+
const parts = filePath.toLowerCase().split(path.sep);
|
|
24
|
+
if (parts.includes('pages') || parts.includes('app') || parts.includes('routes'))
|
|
25
|
+
return true;
|
|
26
|
+
for (const ind of pageIndicators) {
|
|
27
|
+
if (name.includes(ind))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (name.endsWith('.html'))
|
|
31
|
+
return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
export async function findWebPages(projectPath) {
|
|
35
|
+
let files = [];
|
|
36
|
+
async function search(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (SKIP_DIRS.has(item.name))
|
|
41
|
+
continue;
|
|
42
|
+
const fullPath = path.join(dir, item.name);
|
|
43
|
+
if (item.isDirectory())
|
|
44
|
+
await search(fullPath);
|
|
45
|
+
else {
|
|
46
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
47
|
+
if (['.html', '.htm', '.jsx', '.tsx'].includes(ext)) {
|
|
48
|
+
if (isPageFile(fullPath))
|
|
49
|
+
files.push(fullPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
await search(projectPath);
|
|
57
|
+
return files.slice(0, 30);
|
|
58
|
+
}
|
|
59
|
+
export async function checkGeoPage(filePath) {
|
|
60
|
+
const result = { file: path.basename(filePath), passed: [], issues: [], score: 0 };
|
|
61
|
+
try {
|
|
62
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
63
|
+
// JSON-LD
|
|
64
|
+
if (content.includes('application/ld+json')) {
|
|
65
|
+
result.passed.push("JSON-LD structured data found");
|
|
66
|
+
if (content.includes('"@type"')) {
|
|
67
|
+
if (content.includes('Article'))
|
|
68
|
+
result.passed.push("Article schema present");
|
|
69
|
+
if (content.includes('FAQPage'))
|
|
70
|
+
result.passed.push("FAQ schema present");
|
|
71
|
+
if (content.includes('Organization') || content.includes('Person'))
|
|
72
|
+
result.passed.push("Entity schema present");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
result.issues.push("No JSON-LD structured data (AI engines prefer structured content)");
|
|
77
|
+
}
|
|
78
|
+
// Headings
|
|
79
|
+
const h1Count = (content.match(/<h1[^>]*>/gi) || []).length;
|
|
80
|
+
const h2Count = (content.match(/<h2[^>]*>/gi) || []).length;
|
|
81
|
+
if (h1Count === 1)
|
|
82
|
+
result.passed.push("Single H1 heading (clear topic)");
|
|
83
|
+
else if (h1Count === 0)
|
|
84
|
+
result.issues.push("No H1 heading - page topic unclear");
|
|
85
|
+
else
|
|
86
|
+
result.issues.push(`Multiple H1 headings (${h1Count}) - confusing for AI`);
|
|
87
|
+
if (h2Count >= 2)
|
|
88
|
+
result.passed.push(`${h2Count} H2 subheadings (good structure)`);
|
|
89
|
+
else
|
|
90
|
+
result.issues.push("Add more H2 subheadings for scannable content");
|
|
91
|
+
// Author
|
|
92
|
+
const lowerC = content.toLowerCase();
|
|
93
|
+
if (['author', 'byline', 'written-by', 'contributor', 'rel="author"'].some(p => lowerC.includes(p))) {
|
|
94
|
+
result.passed.push("Author attribution found");
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
result.issues.push("No author info (AI prefers attributed content)");
|
|
98
|
+
// Date
|
|
99
|
+
if (/datePublished|dateModified|datetime=|pubdate|article:published/i.test(content)) {
|
|
100
|
+
result.passed.push("Publication date found");
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
result.issues.push("No publication date (freshness matters for AI)");
|
|
104
|
+
// FAQ
|
|
105
|
+
if (/<details|faq|frequently.?asked|"FAQPage"/i.test(content)) {
|
|
106
|
+
result.passed.push("FAQ section detected (highly citable)");
|
|
107
|
+
}
|
|
108
|
+
const listCount = (content.match(/<(ul|ol)[^>]*>/gi) || []).length;
|
|
109
|
+
if (listCount >= 2)
|
|
110
|
+
result.passed.push(`${listCount} lists (structured content)`);
|
|
111
|
+
const tableCount = (content.match(/<table[^>]*>/gi) || []).length;
|
|
112
|
+
if (tableCount >= 1)
|
|
113
|
+
result.passed.push(`${tableCount} table(s) (comparison data)`);
|
|
114
|
+
// Entities
|
|
115
|
+
if (/"@type"\s*:\s*"Organization"|"@type"\s*:\s*"LocalBusiness"|"@type"\s*:\s*"Brand"|itemtype.*schema\.org\/(Organization|Person|Brand)|rel="author"/i.test(content)) {
|
|
116
|
+
result.passed.push("Entity/Brand recognition (E-E-A-T)");
|
|
117
|
+
}
|
|
118
|
+
// Stats
|
|
119
|
+
let stats = 0;
|
|
120
|
+
const statPatterns = [/\d+%/, /\$[\d,]+/, /study\s+(shows|found)/i, /according to/i, /data\s+(shows|reveals)/i, /\d+x\s+(faster|better|more)/i, /(million|billion|trillion)/i];
|
|
121
|
+
for (const p of statPatterns)
|
|
122
|
+
if (p.test(content))
|
|
123
|
+
stats++;
|
|
124
|
+
if (stats >= 2)
|
|
125
|
+
result.passed.push("Original statistics/data (citation magnet)");
|
|
126
|
+
// Direct answers
|
|
127
|
+
const ansPatterns = [/is defined as/i, /refers to/i, /means that/i, /the answer is/i, /in short,/i, /simply put,/i, /<dfn/i];
|
|
128
|
+
if (ansPatterns.some(p => p.test(content)))
|
|
129
|
+
result.passed.push("Direct answer patterns (LLM-friendly)");
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
result.issues.push(`Error: ${e.message}`);
|
|
133
|
+
}
|
|
134
|
+
const total = result.passed.length + result.issues.length;
|
|
135
|
+
result.score = total > 0 ? (result.passed.length / total) * 100 : 0;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
export async function runGeoChecker(projectPath = ".") {
|
|
139
|
+
const root = path.resolve(projectPath);
|
|
140
|
+
let report = `============================================================\n`;
|
|
141
|
+
report += ` GEO CHECKER - AI Citation Readiness Audit\n`;
|
|
142
|
+
report += `============================================================\n`;
|
|
143
|
+
const pages = await findWebPages(root);
|
|
144
|
+
if (pages.length === 0) {
|
|
145
|
+
return { passed: true, report: report + "\n[!] No public web pages found.\n" };
|
|
146
|
+
}
|
|
147
|
+
report += `Found ${pages.length} public pages to analyze\n\n`;
|
|
148
|
+
const results = [];
|
|
149
|
+
for (const page of pages) {
|
|
150
|
+
results.push(await checkGeoPage(page));
|
|
151
|
+
}
|
|
152
|
+
let sumScore = 0;
|
|
153
|
+
for (const r of results) {
|
|
154
|
+
const score = Math.round(r.score);
|
|
155
|
+
sumScore += score;
|
|
156
|
+
const status = score >= 60 ? "[OK]" : "[!]";
|
|
157
|
+
report += `${status} ${r.file}: ${score}%\n`;
|
|
158
|
+
if (r.issues.length > 0 && score < 60) {
|
|
159
|
+
for (const issue of r.issues.slice(0, 2))
|
|
160
|
+
report += ` - ${issue}\n`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const avgScore = Math.round(results.length > 0 ? sumScore / results.length : 0);
|
|
164
|
+
report += `\n============================================================\n`;
|
|
165
|
+
report += `AVERAGE GEO SCORE: ${avgScore}%\n`;
|
|
166
|
+
report += `============================================================\n`;
|
|
167
|
+
if (avgScore >= 80)
|
|
168
|
+
report += "[OK] Excellent - Content well-optimized for AI citations\n";
|
|
169
|
+
else if (avgScore >= 60)
|
|
170
|
+
report += "[OK] Good - Some improvements recommended\n";
|
|
171
|
+
else if (avgScore >= 40)
|
|
172
|
+
report += "[!] Needs work - Add structured elements\n";
|
|
173
|
+
else
|
|
174
|
+
report += "[X] Poor - Content needs GEO optimization\n";
|
|
175
|
+
return { passed: avgScore >= 60, report };
|
|
176
|
+
}
|