recker 1.0.27 → 1.0.28-next.4354f8c

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 (70) hide show
  1. package/dist/browser/scrape/extractors.js +2 -1
  2. package/dist/browser/scrape/types.d.ts +2 -1
  3. package/dist/cli/index.js +142 -3
  4. package/dist/cli/tui/shell.d.ts +2 -0
  5. package/dist/cli/tui/shell.js +269 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/scrape/extractors.js +2 -1
  9. package/dist/scrape/index.d.ts +2 -0
  10. package/dist/scrape/index.js +1 -0
  11. package/dist/scrape/spider.d.ts +59 -0
  12. package/dist/scrape/spider.js +209 -0
  13. package/dist/scrape/types.d.ts +2 -1
  14. package/dist/seo/analyzer.d.ts +42 -0
  15. package/dist/seo/analyzer.js +727 -0
  16. package/dist/seo/index.d.ts +5 -0
  17. package/dist/seo/index.js +2 -0
  18. package/dist/seo/rules/accessibility.d.ts +2 -0
  19. package/dist/seo/rules/accessibility.js +694 -0
  20. package/dist/seo/rules/best-practices.d.ts +2 -0
  21. package/dist/seo/rules/best-practices.js +188 -0
  22. package/dist/seo/rules/content.d.ts +2 -0
  23. package/dist/seo/rules/content.js +236 -0
  24. package/dist/seo/rules/crawl.d.ts +2 -0
  25. package/dist/seo/rules/crawl.js +307 -0
  26. package/dist/seo/rules/cwv.d.ts +2 -0
  27. package/dist/seo/rules/cwv.js +337 -0
  28. package/dist/seo/rules/ecommerce.d.ts +2 -0
  29. package/dist/seo/rules/ecommerce.js +252 -0
  30. package/dist/seo/rules/i18n.d.ts +2 -0
  31. package/dist/seo/rules/i18n.js +222 -0
  32. package/dist/seo/rules/images.d.ts +2 -0
  33. package/dist/seo/rules/images.js +180 -0
  34. package/dist/seo/rules/index.d.ts +52 -0
  35. package/dist/seo/rules/index.js +143 -0
  36. package/dist/seo/rules/internal-linking.d.ts +2 -0
  37. package/dist/seo/rules/internal-linking.js +375 -0
  38. package/dist/seo/rules/links.d.ts +2 -0
  39. package/dist/seo/rules/links.js +150 -0
  40. package/dist/seo/rules/local.d.ts +2 -0
  41. package/dist/seo/rules/local.js +265 -0
  42. package/dist/seo/rules/meta.d.ts +2 -0
  43. package/dist/seo/rules/meta.js +523 -0
  44. package/dist/seo/rules/mobile.d.ts +2 -0
  45. package/dist/seo/rules/mobile.js +71 -0
  46. package/dist/seo/rules/performance.d.ts +2 -0
  47. package/dist/seo/rules/performance.js +246 -0
  48. package/dist/seo/rules/pwa.d.ts +2 -0
  49. package/dist/seo/rules/pwa.js +302 -0
  50. package/dist/seo/rules/readability.d.ts +2 -0
  51. package/dist/seo/rules/readability.js +255 -0
  52. package/dist/seo/rules/schema.d.ts +2 -0
  53. package/dist/seo/rules/schema.js +54 -0
  54. package/dist/seo/rules/security.d.ts +2 -0
  55. package/dist/seo/rules/security.js +525 -0
  56. package/dist/seo/rules/social.d.ts +2 -0
  57. package/dist/seo/rules/social.js +373 -0
  58. package/dist/seo/rules/structural.d.ts +2 -0
  59. package/dist/seo/rules/structural.js +155 -0
  60. package/dist/seo/rules/technical.d.ts +2 -0
  61. package/dist/seo/rules/technical.js +223 -0
  62. package/dist/seo/rules/thresholds.d.ts +196 -0
  63. package/dist/seo/rules/thresholds.js +118 -0
  64. package/dist/seo/rules/types.d.ts +346 -0
  65. package/dist/seo/rules/types.js +11 -0
  66. package/dist/seo/types.d.ts +160 -0
  67. package/dist/seo/types.js +1 -0
  68. package/dist/utils/columns.d.ts +14 -0
  69. package/dist/utils/columns.js +69 -0
  70. package/package.json +1 -1
