recker 1.0.31 → 1.0.32-next.e0741bf

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 (46) hide show
  1. package/dist/cli/index.js +2350 -43
  2. package/dist/cli/tui/shell-search.js +10 -8
  3. package/dist/cli/tui/shell.d.ts +29 -0
  4. package/dist/cli/tui/shell.js +1733 -9
  5. package/dist/mcp/search/hybrid-search.js +4 -2
  6. package/dist/seo/analyzer.d.ts +7 -0
  7. package/dist/seo/analyzer.js +200 -4
  8. package/dist/seo/rules/ai-search.d.ts +2 -0
  9. package/dist/seo/rules/ai-search.js +423 -0
  10. package/dist/seo/rules/canonical.d.ts +12 -0
  11. package/dist/seo/rules/canonical.js +249 -0
  12. package/dist/seo/rules/crawl.js +113 -0
  13. package/dist/seo/rules/cwv.js +0 -95
  14. package/dist/seo/rules/i18n.js +27 -0
  15. package/dist/seo/rules/images.js +23 -27
  16. package/dist/seo/rules/index.js +14 -0
  17. package/dist/seo/rules/internal-linking.js +6 -6
  18. package/dist/seo/rules/links.js +321 -0
  19. package/dist/seo/rules/meta.js +24 -0
  20. package/dist/seo/rules/mobile.js +0 -20
  21. package/dist/seo/rules/performance.js +124 -0
  22. package/dist/seo/rules/redirects.d.ts +16 -0
  23. package/dist/seo/rules/redirects.js +193 -0
  24. package/dist/seo/rules/resources.d.ts +2 -0
  25. package/dist/seo/rules/resources.js +373 -0
  26. package/dist/seo/rules/security.js +290 -0
  27. package/dist/seo/rules/technical-advanced.d.ts +10 -0
  28. package/dist/seo/rules/technical-advanced.js +283 -0
  29. package/dist/seo/rules/technical.js +74 -18
  30. package/dist/seo/rules/types.d.ts +103 -3
  31. package/dist/seo/seo-spider.d.ts +2 -0
  32. package/dist/seo/seo-spider.js +47 -2
  33. package/dist/seo/types.d.ts +48 -28
  34. package/dist/seo/utils/index.d.ts +1 -0
  35. package/dist/seo/utils/index.js +1 -0
  36. package/dist/seo/utils/similarity.d.ts +47 -0
  37. package/dist/seo/utils/similarity.js +273 -0
  38. package/dist/seo/validators/index.d.ts +3 -0
  39. package/dist/seo/validators/index.js +3 -0
  40. package/dist/seo/validators/llms-txt.d.ts +57 -0
  41. package/dist/seo/validators/llms-txt.js +317 -0
  42. package/dist/seo/validators/robots.d.ts +54 -0
  43. package/dist/seo/validators/robots.js +382 -0
  44. package/dist/seo/validators/sitemap.d.ts +69 -0
  45. package/dist/seo/validators/sitemap.js +424 -0
  46. package/package.json +1 -1
