recker 1.0.72 → 1.0.73
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/mcp/tools/seo.js +320 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/mcp/tools/seo.js
CHANGED
|
@@ -1,6 +1,114 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
1
5
|
import { createClient } from '../../core/client.js';
|
|
2
6
|
import { analyzeSeo } from '../../seo/analyzer.js';
|
|
3
7
|
import { seoSpider } from '../../seo/seo-spider.js';
|
|
8
|
+
const DEFAULT_SEO_CACHE_TTL_SEC = 6 * 60 * 60;
|
|
9
|
+
const SEO_REPORT_DIR = join(tmpdir(), 'recker', 'seo');
|
|
10
|
+
function normalizeStringArray(value) {
|
|
11
|
+
if (!Array.isArray(value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const items = value.map(item => String(item).trim()).filter(Boolean);
|
|
14
|
+
if (items.length === 0)
|
|
15
|
+
return undefined;
|
|
16
|
+
return items;
|
|
17
|
+
}
|
|
18
|
+
function safeHost(url) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(url);
|
|
21
|
+
const host = parsed.hostname || parsed.host;
|
|
22
|
+
return host.replace(/[^a-z0-9.-]/gi, '_') || 'unknown';
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return url.replace(/[^a-z0-9.-]/gi, '_').slice(0, 64) || 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function hashArgs(value) {
|
|
29
|
+
return createHash('sha1').update(JSON.stringify(value)).digest('hex').slice(0, 12);
|
|
30
|
+
}
|
|
31
|
+
function resolveReportPersistence(args, options) {
|
|
32
|
+
const output = typeof args.output === 'string' ? args.output : undefined;
|
|
33
|
+
const outputDir = typeof args.outputDir === 'string' ? args.outputDir : undefined;
|
|
34
|
+
const persist = args.persist === true;
|
|
35
|
+
const persistenceEnabled = Boolean(output || outputDir || persist);
|
|
36
|
+
const cacheEnabled = persistenceEnabled ? (typeof args.cache === 'boolean' ? args.cache : true) : false;
|
|
37
|
+
const forceRefresh = args.forceRefresh === true;
|
|
38
|
+
const cacheTtlInput = typeof args.cacheTtlSec === 'number'
|
|
39
|
+
? args.cacheTtlSec
|
|
40
|
+
: Number.isFinite(Number(args.cacheTtlSec))
|
|
41
|
+
? Number(args.cacheTtlSec)
|
|
42
|
+
: undefined;
|
|
43
|
+
const cacheTtlSec = cacheEnabled
|
|
44
|
+
? Math.max(0, cacheTtlInput ?? DEFAULT_SEO_CACHE_TTL_SEC)
|
|
45
|
+
: 0;
|
|
46
|
+
if (!persistenceEnabled) {
|
|
47
|
+
return {
|
|
48
|
+
enabled: false,
|
|
49
|
+
reportPath: null,
|
|
50
|
+
cacheEnabled: false,
|
|
51
|
+
cacheTtlSec: 0,
|
|
52
|
+
forceRefresh,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const host = safeHost(options.url);
|
|
56
|
+
const hash = hashArgs(options.cacheKey);
|
|
57
|
+
const fileName = `${options.toolSlug}--${host}--${hash}.json`;
|
|
58
|
+
const reportPath = output
|
|
59
|
+
? output
|
|
60
|
+
: outputDir
|
|
61
|
+
? join(outputDir, fileName)
|
|
62
|
+
: join(SEO_REPORT_DIR, fileName);
|
|
63
|
+
return {
|
|
64
|
+
enabled: true,
|
|
65
|
+
reportPath,
|
|
66
|
+
cacheEnabled,
|
|
67
|
+
cacheTtlSec,
|
|
68
|
+
forceRefresh,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function readCachedReport(path, ttlSec) {
|
|
72
|
+
if (!path || ttlSec <= 0)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const stats = await fs.stat(path);
|
|
76
|
+
const ageSec = (Date.now() - stats.mtimeMs) / 1000;
|
|
77
|
+
if (ageSec > ttlSec) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const text = await fs.readFile(path, 'utf8');
|
|
81
|
+
const report = JSON.parse(text);
|
|
82
|
+
if (report && typeof report === 'object') {
|
|
83
|
+
delete report.reportMeta;
|
|
84
|
+
delete report.note;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
report,
|
|
88
|
+
ageSec,
|
|
89
|
+
bytes: stats.size,
|
|
90
|
+
savedAt: new Date(stats.mtimeMs).toISOString(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function writeReport(path, payload) {
|
|
98
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
99
|
+
await fs.writeFile(path, payload, 'utf8');
|
|
100
|
+
return { bytes: Buffer.byteLength(payload), savedAt: new Date().toISOString() };
|
|
101
|
+
}
|
|
102
|
+
function buildReportMeta(params) {
|
|
103
|
+
return {
|
|
104
|
+
reportPath: params.reportPath ?? null,
|
|
105
|
+
cacheHit: params.cacheHit ?? false,
|
|
106
|
+
source: params.source,
|
|
107
|
+
...(params.cacheAgeSec !== undefined ? { cacheAgeSec: params.cacheAgeSec } : {}),
|
|
108
|
+
...(params.savedAt ? { savedAt: params.savedAt } : {}),
|
|
109
|
+
...(params.reportBytes !== undefined ? { reportBytes: params.reportBytes } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
4
112
|
function formatCheck(check) {
|
|
5
113
|
const icon = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '⚠';
|
|
6
114
|
let line = `${icon} [${check.status.toUpperCase()}] ${check.name}: ${check.message}`;
|
|
@@ -75,14 +183,46 @@ function generateQuickWins(report) {
|
|
|
75
183
|
}
|
|
76
184
|
async function seoAnalyze(args) {
|
|
77
185
|
const url = String(args.url || '');
|
|
78
|
-
const categories = args.categories;
|
|
186
|
+
const categories = normalizeStringArray(args.categories);
|
|
79
187
|
if (!url) {
|
|
80
188
|
return {
|
|
81
189
|
content: [{ type: 'text', text: 'Error: url is required' }],
|
|
82
190
|
isError: true,
|
|
83
191
|
};
|
|
84
192
|
}
|
|
193
|
+
const persistence = resolveReportPersistence(args, {
|
|
194
|
+
toolSlug: 'seo',
|
|
195
|
+
url,
|
|
196
|
+
cacheKey: {
|
|
197
|
+
url,
|
|
198
|
+
categories: categories ? [...categories].sort() : [],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
85
201
|
try {
|
|
202
|
+
if (persistence.enabled && persistence.cacheEnabled && !persistence.forceRefresh && persistence.reportPath) {
|
|
203
|
+
const cached = await readCachedReport(persistence.reportPath, persistence.cacheTtlSec);
|
|
204
|
+
if (cached) {
|
|
205
|
+
const reportMeta = buildReportMeta({
|
|
206
|
+
reportPath: persistence.reportPath,
|
|
207
|
+
cacheHit: true,
|
|
208
|
+
cacheAgeSec: cached.ageSec,
|
|
209
|
+
savedAt: cached.savedAt,
|
|
210
|
+
reportBytes: cached.bytes,
|
|
211
|
+
source: 'cached',
|
|
212
|
+
});
|
|
213
|
+
const output = {
|
|
214
|
+
...cached.report,
|
|
215
|
+
reportMeta,
|
|
216
|
+
note: `Cache hit: report loaded from ${persistence.reportPath}`,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
content: [{
|
|
220
|
+
type: 'text',
|
|
221
|
+
text: JSON.stringify(output, null, 2),
|
|
222
|
+
}],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
86
226
|
const client = createClient({ timeout: 30000 });
|
|
87
227
|
const response = await client.get(url);
|
|
88
228
|
const html = await response.text();
|
|
@@ -135,10 +275,56 @@ async function seoAnalyze(args) {
|
|
|
135
275
|
images: report.images,
|
|
136
276
|
technical: report.technical,
|
|
137
277
|
};
|
|
278
|
+
const reportPayload = JSON.stringify(output, null, 2);
|
|
279
|
+
let reportMeta = buildReportMeta({
|
|
280
|
+
reportPath: persistence.reportPath,
|
|
281
|
+
cacheHit: false,
|
|
282
|
+
source: 'fresh',
|
|
283
|
+
});
|
|
284
|
+
let note = persistence.enabled
|
|
285
|
+
? `Report generated (not yet saved).`
|
|
286
|
+
: 'Report generated (not persisted). Set persist=true to save and enable caching.';
|
|
287
|
+
if (persistence.enabled && persistence.reportPath) {
|
|
288
|
+
try {
|
|
289
|
+
const saved = await writeReport(persistence.reportPath, reportPayload);
|
|
290
|
+
reportMeta = buildReportMeta({
|
|
291
|
+
reportPath: persistence.reportPath,
|
|
292
|
+
cacheHit: false,
|
|
293
|
+
savedAt: saved.savedAt,
|
|
294
|
+
reportBytes: saved.bytes,
|
|
295
|
+
source: 'fresh',
|
|
296
|
+
});
|
|
297
|
+
note = `Report saved to ${persistence.reportPath}`;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
const message = `Failed to save report to ${persistence.reportPath}: ${error.message}`;
|
|
301
|
+
const failedOutput = {
|
|
302
|
+
...output,
|
|
303
|
+
reportMeta,
|
|
304
|
+
note: message,
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
content: [{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: JSON.stringify(failedOutput, null, 2),
|
|
310
|
+
}],
|
|
311
|
+
isError: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
reportMeta = buildReportMeta({
|
|
317
|
+
reportPath: null,
|
|
318
|
+
cacheHit: false,
|
|
319
|
+
reportBytes: Buffer.byteLength(reportPayload),
|
|
320
|
+
savedAt: new Date().toISOString(),
|
|
321
|
+
source: 'fresh',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
138
324
|
return {
|
|
139
325
|
content: [{
|
|
140
326
|
type: 'text',
|
|
141
|
-
text: JSON.stringify(output, null, 2),
|
|
327
|
+
text: JSON.stringify({ ...output, reportMeta, note }, null, 2),
|
|
142
328
|
}],
|
|
143
329
|
};
|
|
144
330
|
}
|
|
@@ -165,6 +351,41 @@ async function seoSpiderCrawl(args) {
|
|
|
165
351
|
};
|
|
166
352
|
}
|
|
167
353
|
try {
|
|
354
|
+
const persistence = resolveReportPersistence(args, {
|
|
355
|
+
toolSlug: 'seo-spider',
|
|
356
|
+
url,
|
|
357
|
+
cacheKey: {
|
|
358
|
+
url,
|
|
359
|
+
maxPages,
|
|
360
|
+
maxDepth,
|
|
361
|
+
concurrency,
|
|
362
|
+
transport,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
if (persistence.enabled && persistence.cacheEnabled && !persistence.forceRefresh && persistence.reportPath) {
|
|
366
|
+
const cached = await readCachedReport(persistence.reportPath, persistence.cacheTtlSec);
|
|
367
|
+
if (cached) {
|
|
368
|
+
const reportMeta = buildReportMeta({
|
|
369
|
+
reportPath: persistence.reportPath,
|
|
370
|
+
cacheHit: true,
|
|
371
|
+
cacheAgeSec: cached.ageSec,
|
|
372
|
+
savedAt: cached.savedAt,
|
|
373
|
+
reportBytes: cached.bytes,
|
|
374
|
+
source: 'cached',
|
|
375
|
+
});
|
|
376
|
+
const output = {
|
|
377
|
+
...cached.report,
|
|
378
|
+
reportMeta,
|
|
379
|
+
note: `Cache hit: report loaded from ${persistence.reportPath}`,
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
content: [{
|
|
383
|
+
type: 'text',
|
|
384
|
+
text: JSON.stringify(output, null, 2),
|
|
385
|
+
}],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
168
389
|
const result = await seoSpider(url, {
|
|
169
390
|
seo: true,
|
|
170
391
|
maxPages,
|
|
@@ -226,10 +447,56 @@ async function seoSpiderCrawl(args) {
|
|
|
226
447
|
if (recommendations.length > 0) {
|
|
227
448
|
output.recommendations = recommendations;
|
|
228
449
|
}
|
|
450
|
+
const reportPayload = JSON.stringify(output, null, 2);
|
|
451
|
+
let reportMeta = buildReportMeta({
|
|
452
|
+
reportPath: persistence.reportPath,
|
|
453
|
+
cacheHit: false,
|
|
454
|
+
source: 'fresh',
|
|
455
|
+
});
|
|
456
|
+
let note = persistence.enabled
|
|
457
|
+
? 'Report generated (not yet saved).'
|
|
458
|
+
: 'Report generated (not persisted). Set persist=true to save and enable caching.';
|
|
459
|
+
if (persistence.enabled && persistence.reportPath) {
|
|
460
|
+
try {
|
|
461
|
+
const saved = await writeReport(persistence.reportPath, reportPayload);
|
|
462
|
+
reportMeta = buildReportMeta({
|
|
463
|
+
reportPath: persistence.reportPath,
|
|
464
|
+
cacheHit: false,
|
|
465
|
+
savedAt: saved.savedAt,
|
|
466
|
+
reportBytes: saved.bytes,
|
|
467
|
+
source: 'fresh',
|
|
468
|
+
});
|
|
469
|
+
note = `Report saved to ${persistence.reportPath}`;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const message = `Failed to save report to ${persistence.reportPath}: ${error.message}`;
|
|
473
|
+
const failedOutput = {
|
|
474
|
+
...output,
|
|
475
|
+
reportMeta,
|
|
476
|
+
note: message,
|
|
477
|
+
};
|
|
478
|
+
return {
|
|
479
|
+
content: [{
|
|
480
|
+
type: 'text',
|
|
481
|
+
text: JSON.stringify(failedOutput, null, 2),
|
|
482
|
+
}],
|
|
483
|
+
isError: true,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
reportMeta = buildReportMeta({
|
|
489
|
+
reportPath: null,
|
|
490
|
+
cacheHit: false,
|
|
491
|
+
reportBytes: Buffer.byteLength(reportPayload),
|
|
492
|
+
savedAt: new Date().toISOString(),
|
|
493
|
+
source: 'fresh',
|
|
494
|
+
});
|
|
495
|
+
}
|
|
229
496
|
return {
|
|
230
497
|
content: [{
|
|
231
498
|
type: 'text',
|
|
232
|
-
text: JSON.stringify(output, null, 2),
|
|
499
|
+
text: JSON.stringify({ ...output, reportMeta, note }, null, 2),
|
|
233
500
|
}],
|
|
234
501
|
};
|
|
235
502
|
}
|
|
@@ -330,6 +597,7 @@ Returns:
|
|
|
330
597
|
- Warnings and recommendations
|
|
331
598
|
- OpenGraph/social meta analysis
|
|
332
599
|
- Request timing breakdown
|
|
600
|
+
- Optional report persistence and caching for AI agent workflows
|
|
333
601
|
|
|
334
602
|
Perfect for analyzing your localhost dev server or any public URL. Categories include: meta, content, links, images, technical, security, performance, mobile, accessibility, schema, structural, i18n, PWA, social, e-commerce, local SEO, Core Web Vitals, readability, crawlability, internal linking, and best practices.`,
|
|
335
603
|
inputSchema: {
|
|
@@ -344,6 +612,30 @@ Perfect for analyzing your localhost dev server or any public URL. Categories in
|
|
|
344
612
|
items: { type: 'string' },
|
|
345
613
|
description: 'Filter by specific categories (e.g., ["meta", "security", "performance"]). Leave empty for all.',
|
|
346
614
|
},
|
|
615
|
+
output: {
|
|
616
|
+
type: 'string',
|
|
617
|
+
description: 'Save report to explicit file path',
|
|
618
|
+
},
|
|
619
|
+
outputDir: {
|
|
620
|
+
type: 'string',
|
|
621
|
+
description: 'Save report to directory with auto-generated filename',
|
|
622
|
+
},
|
|
623
|
+
persist: {
|
|
624
|
+
type: 'boolean',
|
|
625
|
+
description: 'Save report to default temp path (os.tmpdir()/recker/seo)',
|
|
626
|
+
},
|
|
627
|
+
cache: {
|
|
628
|
+
type: 'boolean',
|
|
629
|
+
description: 'Reuse existing saved report if present and within TTL (default: true when persistence enabled)',
|
|
630
|
+
},
|
|
631
|
+
cacheTtlSec: {
|
|
632
|
+
type: 'number',
|
|
633
|
+
description: `Cache TTL in seconds (default: ${DEFAULT_SEO_CACHE_TTL_SEC})`,
|
|
634
|
+
},
|
|
635
|
+
forceRefresh: {
|
|
636
|
+
type: 'boolean',
|
|
637
|
+
description: 'Bypass cache and recompute even if a saved report exists',
|
|
638
|
+
},
|
|
347
639
|
},
|
|
348
640
|
required: ['url'],
|
|
349
641
|
},
|
|
@@ -357,7 +649,7 @@ Detects site-wide issues:
|
|
|
357
649
|
- Orphan pages (no internal links pointing to them)
|
|
358
650
|
- Pages with low SEO scores
|
|
359
651
|
|
|
360
|
-
Returns per-page scores and prioritized recommendations for improving overall site SEO. Great for auditing a full site before launch or finding issues across your dev environment.`,
|
|
652
|
+
Returns per-page scores and prioritized recommendations for improving overall site SEO. Great for auditing a full site before launch or finding issues across your dev environment. Supports optional report persistence and caching for AI agent workflows.`,
|
|
361
653
|
inputSchema: {
|
|
362
654
|
type: 'object',
|
|
363
655
|
properties: {
|
|
@@ -386,6 +678,30 @@ Returns per-page scores and prioritized recommendations for improving overall si
|
|
|
386
678
|
description: 'HTTP transport: auto (try undici, fallback to curl on WAF block), undici (fast), curl (curl-impersonate for protected sites)',
|
|
387
679
|
default: 'auto',
|
|
388
680
|
},
|
|
681
|
+
output: {
|
|
682
|
+
type: 'string',
|
|
683
|
+
description: 'Save report to explicit file path',
|
|
684
|
+
},
|
|
685
|
+
outputDir: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Save report to directory with auto-generated filename',
|
|
688
|
+
},
|
|
689
|
+
persist: {
|
|
690
|
+
type: 'boolean',
|
|
691
|
+
description: 'Save report to default temp path (os.tmpdir()/recker/seo)',
|
|
692
|
+
},
|
|
693
|
+
cache: {
|
|
694
|
+
type: 'boolean',
|
|
695
|
+
description: 'Reuse existing saved report if present and within TTL (default: true when persistence enabled)',
|
|
696
|
+
},
|
|
697
|
+
cacheTtlSec: {
|
|
698
|
+
type: 'number',
|
|
699
|
+
description: `Cache TTL in seconds (default: ${DEFAULT_SEO_CACHE_TTL_SEC})`,
|
|
700
|
+
},
|
|
701
|
+
forceRefresh: {
|
|
702
|
+
type: 'boolean',
|
|
703
|
+
description: 'Bypass cache and recompute even if a saved report exists',
|
|
704
|
+
},
|
|
389
705
|
},
|
|
390
706
|
required: ['url'],
|
|
391
707
|
},
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.73",
|
|
4
4
|
"description": "Multi-Protocol SDK for the AI Era - HTTP, WebSocket, DNS, FTP, SFTP, Telnet, HLS unified with AI providers and MCP tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|