@@ -76,6 +76,7 @@ export function extractImages($, options) {
76
76
  height: height ? parseInt(height, 10) : undefined,
77
77
  srcset: $el.attr('srcset'),
78
78
  loading: $el.attr('loading'),
79
+ decoding: $el.attr('decoding'),
79
80
  });
80
81
  });
81
82
  return images;
@@ -117,7 +118,7 @@ export function extractMeta($) {
117
118
  meta.author = content;
118
119
  break;
119
120
  case 'robots':
120
- meta.robots = content;
121
+ meta.robots = content.split(',').map((r) => r.trim().toLowerCase());
121
122
  break;
122
123
  case 'viewport':
123
124
  meta.viewport = content;
@@ -14,13 +14,14 @@ export interface ExtractedImage {
14
14
  height?: number;
15
15
  srcset?: string;
16
16
  loading?: 'lazy' | 'eager';
17
+ decoding?: 'async' | 'auto' | 'sync';
17
18
  }
18
19
  export interface ExtractedMeta {
19
20
  title?: string;
20
21
  description?: string;
21
22
  keywords?: string[];
22
23
  author?: string;
23
- robots?: string;
24
+ robots?: string[];
24
25
  canonical?: string;
25
26
  viewport?: string;
26
27
  charset?: string;
package/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ import { program } from 'commander';
3
3
  import { promises as fs } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import colors from '../utils/colors.js';
6
+ import { formatColumns } from '../utils/columns.js';
6
7
  async function readStdin() {
7
8
  if (process.stdin.isTTY) {
8
9
  return null;
@@ -120,7 +121,13 @@ async function main() {
120
121
  }
121
122
  return { method, url, headers, data };
122
123
  }
123
- const PRESET_NAMES = Object.keys(presets).filter(k => k !== 'registry' && !k.startsWith('_'));
124
+ const utilityFunctions = [
125
+ 'registry', 'presetRegistry', 'detectPreset', 'getPreset',
126
+ 'listPresets', 'listAIPresets', 'listCloudPresets', 'listSaaSPresets', 'listDevToolsPresets'
127
+ ];
128
+ const PRESET_NAMES = Object.keys(presets)
129
+ .filter(k => !utilityFunctions.includes(k) && !k.startsWith('_') && typeof presets[k] === 'function')
130
+ .sort();
124
131
  program
125
132
  .name('rek')
126
133
  .description('The HTTP Client for Humans (and Robots)')
@@ -131,7 +138,7 @@ async function main() {
131
138
  .option('-o, --output <file>', 'Write response body to file')
132
139
  .option('-j, --json', 'Force JSON content-type')
133
140
  .option('-e, --env [path]', 'Load .env file from current directory or specified path')
134
- .addHelpText('after', `
141
+ .addHelpText('after', () => `
135
142
  ${colors.bold(colors.yellow('Examples:'))}
136
143
  ${colors.green('$ rek httpbin.org/json')}
137
144
  ${colors.green('$ rek post api.com/users name="Cyber" role="Admin"')}
@@ -139,7 +146,7 @@ ${colors.bold(colors.yellow('Examples:'))}
139
146
  ${colors.green('$ rek @openai/v1/chat/completions model="gpt-5.1"')}
140
147
 
141
148
  ${colors.bold(colors.yellow('Available Presets:'))}
142
- ${colors.cyan(PRESET_NAMES.map(p => '@' + p).join(', '))}
149
+ ${formatColumns(PRESET_NAMES, { prefix: '@', indent: 2, minWidth: 16, transform: colors.cyan })}
143
150
  `)
144
151
  .action(async (args, options) => {
145
152
  if (args.length === 0) {
@@ -381,6 +388,138 @@ ${colors.bold('Details:')}`);
381
388
  process.exit(1);
382
389
  }
383
390
  });
391
+ program
392
+ .command('seo')
393
+ .description('Analyze page SEO (title, meta, headings, links, images, structured data)')
394
+ .argument('<url>', 'URL to analyze')
395
+ .option('-a, --all', 'Show all checks including passed ones')
396
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
397
+ .addHelpText('after', `
398
+ ${colors.bold(colors.yellow('Examples:'))}
399
+ ${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
400
+ ${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
401
+ ${colors.green('$ rek seo example.com --format json')} ${colors.gray('Output as JSON')}
402
+ ${colors.green('$ rek seo example.com --format json | jq')} ${colors.gray('Pipe to jq for processing')}
403
+
404
+ ${colors.bold(colors.yellow('Checks:'))}
405
+ ${colors.cyan('Title Tag')} Length and presence
406
+ ${colors.cyan('Meta Description')} Length and presence
407
+ ${colors.cyan('Headings')} H1 presence and hierarchy
408
+ ${colors.cyan('Images')} Alt text coverage
409
+ ${colors.cyan('Links')} Internal/external distribution
410
+ ${colors.cyan('OpenGraph')} Social sharing meta tags
411
+ ${colors.cyan('Twitter Card')} Twitter sharing meta tags
412
+ ${colors.cyan('Structured Data')} JSON-LD presence
413
+ ${colors.cyan('Technical')} Canonical, viewport, lang
414
+ `)
415
+ .action(async (url, options) => {
416
+ if (!url.startsWith('http'))
417
+ url = `https://${url}`;
418
+ const isJson = options.format === 'json';
419
+ const { createClient } = await import('../core/client.js');
420
+ const { analyzeSeo } = await import('../seo/analyzer.js');
421
+ if (!isJson) {
422
+ console.log(colors.gray(`Analyzing SEO for ${url}...`));
423
+ }
424
+ try {
425
+ const client = createClient({ timeout: 30000 });
426
+ const res = await client.get(url);
427
+ const html = await res.text();
428
+ const report = await analyzeSeo(html, { baseUrl: url });
429
+ if (isJson) {
430
+ const jsonOutput = {
431
+ url,
432
+ analyzedAt: new Date().toISOString(),
433
+ score: report.score,
434
+ grade: report.grade,
435
+ title: report.title,
436
+ metaDescription: report.metaDescription,
437
+ content: report.content,
438
+ headings: report.headings,
439
+ links: report.links,
440
+ images: report.images,
441
+ openGraph: report.social.openGraph,
442
+ twitterCard: report.social.twitterCard,
443
+ jsonLd: report.jsonLd,
444
+ technical: report.technical,
445
+ checks: report.checks,
446
+ summary: {
447
+ total: report.checks.length,
448
+ passed: report.checks.filter(c => c.status === 'pass').length,
449
+ warnings: report.checks.filter(c => c.status === 'warn').length,
450
+ errors: report.checks.filter(c => c.status === 'fail').length,
451
+ info: report.checks.filter(c => c.status === 'info').length,
452
+ },
453
+ };
454
+ console.log(JSON.stringify(jsonOutput, null, 2));
455
+ return;
456
+ }
457
+ let gradeColor = colors.red;
458
+ if (report.grade === 'A')
459
+ gradeColor = colors.green;
460
+ else if (report.grade === 'B')
461
+ gradeColor = colors.blue;
462
+ else if (report.grade === 'C')
463
+ gradeColor = colors.yellow;
464
+ console.log(`
465
+ ${colors.bold(colors.cyan('🔍 SEO Analysis Report'))}
466
+ ${colors.gray('URL:')} ${url}
467
+ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray('Score:')} ${report.score}/100
468
+ `);
469
+ if (report.title) {
470
+ console.log(`${colors.bold('Title:')} ${colors.gray(report.title.text.slice(0, 60))}${report.title.text.length > 60 ? '...' : ''} ${colors.gray(`(${report.title.length} chars)`)}`);
471
+ }
472
+ if (report.metaDescription) {
473
+ console.log(`${colors.bold('Description:')} ${colors.gray(report.metaDescription.text.slice(0, 80))}${report.metaDescription.text.length > 80 ? '...' : ''}`);
474
+ }
475
+ console.log('');
476
+ console.log(`${colors.bold('Content Metrics:')}`);
477
+ console.log(` ${colors.gray('Words:')} ${report.content.wordCount} ${colors.gray('Reading time:')} ~${report.content.readingTimeMinutes} min`);
478
+ console.log(` ${colors.gray('Headings:')} H1×${report.headings.h1Count}, total ${report.headings.structure.length}`);
479
+ console.log(` ${colors.gray('Links:')} ${report.links.total} (${report.links.internal} internal, ${report.links.external} external)`);
480
+ console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
481
+ console.log('');
482
+ console.log(`${colors.bold('Checks:')}`);
483
+ const checksToShow = options.all
484
+ ? report.checks
485
+ : report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
486
+ if (checksToShow.length === 0 && !options.all) {
487
+ console.log(colors.green(' All checks passed! Use -a to see details.'));
488
+ }
489
+ else {
490
+ for (const check of checksToShow) {
491
+ const icon = check.status === 'pass' ? colors.green('✔')
492
+ : check.status === 'warn' ? colors.yellow('⚠')
493
+ : check.status === 'fail' ? colors.red('✖')
494
+ : colors.gray('ℹ');
495
+ const name = colors.bold(check.name.padEnd(18));
496
+ console.log(` ${icon} ${name} ${check.message}`);
497
+ if (check.recommendation && check.status !== 'pass') {
498
+ console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
499
+ }
500
+ const evidence = check.evidence;
501
+ if (evidence && check.status !== 'pass') {
502
+ if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
503
+ const items = evidence.found.slice(0, 3);
504
+ console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
505
+ }
506
+ if (evidence.example) {
507
+ console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ console.log('');
513
+ if (report.jsonLd.count > 0) {
514
+ console.log(`${colors.bold('Structured Data:')} ${report.jsonLd.types.join(', ') || 'Present'}`);
515
+ console.log('');
516
+ }
517
+ }
518
+ catch (error) {
519
+ console.error(colors.red(`SEO analysis failed: ${error.message}`));
520
+ process.exit(1);
521
+ }
522
+ });
384
523
  program
385
524
  .command('ip')
386
525
  .description('Get IP address intelligence using local GeoLite2 database')
@@ -43,6 +43,7 @@ export declare class RekShell {
43
43
  private runWhois;
44
44
  private runTLS;
45
45
  private runSecurityGrader;
46
+ private runSeo;
46
47
  private runIpIntelligence;
47
48
  private runDNS;
48
49
  private runDNSPropagation;
@@ -50,6 +51,7 @@ export declare class RekShell {
50
51
  private runRDAP;
51
52
  private runPing;
52
53
  private runScrap;
54
+ private runSpider;
53
55
  private runSelect;
54
56
  private runSelectText;
55
57
  private runSelectAttr;
@@ -10,10 +10,12 @@ import { inspectTLS } from '../../utils/tls-inspector.js';
10
10
  import { getSecurityRecords } from '../../utils/dns-toolkit.js';
11
11
  import { rdap } from '../../utils/rdap.js';
12
12
  import { ScrapeDocument } from '../../scrape/document.js';
13
+ import { Spider } from '../../scrape/spider.js';
13
14
  import colors from '../../utils/colors.js';
14
15
  import { getShellSearch } from './shell-search.js';
15
16
  import { openSearchPanel } from './search-panel.js';
16
17
  import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
18
+ import { analyzeSeo } from '../../seo/index.js';
17
19
  let highlight;
18
20
  async function initDependencies() {
19
21
  if (!highlight) {
@@ -93,7 +95,7 @@ export class RekShell {
93
95
  'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
94
96
  'ws', 'udp', 'load', 'chat', 'ai',
95
97
  'whois', 'tls', 'ssl', 'security', 'ip', 'dns', 'dns:propagate', 'dns:email', 'rdap', 'ping',
96
- 'scrap', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
98
+ 'scrap', 'spider', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
97
99
  '?', 'search', 'suggest', 'example',
98
100
  'help', 'clear', 'exit', 'set', 'url', 'vars', 'env'
99
101
  ];
@@ -343,6 +345,9 @@ export class RekShell {
343
345
  case 'security':
344
346
  await this.runSecurityGrader(parts[1]);
345
347
  return;
348
+ case 'seo':
349
+ await this.runSeo(parts[1], parts.includes('-a') || parts.includes('--all'), parts.includes('--format') && parts[parts.indexOf('--format') + 1] === 'json');
350
+ return;
346
351
  case 'ip':
347
352
  await this.runIpIntelligence(parts[1]);
348
353
  return;
@@ -364,6 +369,9 @@ export class RekShell {
364
369
  case 'scrap':
365
370
  await this.runScrap(parts[1]);
366
371
  return;
372
+ case 'spider':
373
+ await this.runSpider(parts.slice(1));
374
+ return;
367
375
  case '$':
368
376
  await this.runSelect(parts.slice(1).join(' '));
369
377
  return;
@@ -944,6 +952,156 @@ ${colors.bold('Details:')}`);
944
952
  }
945
953
  console.log('');
946
954
  }
955
+ async runSeo(url, showAll = false, jsonOutput = false) {
956
+ if (!url) {
957
+ url = this.currentDocUrl || this.baseUrl || '';
958
+ if (!url) {
959
+ console.log(colors.yellow('Usage: seo <url> [-a] [--format json]'));
960
+ console.log(colors.gray(' Examples: seo google.com | seo https://example.com -a'));
961
+ console.log(colors.gray(' -a, --all Show all checks (including passed)'));
962
+ console.log(colors.gray(' --format json Output raw JSON for programmatic use'));
963
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
964
+ return;
965
+ }
966
+ }
967
+ else if (!url.startsWith('http') && !url.startsWith('-')) {
968
+ url = `https://${url}`;
969
+ }
970
+ if (!jsonOutput) {
971
+ console.log(colors.gray(`Analyzing SEO for ${url}...`));
972
+ }
973
+ const startTime = performance.now();
974
+ try {
975
+ const res = await this.client.get(url);
976
+ const html = await res.text();
977
+ const duration = Math.round(performance.now() - startTime);
978
+ const report = await analyzeSeo(html, { baseUrl: url });
979
+ if (jsonOutput) {
980
+ const jsonResult = {
981
+ url,
982
+ analyzedAt: new Date().toISOString(),
983
+ durationMs: duration,
984
+ score: report.score,
985
+ grade: report.grade,
986
+ title: report.title,
987
+ metaDescription: report.metaDescription,
988
+ content: report.content,
989
+ headings: report.headings,
990
+ links: report.links,
991
+ images: report.images,
992
+ openGraph: report.social.openGraph,
993
+ twitterCard: report.social.twitterCard,
994
+ jsonLd: report.jsonLd,
995
+ technical: report.technical,
996
+ checks: report.checks,
997
+ summary: {
998
+ total: report.checks.length,
999
+ passed: report.checks.filter(c => c.status === 'pass').length,
1000
+ warnings: report.checks.filter(c => c.status === 'warn').length,
1001
+ errors: report.checks.filter(c => c.status === 'fail').length,
1002
+ info: report.checks.filter(c => c.status === 'info').length,
1003
+ },
1004
+ };
1005
+ console.log(JSON.stringify(jsonResult, null, 2));
1006
+ this.lastResponse = jsonResult;
1007
+ return;
1008
+ }
1009
+ let gradeColor = colors.red;
1010
+ if (report.grade === 'A')
1011
+ gradeColor = colors.green;
1012
+ else if (report.grade === 'B')
1013
+ gradeColor = colors.blue;
1014
+ else if (report.grade === 'C')
1015
+ gradeColor = colors.yellow;
1016
+ else if (report.grade === 'D')
1017
+ gradeColor = colors.magenta;
1018
+ console.log(`
1019
+ ${colors.bold(colors.cyan('🔍 SEO Analysis Report'))} ${colors.gray(`(${duration}ms)`)}
1020
+ Grade: ${gradeColor(colors.bold(report.grade))} (${report.score}/100)
1021
+ `);
1022
+ if (report.title) {
1023
+ console.log(colors.bold('Title:') + ` ${report.title.text} ` + colors.gray(`(${report.title.length} chars)`));
1024
+ }
1025
+ if (report.metaDescription) {
1026
+ const desc = report.metaDescription.text.length > 80
1027
+ ? report.metaDescription.text.slice(0, 77) + '...'
1028
+ : report.metaDescription.text;
1029
+ console.log(colors.bold('Description:') + ` ${desc} ` + colors.gray(`(${report.metaDescription.length} chars)`));
1030
+ }
1031
+ if (report.content) {
1032
+ console.log(colors.bold('Content:') + ` ${report.content.wordCount} words, ${report.content.paragraphCount} paragraphs, ~${report.content.readingTimeMinutes} min read`);
1033
+ }
1034
+ console.log('');
1035
+ console.log(colors.bold('Checks:'));
1036
+ const checksToShow = showAll
1037
+ ? report.checks
1038
+ : report.checks.filter(c => c.status !== 'pass');
1039
+ const failed = checksToShow.filter(c => c.status === 'fail');
1040
+ const warnings = checksToShow.filter(c => c.status === 'warn');
1041
+ const info = checksToShow.filter(c => c.status === 'info');
1042
+ const passed = showAll ? checksToShow.filter(c => c.status === 'pass') : [];
1043
+ const displayCheck = (check) => {
1044
+ let icon;
1045
+ let nameColor;
1046
+ switch (check.status) {
1047
+ case 'pass':
1048
+ icon = colors.green('✔');
1049
+ nameColor = colors.green;
1050
+ break;
1051
+ case 'warn':
1052
+ icon = colors.yellow('⚠');
1053
+ nameColor = colors.yellow;
1054
+ break;
1055
+ case 'fail':
1056
+ icon = colors.red('✖');
1057
+ nameColor = colors.red;
1058
+ break;
1059
+ default:
1060
+ icon = colors.blue('ℹ');
1061
+ nameColor = colors.blue;
1062
+ }
1063
+ console.log(` ${icon} ${nameColor(check.name.padEnd(22))} ${check.message}`);
1064
+ if (check.recommendation && check.status !== 'pass') {
1065
+ console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
1066
+ }
1067
+ const evidence = check.evidence;
1068
+ if (evidence && check.status !== 'pass') {
1069
+ if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
1070
+ const items = evidence.found.slice(0, 3);
1071
+ console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
1072
+ }
1073
+ if (evidence.example) {
1074
+ console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
1075
+ }
1076
+ }
1077
+ };
1078
+ if (failed.length > 0) {
1079
+ console.log(colors.red(`\n Errors (${failed.length}):`));
1080
+ failed.forEach(displayCheck);
1081
+ }
1082
+ if (warnings.length > 0) {
1083
+ console.log(colors.yellow(`\n Warnings (${warnings.length}):`));
1084
+ warnings.forEach(displayCheck);
1085
+ }
1086
+ if (info.length > 0) {
1087
+ console.log(colors.blue(`\n Info (${info.length}):`));
1088
+ info.forEach(displayCheck);
1089
+ }
1090
+ if (passed.length > 0) {
1091
+ console.log(colors.green(`\n Passed (${passed.length}):`));
1092
+ passed.forEach(displayCheck);
1093
+ }
1094
+ if (!showAll && report.checks.filter(c => c.status === 'pass').length > 0) {
1095
+ console.log(colors.gray(`\n ${report.checks.filter(c => c.status === 'pass').length} checks passed. Use -a to show all.`));
1096
+ }
1097
+ console.log('');
1098
+ this.lastResponse = report;
1099
+ }
1100
+ catch (error) {
1101
+ console.error(colors.red(`SEO analysis failed: ${error.message}`));
1102
+ }
1103
+ console.log('');
1104
+ }
947
1105
  async runIpIntelligence(address) {
948
1106
  if (!address) {
949
1107
  console.log(colors.yellow('Usage: ip <address>'));
@@ -1280,6 +1438,105 @@ ${colors.bold('Network:')}
1280
1438
  }
1281
1439
  console.log('');
1282
1440
  }
1441
+ async runSpider(args) {
1442
+ let url = '';
1443
+ let maxDepth = 3;
1444
+ let maxPages = 100;
1445
+ let concurrency = 5;
1446
+ for (let i = 0; i < args.length; i++) {
1447
+ const arg = args[i];
1448
+ if (arg.startsWith('--depth=') || arg.startsWith('-d=')) {
1449
+ maxDepth = parseInt(arg.split('=')[1]) || 3;
1450
+ }
1451
+ else if (arg.startsWith('--limit=') || arg.startsWith('-l=')) {
1452
+ maxPages = parseInt(arg.split('=')[1]) || 100;
1453
+ }
1454
+ else if (arg.startsWith('--concurrency=') || arg.startsWith('-c=')) {
1455
+ concurrency = parseInt(arg.split('=')[1]) || 5;
1456
+ }
1457
+ else if (!arg.startsWith('-')) {
1458
+ url = arg;
1459
+ }
1460
+ }
1461
+ if (!url) {
1462
+ if (!this.baseUrl) {
1463
+ console.log(colors.yellow('Usage: spider <url> [options]'));
1464
+ console.log(colors.gray(' Options:'));
1465
+ console.log(colors.gray(' --depth=3 Max crawl depth'));
1466
+ console.log(colors.gray(' --limit=100 Max pages to crawl'));
1467
+ console.log(colors.gray(' --concurrency=5 Concurrent requests'));
1468
+ console.log(colors.gray(' Examples:'));
1469
+ console.log(colors.gray(' spider https://example.com'));
1470
+ console.log(colors.gray(' spider https://example.com --depth=2 --limit=50'));
1471
+ return;
1472
+ }
1473
+ url = this.baseUrl;
1474
+ }
1475
+ else if (!url.startsWith('http')) {
1476
+ url = `https://${url}`;
1477
+ }
1478
+ console.log(colors.cyan(`\nSpider starting: ${url}`));
1479
+ console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}`));
1480
+ console.log('');
1481
+ const spider = new Spider({
1482
+ maxDepth,
1483
+ maxPages,
1484
+ concurrency,
1485
+ sameDomain: true,
1486
+ delay: 100,
1487
+ onProgress: (progress) => {
1488
+ process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
1489
+ },
1490
+ });
1491
+ try {
1492
+ const result = await spider.crawl(url);
1493
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
1494
+ console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
1495
+ console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
1496
+ console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
1497
+ console.log(` ${colors.cyan('Errors')}: ${result.errors.length}`);
1498
+ const byDepth = new Map();
1499
+ for (const page of result.pages) {
1500
+ byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
1501
+ }
1502
+ console.log(colors.bold('\n Pages by depth:'));
1503
+ for (const [depth, count] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
1504
+ const bar = '█'.repeat(Math.min(count, 40));
1505
+ console.log(` ${colors.gray(`d${depth}:`)} ${bar} ${count}`);
1506
+ }
1507
+ const topPages = [...result.pages]
1508
+ .filter(p => !p.error)
1509
+ .sort((a, b) => b.links.length - a.links.length)
1510
+ .slice(0, 10);
1511
+ if (topPages.length > 0) {
1512
+ console.log(colors.bold('\n Top pages by outgoing links:'));
1513
+ for (const page of topPages) {
1514
+ const title = page.title.slice(0, 40) || new URL(page.url).pathname;
1515
+ console.log(` ${colors.cyan(page.links.length.toString().padStart(3))} ${title}`);
1516
+ }
1517
+ }
1518
+ if (result.errors.length > 0 && result.errors.length <= 10) {
1519
+ console.log(colors.bold('\n Errors:'));
1520
+ for (const err of result.errors) {
1521
+ const path = new URL(err.url).pathname;
1522
+ console.log(` ${colors.red('✗')} ${path.slice(0, 40)} ${colors.gray('→')} ${err.error.slice(0, 30)}`);
1523
+ }
1524
+ }
1525
+ else if (result.errors.length > 10) {
1526
+ console.log(colors.yellow(`\n ${result.errors.length} errors (showing first 10):`));
1527
+ for (const err of result.errors.slice(0, 10)) {
1528
+ const path = new URL(err.url).pathname;
1529
+ console.log(` ${colors.red('✗')} ${path.slice(0, 40)} ${colors.gray('→')} ${err.error.slice(0, 30)}`);
1530
+ }
1531
+ }
1532
+ this.lastResponse = result;
1533
+ console.log(colors.gray('\n Result stored in lastResponse. Use $links to explore.'));
1534
+ }
1535
+ catch (error) {
1536
+ console.error(colors.red(`Spider failed: ${error.message}`));
1537
+ }
1538
+ console.log('');
1539
+ }
1283
1540
  async runSelect(selector) {
1284
1541
  if (!this.currentDoc) {
1285
1542
  console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
@@ -2182,6 +2439,9 @@ ${colors.bold('Network:')}
2182
2439
  ${colors.green('dns <domain>')} Full DNS lookup (A, AAAA, MX, NS, SPF, DMARC).
2183
2440
  ${colors.green('rdap <domain>')} RDAP lookup (modern WHOIS).
2184
2441
  ${colors.green('ping <host>')} Quick TCP connectivity check.
2442
+ ${colors.green('seo <url> [-a] [--format json]')} SEO analysis (70+ rules).
2443
+ ${colors.gray('-a, --all Show all checks including passed')}
2444
+ ${colors.gray('--format json Output raw JSON for programmatic use')}
2185
2445
 
2186
2446
  ${colors.bold('Web Scraping:')}
2187
2447
  ${colors.green('scrap <url>')} Fetch and parse HTML document.
@@ -2201,6 +2461,13 @@ ${colors.bold('Network:')}
2201
2461
  ${colors.green('$beautify:save [f]')} Save beautified code to file.
2202
2462
  ${colors.green('$table <selector>')} Extract table as data.
2203
2463
 
2464
+ ${colors.bold('Web Crawler:')}
2465
+ ${colors.green('spider <url>')} Crawl website following internal links.
2466
+ ${colors.gray('Options:')}
2467
+ ${colors.white('--depth=3')} ${colors.gray('Maximum depth to crawl')}
2468
+ ${colors.white('--limit=100')} ${colors.gray('Maximum pages to crawl')}
2469
+ ${colors.white('--concurrency=5')} ${colors.gray('Parallel requests')}
2470
+
2204
2471
  ${colors.bold('Documentation:')}
2205
2472
  ${colors.green('? <query>')} Search Recker documentation.
2206
2473
  ${colors.green('search <query>')} Alias for ? (hybrid fuzzy+semantic search).
@@ -2218,6 +2485,7 @@ ${colors.bold('Network:')}
2218
2485
  › post /post name="Neo" active:=true role:Admin
2219
2486
  › load /heavy-endpoint users=100 mode=stress
2220
2487
  › chat openai gpt-5.1
2488
+ › spider https://example.com --depth=2 --limit=50
2221
2489
  `);
2222
2490
  }
2223
2491
  }
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
42
42
  export * from './plugins/xml.js';
43
43
  export * from './plugins/scrape.js';
44
44
  export * from './scrape/index.js';
45
+ export * from './seo/index.js';
45
46
  export * from './plugins/server-timing.js';
46
47
  export * from './plugins/auth.js';
47
48
  export * from './plugins/proxy-rotator.js';
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ export * from './plugins/graphql.js';
42
42
  export * from './plugins/xml.js';
43
43
  export * from './plugins/scrape.js';
44
44
  export * from './scrape/index.js';
45
+ export * from './seo/index.js';
45
46
  export * from './plugins/server-timing.js';
46
47
  export * from './plugins/auth.js';
47
48
  export * from './plugins/proxy-rotator.js';
@@ -76,6 +76,7 @@ export function extractImages($, options) {
76
76
  height: height ? parseInt(height, 10) : undefined,
77
77
  srcset: $el.attr('srcset'),
78
78
  loading: $el.attr('loading'),
79
+ decoding: $el.attr('decoding'),
79
80
  });
80
81
  });
81
82
  return images;
@@ -117,7 +118,7 @@ export function extractMeta($) {
117
118
  meta.author = content;
118
119
  break;
119
120
  case 'robots':
120
- meta.robots = content;
121
+ meta.robots = content.split(',').map((r) => r.trim().toLowerCase());
121
122
  break;
122
123
  case 'viewport':
123
124
  meta.viewport = content;
@@ -1,4 +1,6 @@
1
1
  export { ScrapeDocument } from './document.js';
2
2
  export { ScrapeElement } from './element.js';
3
+ export { Spider, spider } from './spider.js';
4
+ export type { SpiderOptions, SpiderPageResult, SpiderProgress, SpiderResult, } from './spider.js';
3
5
  export { extractLinks, extractImages, extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractForms, extractTables, extractScripts, extractStyles, } from './extractors.js';
4
6
  export type { ExtractedLink, ExtractedImage, ExtractedMeta, OpenGraphData, TwitterCardData, JsonLdData, ExtractedForm, ExtractedFormField, ExtractedTable, ExtractedScript, ExtractedStyle, ExtractionSchema, ExtractionSchemaField, ScrapeOptions, LinkExtractionOptions, ImageExtractionOptions, } from './types.js';
@@ -1,3 +1,4 @@
1
1
  export { ScrapeDocument } from './document.js';
2
2
  export { ScrapeElement } from './element.js';
3
+ export { Spider, spider } from './spider.js';
3
4
  export { extractLinks, extractImages, extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractForms, extractTables, extractScripts, extractStyles, } from './extractors.js';