@@ -0,0 +1,273 @@
1
+ export const DEFAULT_SIMILARITY_THRESHOLD = 85;
2
+ const SHINGLE_SIZE = 3;
3
+ const SIMHASH_BITS = 64;
4
+ export function normalizeText(text, removeStopWords = false) {
5
+ let normalized = text
6
+ .toLowerCase()
7
+ .replace(/[\r\n\t]+/g, ' ')
8
+ .replace(/[^\w\s]/g, ' ')
9
+ .replace(/\s+/g, ' ')
10
+ .trim();
11
+ if (removeStopWords) {
12
+ const stopWords = new Set([
13
+ 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
14
+ 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
15
+ 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
16
+ 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need',
17
+ 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'you', 'he',
18
+ 'she', 'we', 'they', 'what', 'which', 'who', 'whom', 'when', 'where',
19
+ 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', 'most',
20
+ 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same',
21
+ 'so', 'than', 'too', 'very', 'just',
22
+ ]);
23
+ normalized = normalized
24
+ .split(' ')
25
+ .filter(word => !stopWords.has(word) && word.length > 1)
26
+ .join(' ');
27
+ }
28
+ return normalized;
29
+ }
30
+ export function tokenize(text) {
31
+ return text.split(/\s+/).filter(word => word.length > 0);
32
+ }
33
+ export function createShingles(tokens, size = SHINGLE_SIZE) {
34
+ const shingles = new Set();
35
+ if (tokens.length < size) {
36
+ for (const token of tokens) {
37
+ shingles.add(hashString(token));
38
+ }
39
+ return shingles;
40
+ }
41
+ for (let i = 0; i <= tokens.length - size; i++) {
42
+ const shingle = tokens.slice(i, i + size).join(' ');
43
+ shingles.add(hashString(shingle));
44
+ }
45
+ return shingles;
46
+ }
47
+ function hashString(str) {
48
+ let hash = 2166136261;
49
+ for (let i = 0; i < str.length; i++) {
50
+ hash ^= str.charCodeAt(i);
51
+ hash = Math.imul(hash, 16777619);
52
+ }
53
+ return hash >>> 0;
54
+ }
55
+ function hash64(str) {
56
+ let h1 = 2166136261n;
57
+ let h2 = 2166136261n;
58
+ for (let i = 0; i < str.length; i++) {
59
+ const c = BigInt(str.charCodeAt(i));
60
+ h1 ^= c;
61
+ h1 = (h1 * 16777619n) & 0xffffffffn;
62
+ h2 ^= c * 31n;
63
+ h2 = (h2 * 16777619n) & 0xffffffffn;
64
+ }
65
+ return (h1 << 32n) | h2;
66
+ }
67
+ export function calculateSimHash(text) {
68
+ const normalized = normalizeText(text, true);
69
+ const tokens = tokenize(normalized);
70
+ if (tokens.length === 0) {
71
+ return 0n;
72
+ }
73
+ const bitCounts = new Array(SIMHASH_BITS).fill(0);
74
+ for (const token of tokens) {
75
+ const hash = hash64(token);
76
+ for (let i = 0; i < SIMHASH_BITS; i++) {
77
+ const bit = (hash >> BigInt(i)) & 1n;
78
+ bitCounts[i] += bit === 1n ? 1 : -1;
79
+ }
80
+ }
81
+ let simhash = 0n;
82
+ for (let i = 0; i < SIMHASH_BITS; i++) {
83
+ if (bitCounts[i] > 0) {
84
+ simhash |= 1n << BigInt(i);
85
+ }
86
+ }
87
+ return simhash;
88
+ }
89
+ export function hammingDistance(hash1, hash2) {
90
+ let xor = hash1 ^ hash2;
91
+ let distance = 0;
92
+ while (xor > 0n) {
93
+ distance += Number(xor & 1n);
94
+ xor >>= 1n;
95
+ }
96
+ return distance;
97
+ }
98
+ export function simhashSimilarity(hash1, hash2) {
99
+ const distance = hammingDistance(hash1, hash2);
100
+ return ((SIMHASH_BITS - distance) / SIMHASH_BITS) * 100;
101
+ }
102
+ export function jaccardSimilarity(setA, setB) {
103
+ if (setA.size === 0 && setB.size === 0) {
104
+ return 100;
105
+ }
106
+ if (setA.size === 0 || setB.size === 0) {
107
+ return 0;
108
+ }
109
+ let intersection = 0;
110
+ for (const item of setA) {
111
+ if (setB.has(item)) {
112
+ intersection++;
113
+ }
114
+ }
115
+ const union = setA.size + setB.size - intersection;
116
+ return (intersection / union) * 100;
117
+ }
118
+ export function createFingerprint(url, text) {
119
+ const normalizedText = normalizeText(text, false);
120
+ const tokens = tokenize(normalizeText(text, true));
121
+ return {
122
+ url,
123
+ simhash: calculateSimHash(text),
124
+ shingles: createShingles(tokens),
125
+ wordCount: tokens.length,
126
+ normalizedText,
127
+ };
128
+ }
129
+ export function compareFingerprints(fpA, fpB) {
130
+ if (fpA.normalizedText === fpB.normalizedText) {
131
+ return {
132
+ urlA: fpA.url,
133
+ urlB: fpB.url,
134
+ similarity: 100,
135
+ type: 'exact',
136
+ simhashDistance: 0,
137
+ jaccardIndex: 100,
138
+ };
139
+ }
140
+ const simhashSim = simhashSimilarity(fpA.simhash, fpB.simhash);
141
+ const jaccardSim = jaccardSimilarity(fpA.shingles, fpB.shingles);
142
+ const similarity = (simhashSim * 0.3 + jaccardSim * 0.7);
143
+ let type;
144
+ if (similarity >= 95) {
145
+ type = 'near-duplicate';
146
+ }
147
+ else if (similarity >= 70) {
148
+ type = 'similar';
149
+ }
150
+ else {
151
+ type = 'different';
152
+ }
153
+ return {
154
+ urlA: fpA.url,
155
+ urlB: fpB.url,
156
+ similarity: Math.round(similarity * 10) / 10,
157
+ type,
158
+ simhashDistance: hammingDistance(fpA.simhash, fpB.simhash),
159
+ jaccardIndex: Math.round(jaccardSim * 10) / 10,
160
+ };
161
+ }
162
+ export function findDuplicateContent(pages, threshold = DEFAULT_SIMILARITY_THRESHOLD) {
163
+ const duplicates = [];
164
+ const fingerprints = pages.map(p => createFingerprint(p.url, p.content));
165
+ for (let i = 0; i < fingerprints.length; i++) {
166
+ for (let j = i + 1; j < fingerprints.length; j++) {
167
+ const result = compareFingerprints(fingerprints[i], fingerprints[j]);
168
+ if (result.similarity >= threshold) {
169
+ duplicates.push(result);
170
+ }
171
+ }
172
+ }
173
+ return duplicates.sort((a, b) => b.similarity - a.similarity);
174
+ }
175
+ export function findDuplicateMetadata(pages, threshold = DEFAULT_SIMILARITY_THRESHOLD) {
176
+ const groups = [];
177
+ const findFieldDuplicates = (field, getValue) => {
178
+ const exactGroups = new Map();
179
+ const values = [];
180
+ for (const page of pages) {
181
+ const value = getValue(page);
182
+ if (!value || value.trim().length === 0)
183
+ continue;
184
+ const normalized = normalizeText(value);
185
+ values.push({ url: page.url, value, normalized });
186
+ if (!exactGroups.has(normalized)) {
187
+ exactGroups.set(normalized, []);
188
+ }
189
+ exactGroups.get(normalized).push(page.url);
190
+ }
191
+ for (const [normalized, urls] of exactGroups) {
192
+ if (urls.length > 1) {
193
+ const originalValue = values.find(v => v.normalized === normalized)?.value || normalized;
194
+ groups.push({
195
+ type: field,
196
+ value: originalValue,
197
+ urls,
198
+ similarity: 100,
199
+ });
200
+ }
201
+ }
202
+ if (threshold < 100) {
203
+ const uniqueNormalized = [...new Set(values.map(v => v.normalized))];
204
+ for (let i = 0; i < uniqueNormalized.length; i++) {
205
+ for (let j = i + 1; j < uniqueNormalized.length; j++) {
206
+ const a = uniqueNormalized[i];
207
+ const b = uniqueNormalized[j];
208
+ const similarity = calculateStringSimilarity(a, b);
209
+ if (similarity >= threshold && similarity < 100) {
210
+ const urlsA = values.filter(v => v.normalized === a).map(v => v.url);
211
+ const urlsB = values.filter(v => v.normalized === b).map(v => v.url);
212
+ const allUrls = [...new Set([...urlsA, ...urlsB])];
213
+ if (allUrls.length > 1) {
214
+ groups.push({
215
+ type: field,
216
+ value: values.find(v => v.normalized === a)?.value || a,
217
+ urls: allUrls,
218
+ similarity: Math.round(similarity),
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ };
226
+ findFieldDuplicates('title', p => p.title);
227
+ findFieldDuplicates('description', p => p.description);
228
+ findFieldDuplicates('h1', p => p.h1);
229
+ return groups;
230
+ }
231
+ export function calculateStringSimilarity(a, b) {
232
+ if (a === b)
233
+ return 100;
234
+ if (!a || !b)
235
+ return 0;
236
+ const normA = normalizeText(a);
237
+ const normB = normalizeText(b);
238
+ if (normA === normB)
239
+ return 100;
240
+ const tokensA = new Set(tokenize(normA));
241
+ const tokensB = new Set(tokenize(normB));
242
+ let intersection = 0;
243
+ for (const token of tokensA) {
244
+ if (tokensB.has(token))
245
+ intersection++;
246
+ }
247
+ const tokenSimilarity = (2 * intersection) / (tokensA.size + tokensB.size) * 100;
248
+ const lengthRatio = Math.min(normA.length, normB.length) / Math.max(normA.length, normB.length);
249
+ const lengthSimilarity = lengthRatio * 100;
250
+ return (tokenSimilarity * 0.7 + lengthSimilarity * 0.3);
251
+ }
252
+ export function calculateTextToHtmlRatio(html, text) {
253
+ const htmlLength = html.length;
254
+ const textLength = text.length;
255
+ if (htmlLength === 0)
256
+ return 0;
257
+ return (textLength / htmlLength) * 100;
258
+ }
259
+ export function isThinContent(wordCount, textToHtmlRatio, minWords = 300, minRatio = 10) {
260
+ if (wordCount < minWords) {
261
+ return {
262
+ isThin: true,
263
+ reason: `Low word count: ${wordCount} words (min: ${minWords})`,
264
+ };
265
+ }
266
+ if (textToHtmlRatio !== undefined && textToHtmlRatio < minRatio) {
267
+ return {
268
+ isThin: true,
269
+ reason: `Low text-to-HTML ratio: ${textToHtmlRatio.toFixed(1)}% (min: ${minRatio}%)`,
270
+ };
271
+ }
272
+ return { isThin: false };
273
+ }
@@ -0,0 +1,3 @@
1
+ export * from './robots.js';
2
+ export * from './sitemap.js';
3
+ export * from './llms-txt.js';
@@ -0,0 +1,3 @@
1
+ export * from './robots.js';
2
+ export * from './sitemap.js';
3
+ export * from './llms-txt.js';
@@ -0,0 +1,57 @@
1
+ export interface LlmsTxtLink {
2
+ text: string;
3
+ url: string;
4
+ description?: string;
5
+ section?: string;
6
+ }
7
+ export interface LlmsTxtSection {
8
+ title: string;
9
+ content: string;
10
+ links: LlmsTxtLink[];
11
+ }
12
+ export interface LlmsTxtParseResult {
13
+ valid: boolean;
14
+ errors: string[];
15
+ warnings: string[];
16
+ siteName?: string;
17
+ siteDescription?: string;
18
+ sections: LlmsTxtSection[];
19
+ links: LlmsTxtLink[];
20
+ hasFullVersion: boolean;
21
+ rawContent: string;
22
+ size: number;
23
+ }
24
+ export interface LlmsTxtValidationIssue {
25
+ type: 'error' | 'warning' | 'info';
26
+ code: string;
27
+ message: string;
28
+ line?: number;
29
+ recommendation?: string;
30
+ }
31
+ export interface LlmsTxtValidationResult {
32
+ valid: boolean;
33
+ issues: LlmsTxtValidationIssue[];
34
+ parseResult: LlmsTxtParseResult;
35
+ }
36
+ export declare function parseLlmsTxt(content: string): LlmsTxtParseResult;
37
+ export declare function validateLlmsTxt(content: string, baseUrl?: string): LlmsTxtValidationResult;
38
+ export declare function fetchAndValidateLlmsTxt(url: string, fetcher?: (url: string) => Promise<{
39
+ status: number;
40
+ text: string;
41
+ }>): Promise<LlmsTxtValidationResult & {
42
+ exists: boolean;
43
+ status?: number;
44
+ fullVersionExists?: boolean;
45
+ }>;
46
+ export declare function generateLlmsTxtTemplate(options: {
47
+ siteName: string;
48
+ siteDescription: string;
49
+ sections?: Array<{
50
+ title: string;
51
+ links: Array<{
52
+ text: string;
53
+ url: string;
54
+ description?: string;
55
+ }>;
56
+ }>;
57
+ }): string;
@@ -0,0 +1,317 @@
1
+ const MAX_FILE_SIZE = 100 * 1024;
2
+ const MIN_DESCRIPTION_LENGTH = 50;
3
+ const MAX_DESCRIPTION_LENGTH = 500;
4
+ export function parseLlmsTxt(content) {
5
+ const errors = [];
6
+ const warnings = [];
7
+ const sections = [];
8
+ const links = [];
9
+ let siteName;
10
+ let siteDescription;
11
+ let hasFullVersion = false;
12
+ let currentSection = null;
13
+ const lines = content.split(/\r?\n/);
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const line = lines[i];
16
+ const trimmedLine = line.trim();
17
+ if (!trimmedLine)
18
+ continue;
19
+ if (trimmedLine.startsWith('# ') && !siteName) {
20
+ siteName = trimmedLine.substring(2).trim();
21
+ continue;
22
+ }
23
+ if (trimmedLine.startsWith('> ') && !siteDescription) {
24
+ siteDescription = trimmedLine.substring(2).trim();
25
+ continue;
26
+ }
27
+ if (trimmedLine.startsWith('## ')) {
28
+ const title = trimmedLine.substring(3).trim();
29
+ currentSection = { title, content: '', links: [] };
30
+ sections.push(currentSection);
31
+ continue;
32
+ }
33
+ const linkMatches = trimmedLine.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g);
34
+ for (const match of linkMatches) {
35
+ const linkText = match[1];
36
+ const linkUrl = match[2];
37
+ if (linkUrl.includes('llms-full.txt') || linkUrl.includes('llms_full.txt')) {
38
+ hasFullVersion = true;
39
+ }
40
+ const link = {
41
+ text: linkText,
42
+ url: linkUrl,
43
+ section: currentSection?.title,
44
+ };
45
+ const afterLink = trimmedLine.substring(trimmedLine.indexOf(match[0]) + match[0].length).trim();
46
+ if (afterLink.startsWith(':') || afterLink.startsWith('-')) {
47
+ link.description = afterLink.substring(1).trim();
48
+ }
49
+ links.push(link);
50
+ if (currentSection) {
51
+ currentSection.links.push(link);
52
+ }
53
+ }
54
+ if (currentSection && !trimmedLine.startsWith('##')) {
55
+ if (currentSection.content) {
56
+ currentSection.content += '\n' + trimmedLine;
57
+ }
58
+ else {
59
+ currentSection.content = trimmedLine;
60
+ }
61
+ }
62
+ }
63
+ if (!siteName) {
64
+ errors.push('Missing site name (should be first line starting with #)');
65
+ }
66
+ if (!siteDescription) {
67
+ warnings.push('Missing site description (should be a > blockquote after site name)');
68
+ }
69
+ if (links.length === 0) {
70
+ warnings.push('No links found in llms.txt');
71
+ }
72
+ return {
73
+ valid: errors.length === 0,
74
+ errors,
75
+ warnings,
76
+ siteName,
77
+ siteDescription,
78
+ sections,
79
+ links,
80
+ hasFullVersion,
81
+ rawContent: content,
82
+ size: content.length,
83
+ };
84
+ }
85
+ export function validateLlmsTxt(content, baseUrl) {
86
+ const parseResult = parseLlmsTxt(content);
87
+ const issues = [];
88
+ for (const error of parseResult.errors) {
89
+ issues.push({
90
+ type: 'error',
91
+ code: 'PARSE_ERROR',
92
+ message: error,
93
+ });
94
+ }
95
+ for (const warning of parseResult.warnings) {
96
+ issues.push({
97
+ type: 'warning',
98
+ code: 'PARSE_WARNING',
99
+ message: warning,
100
+ });
101
+ }
102
+ if (parseResult.size > MAX_FILE_SIZE) {
103
+ issues.push({
104
+ type: 'warning',
105
+ code: 'FILE_TOO_LARGE',
106
+ message: `llms.txt is ${Math.round(parseResult.size / 1024)}KB (recommended max: 100KB)`,
107
+ recommendation: 'Keep llms.txt concise; use llms-full.txt for detailed content',
108
+ });
109
+ }
110
+ if (parseResult.siteDescription) {
111
+ if (parseResult.siteDescription.length < MIN_DESCRIPTION_LENGTH) {
112
+ issues.push({
113
+ type: 'info',
114
+ code: 'SHORT_DESCRIPTION',
115
+ message: `Site description is short (${parseResult.siteDescription.length} chars)`,
116
+ recommendation: `Provide at least ${MIN_DESCRIPTION_LENGTH} characters of description`,
117
+ });
118
+ }
119
+ else if (parseResult.siteDescription.length > MAX_DESCRIPTION_LENGTH) {
120
+ issues.push({
121
+ type: 'info',
122
+ code: 'LONG_DESCRIPTION',
123
+ message: `Site description is long (${parseResult.siteDescription.length} chars)`,
124
+ recommendation: `Keep description under ${MAX_DESCRIPTION_LENGTH} characters`,
125
+ });
126
+ }
127
+ }
128
+ const sectionTitles = parseResult.sections.map(s => s.title.toLowerCase());
129
+ const recommendedSections = ['docs', 'documentation', 'api', 'getting started', 'about'];
130
+ const hasSomeRecommended = recommendedSections.some(s => sectionTitles.some(t => t.includes(s)));
131
+ if (!hasSomeRecommended && parseResult.sections.length === 0) {
132
+ issues.push({
133
+ type: 'info',
134
+ code: 'NO_SECTIONS',
135
+ message: 'No content sections found',
136
+ recommendation: 'Add sections like ## Docs, ## API, ## About for better organization',
137
+ });
138
+ }
139
+ if (baseUrl) {
140
+ const baseHost = new URL(baseUrl).hostname;
141
+ for (const link of parseResult.links) {
142
+ try {
143
+ const linkUrl = new URL(link.url, baseUrl);
144
+ if (!link.url.startsWith('http') && !link.url.startsWith('/')) {
145
+ issues.push({
146
+ type: 'warning',
147
+ code: 'RELATIVE_URL',
148
+ message: `Relative URL may not resolve correctly: ${link.url}`,
149
+ recommendation: 'Use absolute URLs or URLs starting with /',
150
+ });
151
+ }
152
+ }
153
+ catch {
154
+ issues.push({
155
+ type: 'error',
156
+ code: 'INVALID_URL',
157
+ message: `Invalid URL: ${link.url}`,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ if (!parseResult.hasFullVersion && parseResult.size > 10000) {
163
+ issues.push({
164
+ type: 'info',
165
+ code: 'CONSIDER_FULL_VERSION',
166
+ message: 'Large llms.txt without llms-full.txt reference',
167
+ recommendation: 'Consider creating llms-full.txt for detailed content',
168
+ });
169
+ }
170
+ const seenUrls = new Set();
171
+ for (const link of parseResult.links) {
172
+ const normalized = link.url.toLowerCase();
173
+ if (seenUrls.has(normalized)) {
174
+ issues.push({
175
+ type: 'info',
176
+ code: 'DUPLICATE_LINK',
177
+ message: `Duplicate link: ${link.url}`,
178
+ });
179
+ }
180
+ else {
181
+ seenUrls.add(normalized);
182
+ }
183
+ }
184
+ return {
185
+ valid: issues.filter(i => i.type === 'error').length === 0,
186
+ issues,
187
+ parseResult,
188
+ };
189
+ }
190
+ export async function fetchAndValidateLlmsTxt(url, fetcher) {
191
+ const llmsTxtUrl = new URL('/llms.txt', url).href;
192
+ try {
193
+ let response;
194
+ if (fetcher) {
195
+ response = await fetcher(llmsTxtUrl);
196
+ }
197
+ else {
198
+ const fetchResponse = await fetch(llmsTxtUrl);
199
+ response = {
200
+ status: fetchResponse.status,
201
+ text: await fetchResponse.text(),
202
+ };
203
+ }
204
+ if (response.status === 404) {
205
+ return {
206
+ exists: false,
207
+ status: 404,
208
+ valid: false,
209
+ issues: [{
210
+ type: 'info',
211
+ code: 'NOT_FOUND',
212
+ message: 'llms.txt not found',
213
+ recommendation: 'Create llms.txt to optimize for AI/LLM discovery (see llmstxt.org)',
214
+ }],
215
+ parseResult: {
216
+ valid: false,
217
+ errors: [],
218
+ warnings: [],
219
+ sections: [],
220
+ links: [],
221
+ hasFullVersion: false,
222
+ rawContent: '',
223
+ size: 0,
224
+ },
225
+ };
226
+ }
227
+ if (response.status >= 400) {
228
+ return {
229
+ exists: false,
230
+ status: response.status,
231
+ valid: false,
232
+ issues: [{
233
+ type: 'error',
234
+ code: 'FETCH_ERROR',
235
+ message: `Failed to fetch llms.txt (HTTP ${response.status})`,
236
+ }],
237
+ parseResult: {
238
+ valid: false,
239
+ errors: [],
240
+ warnings: [],
241
+ sections: [],
242
+ links: [],
243
+ hasFullVersion: false,
244
+ rawContent: '',
245
+ size: 0,
246
+ },
247
+ };
248
+ }
249
+ const validation = validateLlmsTxt(response.text, url);
250
+ let fullVersionExists = false;
251
+ if (validation.parseResult.hasFullVersion) {
252
+ try {
253
+ const fullUrl = new URL('/llms-full.txt', url).href;
254
+ let fullResponse;
255
+ if (fetcher) {
256
+ fullResponse = await fetcher(fullUrl);
257
+ }
258
+ else {
259
+ fullResponse = await fetch(fullUrl, { method: 'HEAD' });
260
+ }
261
+ fullVersionExists = fullResponse.status === 200;
262
+ }
263
+ catch {
264
+ }
265
+ }
266
+ return {
267
+ ...validation,
268
+ exists: true,
269
+ status: response.status,
270
+ fullVersionExists,
271
+ };
272
+ }
273
+ catch (error) {
274
+ return {
275
+ exists: false,
276
+ valid: false,
277
+ issues: [{
278
+ type: 'error',
279
+ code: 'FETCH_ERROR',
280
+ message: `Failed to fetch llms.txt: ${error instanceof Error ? error.message : 'Unknown error'}`,
281
+ }],
282
+ parseResult: {
283
+ valid: false,
284
+ errors: [],
285
+ warnings: [],
286
+ sections: [],
287
+ links: [],
288
+ hasFullVersion: false,
289
+ rawContent: '',
290
+ size: 0,
291
+ },
292
+ };
293
+ }
294
+ }
295
+ export function generateLlmsTxtTemplate(options) {
296
+ const lines = [];
297
+ lines.push(`# ${options.siteName}`);
298
+ lines.push('');
299
+ lines.push(`> ${options.siteDescription}`);
300
+ lines.push('');
301
+ if (options.sections) {
302
+ for (const section of options.sections) {
303
+ lines.push(`## ${section.title}`);
304
+ lines.push('');
305
+ for (const link of section.links) {
306
+ if (link.description) {
307
+ lines.push(`- [${link.text}](${link.url}): ${link.description}`);
308
+ }
309
+ else {
310
+ lines.push(`- [${link.text}](${link.url})`);
311
+ }
312
+ }
313
+ lines.push('');
314
+ }
315
+ }
316
+ return lines.join('\n');
317
+ }