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.
Files changed (67) hide show
  1. package/ARCHITECTURE.md +2 -3
  2. package/README.md +1 -0
  3. package/build/index.js +75 -0
  4. package/build/tools/autoPreview.js +99 -0
  5. package/build/tools/checklist.js +120 -0
  6. package/build/tools/sessionManager.js +107 -0
  7. package/build/tools/validators/__tests__/apiSchema.test.js +77 -0
  8. package/build/tools/validators/__tests__/convertRules.test.js +38 -0
  9. package/build/tools/validators/__tests__/frontendDesign.test.js +55 -0
  10. package/build/tools/validators/__tests__/geoChecker.test.js +45 -0
  11. package/build/tools/validators/__tests__/i18nChecker.test.js +32 -0
  12. package/build/tools/validators/__tests__/lintRunner.test.js +65 -0
  13. package/build/tools/validators/__tests__/mobileAudit.test.js +40 -0
  14. package/build/tools/validators/__tests__/playwrightRunner.test.js +55 -0
  15. package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +49 -0
  16. package/build/tools/validators/__tests__/securityScan.test.js +42 -0
  17. package/build/tools/validators/__tests__/seoChecker.test.js +44 -0
  18. package/build/tools/validators/__tests__/testRunner.test.js +49 -0
  19. package/build/tools/validators/__tests__/typeCoverage.test.js +62 -0
  20. package/build/tools/validators/accessibilityChecker.js +124 -0
  21. package/build/tools/validators/apiValidator.js +140 -0
  22. package/build/tools/validators/convertRules.js +170 -0
  23. package/build/tools/validators/geoChecker.js +176 -0
  24. package/build/tools/validators/i18nChecker.js +205 -0
  25. package/build/tools/validators/lighthouseAudit.js +50 -0
  26. package/build/tools/validators/lintRunner.js +106 -0
  27. package/build/tools/validators/mobileAudit.js +190 -0
  28. package/build/tools/validators/playwrightRunner.js +101 -0
  29. package/build/tools/validators/reactPerformanceChecker.js +199 -0
  30. package/build/tools/validators/schemaValidator.js +105 -0
  31. package/build/tools/validators/securityScan.js +215 -0
  32. package/build/tools/validators/seoChecker.js +122 -0
  33. package/build/tools/validators/testRunner.js +111 -0
  34. package/build/tools/validators/typeCoverage.js +150 -0
  35. package/build/tools/validators/uxAudit.js +222 -0
  36. package/build/tools/verifyAll.js +159 -0
  37. package/package.json +5 -3
  38. package/skills/tech/api-patterns/SKILL.md +1 -1
  39. package/skills/tech/clean-code/SKILL.md +14 -14
  40. package/skills/tech/doc.md +3 -3
  41. package/skills/tech/frontend-design/SKILL.md +1 -1
  42. package/skills/tech/geo-fundamentals/SKILL.md +1 -1
  43. package/skills/tech/i18n-localization/SKILL.md +1 -1
  44. package/skills/tech/lint-and-validate/SKILL.md +2 -2
  45. package/skills/tech/mobile-design/SKILL.md +1 -1
  46. package/skills/tech/nextjs-react-expert/SKILL.md +1 -1
  47. package/skills/tech/parallel-agents/SKILL.md +3 -3
  48. package/skills/tech/performance-profiling/SKILL.md +1 -1
  49. package/skills/tech/vulnerability-scanner/SKILL.md +1 -1
  50. package/skills/tech/webapp-testing/SKILL.md +3 -3
  51. package/workflows/review-compound.md +1 -1
  52. package/skills/tech/api-patterns/scripts/api_validator.py +0 -211
  53. package/skills/tech/database-design/scripts/schema_validator.py +0 -172
  54. package/skills/tech/frontend-design/scripts/accessibility_checker.py +0 -183
  55. package/skills/tech/frontend-design/scripts/ux_audit.py +0 -722
  56. package/skills/tech/geo-fundamentals/scripts/geo_checker.py +0 -289
  57. package/skills/tech/i18n-localization/scripts/i18n_checker.py +0 -241
  58. package/skills/tech/lint-and-validate/scripts/lint_runner.py +0 -184
  59. package/skills/tech/lint-and-validate/scripts/type_coverage.py +0 -173
  60. package/skills/tech/mobile-design/scripts/mobile_audit.py +0 -670
  61. package/skills/tech/nextjs-react-expert/scripts/convert_rules.py +0 -222
  62. package/skills/tech/nextjs-react-expert/scripts/react_performance_checker.py +0 -252
  63. package/skills/tech/performance-profiling/scripts/lighthouse_audit.py +0 -76
  64. package/skills/tech/seo-fundamentals/scripts/seo_checker.py +0 -219
  65. package/skills/tech/testing-patterns/scripts/test_runner.py +0 -219
  66. package/skills/tech/vulnerability-scanner/scripts/security_scan.py +0 -458
  67. 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
+ }