recker 1.0.79 → 1.0.80-next.70ea84d

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 (196) hide show
  1. package/dist/browser/browser/mini.d.ts +2 -2
  2. package/dist/browser/browser/recker-mini.d.ts +8 -8
  3. package/dist/browser/browser/recker.d.ts +11 -8
  4. package/dist/browser/browser/recker.js +54 -6
  5. package/dist/browser/core/client.d.ts +15 -10
  6. package/dist/browser/core/client.js +54 -38
  7. package/dist/browser/index.iife.min.js +129 -130
  8. package/dist/browser/index.min.js +129 -130
  9. package/dist/browser/index.mini.iife.js +5697 -636
  10. package/dist/browser/index.mini.iife.min.js +47 -48
  11. package/dist/browser/index.mini.min.js +47 -48
  12. package/dist/browser/index.mini.umd.js +5697 -636
  13. package/dist/browser/index.mini.umd.min.js +47 -48
  14. package/dist/browser/index.umd.min.js +129 -130
  15. package/dist/browser/mini.d.ts +2 -2
  16. package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
  17. package/dist/browser/plugins/proxy-rotator.js +6 -28
  18. package/dist/browser/recker-mini.d.ts +8 -8
  19. package/dist/browser/recker.d.ts +11 -8
  20. package/dist/browser/recker.js +54 -6
  21. package/dist/browser/scrape/document.js +2 -2
  22. package/dist/browser/scrape/element.js +7 -1
  23. package/dist/browser/scrape/parser/nodes/html.js +1 -1
  24. package/dist/browser/scrape/spider.d.ts +52 -0
  25. package/dist/browser/scrape/spider.js +620 -38
  26. package/dist/browser/scrape/types.d.ts +2 -0
  27. package/dist/browser/search/google.d.ts +26 -1
  28. package/dist/browser/search/google.js +427 -45
  29. package/dist/browser/seo/analyzer.d.ts +1 -0
  30. package/dist/browser/seo/analyzer.js +144 -1
  31. package/dist/browser/seo/index.d.ts +1 -1
  32. package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
  33. package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
  34. package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  35. package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
  36. package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
  37. package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
  38. package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
  39. package/dist/browser/seo/keyword-campaign-shared.js +59 -0
  40. package/dist/browser/seo/keyword-campaign.d.ts +4 -107
  41. package/dist/browser/seo/keyword-campaign.js +2 -380
  42. package/dist/browser/seo/keywords.js +5 -22
  43. package/dist/browser/seo/types.d.ts +19 -0
  44. package/dist/browser/transport/curl.d.ts +5 -1
  45. package/dist/browser/transport/curl.js +207 -50
  46. package/dist/browser/transport/undici.d.ts +4 -3
  47. package/dist/browser/transport/undici.js +123 -49
  48. package/dist/browser/types/index.d.ts +9 -3
  49. package/dist/browser/utils/binary-manager.js +26 -3
  50. package/dist/browser/utils/block-detector.d.ts +8 -0
  51. package/dist/browser/utils/block-detector.js +542 -7
  52. package/dist/cli/commands/hls-runner.js +5 -4
  53. package/dist/cli/commands/live-runner.js +5 -4
  54. package/dist/cli/commands/loadtest-runner.js +3 -2
  55. package/dist/cli/commands/search.d.ts +2 -0
  56. package/dist/cli/commands/search.js +105 -0
  57. package/dist/cli/commands/seo-runner.js +9 -7
  58. package/dist/cli/commands/seo.js +140 -1
  59. package/dist/cli/commands/serve.js +75 -131
  60. package/dist/cli/commands/server-runner.js +59 -82
  61. package/dist/cli/commands/spider-runner.d.ts +37 -1
  62. package/dist/cli/commands/spider-runner.js +134 -10
  63. package/dist/cli/commands/spider.d.ts +18 -1
  64. package/dist/cli/commands/spider.js +457 -27
  65. package/dist/cli/events/handlers/cli.js +30 -1
  66. package/dist/cli/events/handlers/tui.js +26 -0
  67. package/dist/cli/events/types.d.ts +27 -0
  68. package/dist/cli/handler.d.ts +2 -12
  69. package/dist/cli/handler.js +20 -15
  70. package/dist/cli/handlers/protocols.js +39 -12
  71. package/dist/cli/handlers/search.d.ts +2 -0
  72. package/dist/cli/handlers/search.js +171 -0
  73. package/dist/cli/handlers/seo-analyze.d.ts +1 -0
  74. package/dist/cli/handlers/seo-analyze.js +666 -0
  75. package/dist/cli/handlers/seo-robots.d.ts +1 -0
  76. package/dist/cli/handlers/seo-robots.js +76 -0
  77. package/dist/cli/handlers/seo-serp.d.ts +54 -0
  78. package/dist/cli/handlers/seo-serp.js +243 -0
  79. package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
  80. package/dist/cli/handlers/seo-sitemap.js +55 -0
  81. package/dist/cli/handlers/seo-spider.d.ts +1 -0
  82. package/dist/cli/handlers/seo-spider.js +334 -0
  83. package/dist/cli/handlers/seo.d.ts +8 -4
  84. package/dist/cli/handlers/seo.js +294 -442
  85. package/dist/cli/handlers/spider.js +94 -17
  86. package/dist/cli/handlers/streaming.js +5 -1
  87. package/dist/cli/index.js +11 -2
  88. package/dist/cli/presets.d.ts +1 -1
  89. package/dist/cli/presets.js +15 -4
  90. package/dist/cli/tui/app.js +6 -1
  91. package/dist/cli/tui/components/rich-response.d.ts +72 -0
  92. package/dist/cli/tui/components/rich-response.js +117 -0
  93. package/dist/cli/tui/executor-commands/background.js +30 -24
  94. package/dist/cli/tui/executor-commands/testing.js +3 -2
  95. package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
  96. package/dist/cli/tui/hooks/useHelp.js +15 -2
  97. package/dist/cli/tui/job-manager.d.ts +4 -4
  98. package/dist/cli/tui/job-manager.js +5 -1
  99. package/dist/cli/tui/spider-tui.d.ts +63 -0
  100. package/dist/cli/tui/spider-tui.js +120 -2
  101. package/dist/cli/types.d.ts +12 -0
  102. package/dist/cli/types.js +1 -0
  103. package/dist/cli/utils/option-helpers.d.ts +10 -0
  104. package/dist/cli/utils/option-helpers.js +63 -0
  105. package/dist/cli/utils/score-color.d.ts +11 -0
  106. package/dist/cli/utils/score-color.js +11 -0
  107. package/dist/cli/utils/serp-campaign.d.ts +53 -0
  108. package/dist/cli/utils/serp-campaign.js +53 -0
  109. package/dist/cli/utils/serp-config.d.ts +26 -0
  110. package/dist/cli/utils/serp-config.js +125 -0
  111. package/dist/core/client.d.ts +15 -10
  112. package/dist/core/client.js +54 -38
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +1 -0
  115. package/dist/mcp/cli.js +35 -34
  116. package/dist/mcp/client.d.ts +2 -2
  117. package/dist/mcp/client.js +20 -2
  118. package/dist/mcp/contract.d.ts +1 -1
  119. package/dist/mcp/profiles.js +5 -1
  120. package/dist/mcp/prompts/index.js +8 -4
  121. package/dist/mcp/resources/index.js +46 -23
  122. package/dist/mcp/server.js +9 -6
  123. package/dist/mcp/tools/protocols.js +9 -2
  124. package/dist/mcp/tools/registry.js +13 -3
  125. package/dist/mcp/tools/seo.js +427 -2
  126. package/dist/mcp/types.d.ts +5 -1
  127. package/dist/plugins/proxy-rotator.d.ts +2 -2
  128. package/dist/plugins/proxy-rotator.js +6 -28
  129. package/dist/raffel/client.d.ts +38 -0
  130. package/dist/raffel/client.js +282 -0
  131. package/dist/raffel/index.d.ts +2 -0
  132. package/dist/raffel/index.js +2 -0
  133. package/dist/raffel/types.d.ts +40 -0
  134. package/dist/raffel/types.js +14 -0
  135. package/dist/recker.d.ts +13 -7
  136. package/dist/recker.js +58 -6
  137. package/dist/scrape/document.js +2 -2
  138. package/dist/scrape/element.js +7 -1
  139. package/dist/scrape/parser/nodes/html.js +1 -1
  140. package/dist/scrape/spider.d.ts +52 -0
  141. package/dist/scrape/spider.js +620 -38
  142. package/dist/scrape/types.d.ts +2 -0
  143. package/dist/search/google.d.ts +26 -1
  144. package/dist/search/google.js +427 -45
  145. package/dist/search/index.d.ts +1 -1
  146. package/dist/seo/analyzer.d.ts +1 -0
  147. package/dist/seo/analyzer.js +144 -1
  148. package/dist/seo/index.d.ts +1 -1
  149. package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
  150. package/dist/seo/keyword-campaign-analyzer.js +538 -0
  151. package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  152. package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
  153. package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
  154. package/dist/seo/keyword-campaign-seed-core.js +525 -0
  155. package/dist/seo/keyword-campaign-shared.d.ts +165 -0
  156. package/dist/seo/keyword-campaign-shared.js +59 -0
  157. package/dist/seo/keyword-campaign.d.ts +4 -107
  158. package/dist/seo/keyword-campaign.js +2 -380
  159. package/dist/seo/keywords.js +5 -22
  160. package/dist/seo/types.d.ts +19 -0
  161. package/dist/template/index.d.ts +1 -1
  162. package/dist/template/types.d.ts +0 -2
  163. package/dist/testing/index.d.ts +1 -22
  164. package/dist/testing/index.js +1 -11
  165. package/dist/transport/curl.d.ts +5 -1
  166. package/dist/transport/curl.js +207 -50
  167. package/dist/transport/undici.d.ts +4 -3
  168. package/dist/transport/undici.js +123 -49
  169. package/dist/types/index.d.ts +9 -3
  170. package/dist/utils/binary-manager.js +26 -3
  171. package/dist/utils/block-detector.d.ts +8 -0
  172. package/dist/utils/block-detector.js +542 -7
  173. package/dist/version.js +1 -1
  174. package/package.json +12 -1
  175. package/dist/testing/mock-dns-server.d.ts +0 -69
  176. package/dist/testing/mock-dns-server.js +0 -269
  177. package/dist/testing/mock-ftp-server.d.ts +0 -89
  178. package/dist/testing/mock-ftp-server.js +0 -562
  179. package/dist/testing/mock-hls-server.d.ts +0 -80
  180. package/dist/testing/mock-hls-server.js +0 -381
  181. package/dist/testing/mock-http-server.d.ts +0 -124
  182. package/dist/testing/mock-http-server.js +0 -343
  183. package/dist/testing/mock-proxy-server.d.ts +0 -108
  184. package/dist/testing/mock-proxy-server.js +0 -615
  185. package/dist/testing/mock-sse-server.d.ts +0 -76
  186. package/dist/testing/mock-sse-server.js +0 -291
  187. package/dist/testing/mock-telnet-server.d.ts +0 -59
  188. package/dist/testing/mock-telnet-server.js +0 -274
  189. package/dist/testing/mock-udp-server.d.ts +0 -43
  190. package/dist/testing/mock-udp-server.js +0 -188
  191. package/dist/testing/mock-websocket-server.d.ts +0 -76
  192. package/dist/testing/mock-websocket-server.js +0 -334
  193. package/dist/testing/mock-whois-server.d.ts +0 -56
  194. package/dist/testing/mock-whois-server.js +0 -234
  195. package/dist/testing/proxy-certs.d.ts +0 -19
  196. package/dist/testing/proxy-certs.js +0 -208
@@ -1,6 +1,151 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import colors from '../../utils/colors.js';
3
3
  import { summarizeErrors, formatErrorSummary } from '../helpers.js';
4
+ import { parseSpiderSerpConfig } from '../utils/serp-config.js';
5
+ import { runSpiderKeywordCampaign, toSerpPayload } from '../utils/serp-campaign.js';
6
+ import { getOptionValue, toBoolean, toNonNegativeInt, toOptionString, toSpiderTransport, } from '../utils/option-helpers.js';
7
+ import { getScoreColor } from '../utils/score-color.js';
8
+ const FOCUS_MODES = ['all', 'links', 'duplicates', 'security', 'ai', 'resources'];
9
+ function normalizeSpiderFocus(value) {
10
+ const raw = toOptionString(value)?.toLowerCase();
11
+ return raw && FOCUS_MODES.includes(raw) ? raw : 'all';
12
+ }
13
+ function toStringOrStringArray(value) {
14
+ if (Array.isArray(value)) {
15
+ return value.map((item) => String(item)).filter(Boolean);
16
+ }
17
+ return toOptionString(value);
18
+ }
19
+ function printSerpCampaignSummary(run, color) {
20
+ const campaign = run.campaign;
21
+ const summary = campaign.summary;
22
+ const foundRate = summary.queriesRequested > 0
23
+ ? Math.round((summary.queriesFound / summary.queriesRequested) * 100)
24
+ : 0;
25
+ console.log(color.bold('\n SERP campaign:'));
26
+ console.log(` ${color.green('Queries')}: ${summary.queriesExecuted}/${summary.queriesRequested} searched | ${color.green(String(summary.queriesFound))} found (${foundRate}%)`);
27
+ console.log(` ${color.blue('Top3')}: ${summary.top3Count} | ${color.blue('Top10')}: ${summary.top10Count}`);
28
+ console.log(` ${color.yellow('Avg position')}: ${summary.avgTopPosition !== null ? String(Math.round(summary.avgTopPosition)) : 'n/a'}`);
29
+ if (campaign.results.length > 0) {
30
+ console.log(` ${color.blue('Keywords')}:`);
31
+ for (const result of campaign.results.slice(0, 15)) {
32
+ const icon = result.found ? color.green('✔') : color.red('✗');
33
+ const pos = result.bestPosition !== null ? color.gray(` #${result.bestPosition}`) : color.gray(' -');
34
+ const url = result.matchedUrl ? color.gray(` ${result.matchedUrl.replace(/^https?:\/\/[^/]+/, '').slice(0, 40)}`) : '';
35
+ console.log(` ${icon} ${result.keyword.padEnd(28)}${pos}${url}`);
36
+ }
37
+ if (campaign.results.length > 15) {
38
+ console.log(color.gray(` ... and ${campaign.results.length - 15} more keywords`));
39
+ }
40
+ }
41
+ if (summary.topOrganicCompetitors.length > 0) {
42
+ console.log(` ${color.blue('Top competitors')}:`);
43
+ for (const competitor of summary.topOrganicCompetitors.slice(0, 3)) {
44
+ const wins = competitor.totalOutperformedQueries ?? 0;
45
+ console.log(` ${color.gray(`${competitor.domain}`)} (${wins} wins / ${competitor.matchedKeywords} matched)`);
46
+ }
47
+ }
48
+ if (campaign.pageComparison.length > 0) {
49
+ console.log(` ${color.blue('Best pages')}:`);
50
+ for (const page of campaign.pageComparison
51
+ .sort((a, b) => b.found - a.found || b.tracked - a.tracked)
52
+ .slice(0, 3)) {
53
+ console.log(` ${color.gray(`${page.pageUrl}`)} ${page.found}/${page.tracked} (${page.appearanceRate.toFixed(1)}%)`);
54
+ }
55
+ }
56
+ }
57
+ function summarizePageSecurity(pages) {
58
+ let blockedPages = 0;
59
+ let captchaPages = 0;
60
+ const captchaProviders = {};
61
+ let attempts = 0;
62
+ let retries = 0;
63
+ let undici = 0;
64
+ let curl = 0;
65
+ let ttfbSum = 0;
66
+ let totalSum = 0;
67
+ let downloadSum = 0;
68
+ let ttfbCount = 0;
69
+ let totalCount = 0;
70
+ let downloadCount = 0;
71
+ const totalPages = pages.length;
72
+ for (const page of pages) {
73
+ const security = page.security;
74
+ attempts += Number.isFinite(security?.attempts ?? NaN) ? (security?.attempts || 0) : 1;
75
+ retries += security?.retryCount ?? 0;
76
+ if (security?.blocked)
77
+ blockedPages += 1;
78
+ if (security?.captchaDetected)
79
+ captchaPages += 1;
80
+ if (security?.captchaDetected) {
81
+ const provider = (security.captchaProvider || '').trim().toLowerCase();
82
+ const key = provider.length > 0 ? provider : 'unknown';
83
+ captchaProviders[key] = (captchaProviders[key] || 0) + 1;
84
+ }
85
+ if (security?.transport === 'undici') {
86
+ undici += 1;
87
+ }
88
+ else if (security?.transport === 'curl') {
89
+ curl += 1;
90
+ }
91
+ const ttfb = page.timings?.ttfb;
92
+ const total = page.timings?.total;
93
+ const download = page.timings?.download;
94
+ if (typeof ttfb === 'number' && Number.isFinite(ttfb)) {
95
+ ttfbSum += ttfb;
96
+ ttfbCount += 1;
97
+ }
98
+ if (typeof total === 'number' && Number.isFinite(total)) {
99
+ totalSum += total;
100
+ totalCount += 1;
101
+ }
102
+ if (typeof download === 'number' && Number.isFinite(download)) {
103
+ downloadSum += download;
104
+ downloadCount += 1;
105
+ }
106
+ }
107
+ return {
108
+ pages: totalPages,
109
+ blockedPages,
110
+ captchaPages,
111
+ captchaProviders,
112
+ attempts: attempts || totalPages || 1,
113
+ retries,
114
+ transportUsage: {
115
+ undici,
116
+ curl,
117
+ },
118
+ avgAttempts: totalPages > 0 ? Number((attempts / totalPages).toFixed(2)) : 0,
119
+ avgTtfbMs: ttfbCount > 0 ? Math.round(ttfbSum / ttfbCount) : undefined,
120
+ avgTotalMs: totalCount > 0 ? Math.round(totalSum / totalCount) : undefined,
121
+ avgDownloadMs: downloadCount > 0 ? Math.round(downloadSum / downloadCount) : undefined,
122
+ };
123
+ }
124
+ function toNumber(value) {
125
+ const parsed = parseFloat(value);
126
+ return Number.isFinite(parsed) ? parsed : 0;
127
+ }
128
+ function printSecuritySummary(summary, securityPages, colors) {
129
+ const blockedRate = securityPages > 0 ? Math.round((summary.blockedPages / securityPages) * 100) : 0;
130
+ const captchaRate = securityPages > 0 ? Math.round((summary.captchaPages / securityPages) * 100) : 0;
131
+ const transportTotal = summary.transportUsage.undici + summary.transportUsage.curl;
132
+ const transportLine = transportTotal > 0
133
+ ? `curl ${summary.transportUsage.curl} (${Math.round((summary.transportUsage.curl / transportTotal) * 100)}%) / undici ${summary.transportUsage.undici} (${Math.round((summary.transportUsage.undici / transportTotal) * 100)}%)`
134
+ : 'transport usage n/a';
135
+ console.log(colors.bold('\n Anti-bot signal:'));
136
+ console.log(` ${colors.red('Blocked')}: ${summary.blockedPages}/${securityPages} (${blockedRate}%)`);
137
+ console.log(` ${colors.yellow('Captcha')}: ${summary.captchaPages}/${securityPages} (${captchaRate}%)`);
138
+ console.log(` ${colors.blue('Attempts')}: avg ${summary.avgAttempts}, retries ${summary.retries}`);
139
+ console.log(` ${colors.cyan('Timings')}: ttfb ${summary.avgTtfbMs ?? 'n/a'}ms · total ${summary.avgTotalMs ?? 'n/a'}ms · download ${summary.avgDownloadMs ?? 'n/a'}ms`);
140
+ console.log(` ${colors.magenta('Transport')}: ${transportLine}`);
141
+ if (Object.keys(summary.captchaProviders).length > 0) {
142
+ const providers = Object.entries(summary.captchaProviders)
143
+ .sort((a, b) => b[1] - a[1])
144
+ .map(([provider, count]) => `${provider}: ${count}`)
145
+ .join(', ');
146
+ console.log(` ${colors.gray('Captcha providers')}: ${providers}`);
147
+ }
148
+ }
4
149
  export async function runSpider(opts) {
5
150
  const url = opts.url;
6
151
  const formatJson = !!opts.json;
@@ -10,9 +155,29 @@ export async function runSpider(opts) {
10
155
  const focusMode = opts.focus || 'all';
11
156
  const respectRobotsTxt = !!opts.robots;
12
157
  const useSitemap = seoEnabled && !opts.noSitemap;
13
- const depth = opts.depth || 10;
14
- const limit = opts.limit || 1000;
15
- const concurrency = opts.concurrency || 5;
158
+ const serpConfig = opts.serpConfig || parseSpiderSerpConfig(opts);
159
+ const shouldRunSerp = seoEnabled && serpConfig.enabled;
160
+ let startUrl = url;
161
+ if (!startUrl.startsWith('http'))
162
+ startUrl = `https://${startUrl}`;
163
+ if (serpConfig.enabled && !seoEnabled) {
164
+ console.log(colors.yellow('\nSERP campaign requires --seo. Skipping SERP checks.'));
165
+ }
166
+ const depth = toNonNegativeInt(opts.depth, 10);
167
+ const limit = toNonNegativeInt(opts.limit, 1000);
168
+ const concurrency = toNonNegativeInt(opts.concurrency, 5);
169
+ const transport = toSpiderTransport(opts.transport) ?? 'auto';
170
+ const preferCurlFirst = toBoolean(opts.preferCurlFirst, true);
171
+ const timeout = toNonNegativeInt(opts.timeout, 10_000);
172
+ const delay = toNonNegativeInt(opts.delay, 100);
173
+ const maxRetryAttempts = toNonNegativeInt(opts.maxRetryAttempts, 3);
174
+ const baseRetryDelayMs = toNonNegativeInt(opts.baseRetryDelayMs, 1_000);
175
+ const maxRetryDelayMs = toNonNegativeInt(opts.maxRetryDelayMs, 12_000);
176
+ const retryBackoffMultiplier = toNonNegativeInt(opts.retryBackoffMultiplier, 2);
177
+ const retryJitterMs = toNonNegativeInt(opts.retryJitterMs, 250);
178
+ const maxDomainBlockStrikes = toNonNegativeInt(opts.maxDomainBlockStrikes, 2);
179
+ const rotateUserAgent = toBoolean(opts.rotateUserAgent, true);
180
+ const randomizeHeaders = toBoolean(opts.randomizeHeaders, true);
16
181
  const parseList = (val) => {
17
182
  if (!val)
18
183
  return undefined;
@@ -26,7 +191,7 @@ export async function runSpider(opts) {
26
191
  if (formatJsonl) {
27
192
  const { runSpiderWithEvents } = await import('./spider-runner.js');
28
193
  try {
29
- await runSpiderWithEvents(url, {
194
+ const crawlResult = await runSpiderWithEvents(url, {
30
195
  depth,
31
196
  limit,
32
197
  concurrency,
@@ -34,16 +199,47 @@ export async function runSpider(opts) {
34
199
  seo: seoEnabled,
35
200
  useSitemap,
36
201
  focus: focusMode,
202
+ transport,
203
+ preferCurlFirst,
204
+ timeout,
205
+ delay,
206
+ maxRetryAttempts,
207
+ baseRetryDelayMs,
208
+ maxRetryDelayMs,
209
+ retryBackoffMultiplier,
210
+ retryJitterMs,
211
+ maxDomainBlockStrikes,
212
+ rotateUserAgent,
213
+ randomizeHeaders,
37
214
  extract: extractSelectors,
38
215
  include: includePatterns,
39
216
  exclude: excludePatterns,
40
217
  jsonl: true,
41
218
  jsonlOutput: outputFile,
42
219
  });
220
+ if (shouldRunSerp && crawlResult.pages?.length > 0) {
221
+ const serpCampaign = await runSpiderKeywordCampaign(startUrl, crawlResult.pages, serpConfig);
222
+ if (serpCampaign) {
223
+ const outputRecord = {
224
+ type: 'serp',
225
+ ...(toSerpPayload(serpCampaign)),
226
+ };
227
+ if (!outputFile) {
228
+ console.log(JSON.stringify(outputRecord));
229
+ return;
230
+ }
231
+ await fs.appendFile(outputFile, `${JSON.stringify(outputRecord)}\n`);
232
+ }
233
+ else if (!outputFile) {
234
+ console.log(colors.yellow('SERP enabled but no valid keyword seeds were found in crawl pages.'));
235
+ }
236
+ return;
237
+ }
43
238
  return;
44
239
  }
45
240
  catch (error) {
46
- console.error(colors.red(`\nSpider failed: ${error.message}`));
241
+ const message = error instanceof Error ? error.message : 'Unknown error';
242
+ console.error(colors.red(`\nSpider failed: ${message}`));
47
243
  process.exit(1);
48
244
  }
49
245
  }
@@ -55,9 +251,6 @@ export async function runSpider(opts) {
55
251
  resources: ['resources', 'performance'],
56
252
  all: [],
57
253
  };
58
- let startUrl = url;
59
- if (!startUrl.startsWith('http'))
60
- startUrl = `https://${startUrl}`;
61
254
  if (!formatJson) {
62
255
  const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
63
256
  const focusLabel = focusMode !== 'all' ? colors.cyan(` [focus: ${focusMode}]`) : '';
@@ -85,13 +278,24 @@ Spider starting: ${startUrl}`));
85
278
  maxPages: limit,
86
279
  concurrency: concurrency,
87
280
  sameDomain: true,
88
- delay: 100,
281
+ delay,
282
+ timeout,
283
+ transport,
284
+ preferCurlFirst,
285
+ maxRetryAttempts,
286
+ baseRetryDelayMs,
287
+ maxRetryDelayMs,
288
+ retryBackoffMultiplier,
289
+ retryJitterMs,
290
+ maxDomainBlockStrikes,
291
+ rotateUserAgent,
292
+ randomizeHeaders,
89
293
  seo: true,
90
294
  respectRobotsTxt,
91
295
  useSitemap,
92
296
  output: outputFile || undefined,
93
297
  focusCategories: focusCategories[focusMode],
94
- focusMode: focusMode,
298
+ focusMode,
95
299
  onProgress: (progress) => {
96
300
  currentCrawled = progress.crawled;
97
301
  if (tui) {
@@ -111,24 +315,66 @@ Spider starting: ${startUrl}`));
111
315
  if (tui) {
112
316
  const bytes = seoPage.metrics?.htmlSize;
113
317
  const timings = seoPage.timings || { total: seoPage.duration };
318
+ const securityTransport = seoPage.security?.transport === 'curl' || seoPage.security?.transport === 'undici'
319
+ ? seoPage.security.transport
320
+ : undefined;
114
321
  tui.updateSeo(seoPage.url, seoPage.seoReport.score, bytes, timings);
322
+ tui.updateSecurity({
323
+ blocked: seoPage.security?.blocked,
324
+ captchaDetected: seoPage.security?.captchaDetected,
325
+ captchaProvider: seoPage.security?.captchaProvider,
326
+ attempts: seoPage.security?.attempts,
327
+ retryCount: seoPage.security?.retryCount,
328
+ transport: securityTransport,
329
+ ttfb: timings.ttfb,
330
+ total: timings.total,
331
+ download: timings.download,
332
+ });
115
333
  }
116
334
  }
117
335
  },
118
336
  });
119
337
  const result = await seoSpider.crawl(startUrl);
338
+ const serpCampaign = shouldRunSerp ? await runSpiderKeywordCampaign(startUrl, result.pages, serpConfig) : undefined;
339
+ const serpPayload = serpCampaign ? toSerpPayload(serpCampaign) : undefined;
120
340
  if (tui) {
341
+ const securitySummary = summarizePageSecurity(result.pages);
121
342
  tui.setComplete({
343
+ security: securitySummary,
122
344
  duplicateTitles: result.summary.duplicateTitles,
123
345
  duplicateDescriptions: result.summary.duplicateDescriptions,
124
346
  orphanPages: result.summary.orphanPages,
125
347
  pagesWithErrors: result.summary.pagesWithErrors,
126
348
  });
349
+ tui.setSecuritySummary(securitySummary);
350
+ if (serpPayload) {
351
+ tui.setSerpSummary({
352
+ queriesRequested: serpPayload.summary.queriesRequested,
353
+ queriesFound: serpPayload.summary.queriesFound,
354
+ avgTopPosition: serpPayload.summary.avgTopPosition,
355
+ top3Count: serpPayload.summary.top3Count,
356
+ top10Count: serpPayload.summary.top10Count,
357
+ topOrganicCompetitors: (serpPayload.summary.topOrganicCompetitors || [])
358
+ .slice(0, 4)
359
+ .map((item) => ({
360
+ domain: item.domain,
361
+ matchedKeywords: item.matchedKeywords,
362
+ totalOutperformedQueries: item.totalOutperformedQueries || 0,
363
+ })),
364
+ topPages: serpPayload.pageComparison.slice(0, 5).map((item) => ({
365
+ pageUrl: item.pageUrl,
366
+ found: item.found,
367
+ tracked: item.tracked,
368
+ appearanceRate: toNumber(item.appearanceRate),
369
+ })),
370
+ });
371
+ }
127
372
  await new Promise(resolve => setTimeout(resolve, 500));
128
373
  tui.stop();
129
374
  await tui.waitUntilExit();
130
375
  }
131
376
  if (formatJson) {
377
+ const securitySummary = summarizePageSecurity(result.pages);
132
378
  const responseTimes = result.pages.filter(p => p.duration > 0).map(p => p.duration);
133
379
  const avgResponseTime = responseTimes.length > 0
134
380
  ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
@@ -172,7 +418,9 @@ Spider starting: ${startUrl}`));
172
418
  duplicateDescriptions: result.summary.duplicateDescriptions,
173
419
  duplicateH1s: result.summary.duplicateH1s,
174
420
  orphanPages: result.summary.orphanPages,
421
+ security: securitySummary,
175
422
  },
423
+ ...(serpPayload ? { serp: serpPayload } : {}),
176
424
  discovery: result.discovery ? {
177
425
  humans: result.discovery.humans.found,
178
426
  llms: result.discovery.llms.found,
@@ -227,10 +475,12 @@ Spider starting: ${startUrl}`));
227
475
  return;
228
476
  }
229
477
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
478
+ const securitySummary = summarizePageSecurity(result.pages);
230
479
  console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
231
480
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
232
481
  console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
233
482
  console.log(` ${colors.cyan('Avg SEO Score')}: ${result.summary.avgScore}/100`);
483
+ printSecuritySummary(securitySummary, securitySummary.pages, colors);
234
484
  const responseTimes = result.pages.filter(p => p.duration > 0).map(p => p.duration);
235
485
  const avgResponseTime = responseTimes.length > 0
236
486
  ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
@@ -381,13 +631,16 @@ Spider starting: ${startUrl}`));
381
631
  const score = page.seoReport?.score || 0;
382
632
  const grade = page.seoReport?.grade || '?';
383
633
  const path = new URL(page.url).pathname;
384
- const scoreColor = score >= 80 ? colors.green : score >= 60 ? colors.yellow : colors.red;
634
+ const scoreColor = getScoreColor(score, colors);
385
635
  console.log(` ${scoreColor(`${score.toString().padStart(3)}`)} ${colors.gray(`[${grade}]`)} ${path.slice(0, 50)}`);
386
636
  }
387
637
  if (uniquePages.length > 5) {
388
638
  console.log(colors.gray(` ... and ${uniquePages.length - 5} more pages`));
389
639
  }
390
640
  }
641
+ if (serpPayload && serpCampaign) {
642
+ printSerpCampaignSummary(serpCampaign, colors);
643
+ }
391
644
  if (outputFile) {
392
645
  console.log(colors.green(`\n Report saved to: ${outputFile}`));
393
646
  }
@@ -399,7 +652,18 @@ Spider starting: ${startUrl}`));
399
652
  maxPages: limit,
400
653
  concurrency: concurrency,
401
654
  sameDomain: true,
402
- delay: 100,
655
+ timeout,
656
+ delay,
657
+ transport,
658
+ preferCurlFirst,
659
+ maxRetryAttempts,
660
+ baseRetryDelayMs,
661
+ maxRetryDelayMs,
662
+ retryBackoffMultiplier,
663
+ retryJitterMs,
664
+ maxDomainBlockStrikes,
665
+ rotateUserAgent,
666
+ randomizeHeaders,
403
667
  respectRobotsTxt,
404
668
  extract: extractSelectors,
405
669
  include: includePatterns?.map(p => new RegExp(p)),
@@ -410,6 +674,7 @@ Spider starting: ${startUrl}`));
410
674
  });
411
675
  const result = await spider.crawl(startUrl);
412
676
  if (formatJson) {
677
+ const securitySummary = summarizePageSecurity(result.pages);
413
678
  const jsonOutput = {
414
679
  startUrl: result.startUrl,
415
680
  startTime: result.startTime,
@@ -425,6 +690,7 @@ Spider starting: ${startUrl}`));
425
690
  successCount: result.pages.filter(p => !p.error).length,
426
691
  errorCount: result.errors.length,
427
692
  uniqueUrls: result.visited.size,
693
+ security: securitySummary,
428
694
  },
429
695
  pages: result.pages.map(p => ({
430
696
  url: p.url,
@@ -464,10 +730,12 @@ Spider starting: ${startUrl}`));
464
730
  return;
465
731
  }
466
732
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
733
+ const securitySummary = summarizePageSecurity(result.pages);
467
734
  console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
468
735
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
469
736
  console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
470
737
  console.log(` ${colors.cyan('Errors')}: ${result.errors.length}`);
738
+ printSecuritySummary(securitySummary, securitySummary.pages, colors);
471
739
  const byDepth = new Map();
472
740
  for (const page of result.pages) {
473
741
  byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
@@ -544,6 +812,7 @@ Spider starting: ${startUrl}`));
544
812
  successCount: result.pages.filter(p => !p.error).length,
545
813
  errorCount: result.errors.length,
546
814
  uniqueUrls: result.visited.size,
815
+ security: securitySummary,
547
816
  },
548
817
  pages: result.pages.map(p => ({
549
818
  url: p.url,
@@ -586,7 +855,8 @@ Spider starting: ${startUrl}`));
586
855
  console.log('');
587
856
  }
588
857
  catch (error) {
589
- console.error(colors.red(`\nSpider failed: ${error.message}`));
858
+ const message = error instanceof Error ? error.message : 'Unknown error';
859
+ console.error(colors.red(`\nSpider failed: ${message}`));
590
860
  process.exit(1);
591
861
  }
592
862
  }
@@ -667,6 +937,150 @@ export function registerSpiderCommand(program) {
667
937
  .option('no-sitemap', {
668
938
  short: 'N',
669
939
  description: 'Disable sitemap.xml crawling (sitemap is used by default in SEO mode)',
940
+ })
941
+ .option('transport', {
942
+ type: 'string',
943
+ default: 'auto',
944
+ description: 'HTTP transport for crawler (auto | undici | curl)',
945
+ })
946
+ .option('prefer-curl-first', {
947
+ type: 'boolean',
948
+ default: true,
949
+ description: 'Prefer curl-impersonate before undici in auto mode',
950
+ })
951
+ .option('timeout', {
952
+ type: 'number',
953
+ default: 10000,
954
+ description: 'Request timeout in ms',
955
+ })
956
+ .option('delay', {
957
+ type: 'number',
958
+ default: 100,
959
+ description: 'Request delay in ms',
960
+ })
961
+ .option('max-retry-attempts', {
962
+ type: 'number',
963
+ default: 3,
964
+ description: 'Max retries for failed or blocked requests',
965
+ })
966
+ .option('base-retry-delay-ms', {
967
+ type: 'number',
968
+ default: 1000,
969
+ description: 'Base retry delay in ms',
970
+ })
971
+ .option('max-retry-delay-ms', {
972
+ type: 'number',
973
+ default: 12000,
974
+ description: 'Maximum retry delay in ms',
975
+ })
976
+ .option('retry-backoff-multiplier', {
977
+ type: 'number',
978
+ default: 2,
979
+ description: 'Retry backoff multiplier',
980
+ })
981
+ .option('retry-jitter-ms', {
982
+ type: 'number',
983
+ default: 250,
984
+ description: 'Random retry jitter in ms',
985
+ })
986
+ .option('max-domain-block-strikes', {
987
+ type: 'number',
988
+ default: 2,
989
+ description: 'Number of strikes before forcing curl in auto mode',
990
+ })
991
+ .option('rotate-user-agent', {
992
+ type: 'boolean',
993
+ default: true,
994
+ description: 'Rotate user-agent per request',
995
+ })
996
+ .option('randomize-headers', {
997
+ type: 'boolean',
998
+ default: true,
999
+ description: 'Randomize request headers',
1000
+ })
1001
+ .option('serp', {
1002
+ type: 'boolean',
1003
+ description: 'Enable SERP campaign for top extracted keywords',
1004
+ })
1005
+ .option('serp-top-keywords', {
1006
+ type: 'number',
1007
+ description: 'Top keywords per page to use as campaign seeds',
1008
+ example: '20',
1009
+ })
1010
+ .option('serp-query-limit', {
1011
+ type: 'number',
1012
+ description: 'Number of SERP queries to execute',
1013
+ example: '10',
1014
+ })
1015
+ .option('serp-results-per-query', {
1016
+ type: 'number',
1017
+ description: 'Results fetched per SERP query',
1018
+ example: '10',
1019
+ })
1020
+ .option('serp-concurrency', {
1021
+ type: 'number',
1022
+ description: 'Number of parallel SERP queries',
1023
+ example: '4',
1024
+ })
1025
+ .option('serp-delay-ms', {
1026
+ type: 'number',
1027
+ description: 'Base delay in ms between SERP queries',
1028
+ example: '450',
1029
+ })
1030
+ .option('serp-delay-jitter-ms', {
1031
+ type: 'number',
1032
+ description: 'Random delay jitter in ms between SERP queries',
1033
+ example: '250',
1034
+ })
1035
+ .option('serp-captcha-cooldown-ms', {
1036
+ type: 'number',
1037
+ description: 'Cooldown in ms after captcha before continuing SERP queries',
1038
+ example: '1200',
1039
+ })
1040
+ .option('serp-retry-count', {
1041
+ type: 'number',
1042
+ description: 'Retry count per SERP query',
1043
+ example: '1',
1044
+ })
1045
+ .option('serp-retry-delay-ms', {
1046
+ type: 'number',
1047
+ description: 'Base delay in ms for SERP query retries',
1048
+ example: '900',
1049
+ })
1050
+ .option('serp-transport', {
1051
+ type: 'string',
1052
+ default: 'curl',
1053
+ description: 'Search transport (auto | undici | curl)',
1054
+ example: 'curl',
1055
+ })
1056
+ .option('serp-source', {
1057
+ type: 'string',
1058
+ default: 'google',
1059
+ description: 'Search source/provider. Current options: google',
1060
+ example: 'google',
1061
+ })
1062
+ .option('serp-timeout', {
1063
+ type: 'number',
1064
+ description: 'Search timeout in ms',
1065
+ example: '15000',
1066
+ })
1067
+ .option('serp-country', {
1068
+ type: 'string',
1069
+ description: 'Search country/region',
1070
+ example: 'br',
1071
+ })
1072
+ .option('serp-gl', {
1073
+ type: 'string',
1074
+ description: 'Google GL parameter',
1075
+ })
1076
+ .option('serp-hl', {
1077
+ type: 'string',
1078
+ description: 'Google HL parameter',
1079
+ })
1080
+ .option('serp-human-profile', {
1081
+ type: 'string',
1082
+ default: 'chrome',
1083
+ description: 'Human-like SERP query profile: chrome or off',
670
1084
  })
671
1085
  .example('rek spider example.com', 'Basic crawl')
672
1086
  .example('rek spider example.com -d 3 -l 50', 'Depth 3, max 50 pages')
@@ -677,25 +1091,41 @@ export function registerSpiderCommand(program) {
677
1091
  .example('rek spider example.com -E "a:href" --json', 'Extract all links as JSON')
678
1092
  .example('rek spider example.com --include "^/blog/"', 'Only crawl /blog/ paths')
679
1093
  .example('rek spider example.com --exclude "/admin/"', 'Skip /admin/ paths')
1094
+ .example('rek spider example.com --seo --serp', 'Run SEO + SERP campaign')
1095
+ .example('rek spider example.com --seo --serp --serp-top-keywords 12 --serp-query-limit 10 --serp-results-per-query 10', 'Tuned SERP campaign')
680
1096
  .example('rek spider example.com --jsonl -o crawl.jsonl', 'Stream to JSONL file')
681
1097
  .example('rek spider example.com -L | jq -c', 'Stream and process with jq')
682
- .action(async (url, args, cmdObj) => {
1098
+ .action(async (url, _args, cmdObj) => {
683
1099
  const options = cmdObj.opts ? cmdObj.opts() : {};
1100
+ const optionBag = options;
684
1101
  await runSpider({
685
1102
  url,
686
- depth: options.depth,
687
- limit: options.limit,
688
- concurrency: options.concurrency,
689
- output: options.output,
690
- focus: options.focus,
691
- seo: options.seo,
692
- robots: options.robots,
693
- json: options.json,
694
- jsonl: options.jsonl,
695
- extract: options.extract,
696
- include: options.include,
697
- exclude: options.exclude,
698
- noSitemap: options.noSitemap || options['no-sitemap'],
1103
+ depth: toNonNegativeInt(getOptionValue(optionBag, 'depth'), 10),
1104
+ limit: toNonNegativeInt(getOptionValue(optionBag, 'limit'), 1000),
1105
+ concurrency: toNonNegativeInt(getOptionValue(optionBag, 'concurrency'), 5),
1106
+ output: toOptionString(getOptionValue(optionBag, 'output')),
1107
+ focus: normalizeSpiderFocus(getOptionValue(optionBag, 'focus')),
1108
+ seo: toBoolean(getOptionValue(optionBag, 'seo')),
1109
+ robots: toBoolean(getOptionValue(optionBag, 'robots')),
1110
+ json: toBoolean(getOptionValue(optionBag, 'json')),
1111
+ jsonl: toBoolean(getOptionValue(optionBag, 'jsonl')),
1112
+ extract: toStringOrStringArray(getOptionValue(optionBag, 'extract')),
1113
+ include: toStringOrStringArray(getOptionValue(optionBag, 'include')),
1114
+ exclude: toStringOrStringArray(getOptionValue(optionBag, 'exclude')),
1115
+ noSitemap: toBoolean(getOptionValue(optionBag, 'noSitemap'), false) || toBoolean(getOptionValue(optionBag, 'no-sitemap'), false),
1116
+ transport: toSpiderTransport(getOptionValue(optionBag, 'transport')) || 'auto',
1117
+ preferCurlFirst: toBoolean(getOptionValue(optionBag, 'preferCurlFirst', 'prefer-curl-first'), true),
1118
+ timeout: toNonNegativeInt(getOptionValue(optionBag, 'timeout'), 10_000),
1119
+ delay: toNonNegativeInt(getOptionValue(optionBag, 'delay'), 100),
1120
+ maxRetryAttempts: toNonNegativeInt(getOptionValue(optionBag, 'maxRetryAttempts', 'max-retry-attempts'), 3),
1121
+ baseRetryDelayMs: toNonNegativeInt(getOptionValue(optionBag, 'baseRetryDelayMs', 'base-retry-delay-ms'), 1_000),
1122
+ maxRetryDelayMs: toNonNegativeInt(getOptionValue(optionBag, 'maxRetryDelayMs', 'max-retry-delay-ms'), 12_000),
1123
+ retryBackoffMultiplier: toNonNegativeInt(getOptionValue(optionBag, 'retryBackoffMultiplier', 'retry-backoff-multiplier'), 2),
1124
+ retryJitterMs: toNonNegativeInt(getOptionValue(optionBag, 'retryJitterMs', 'retry-jitter-ms'), 250),
1125
+ maxDomainBlockStrikes: toNonNegativeInt(getOptionValue(optionBag, 'maxDomainBlockStrikes', 'max-domain-block-strikes'), 2),
1126
+ rotateUserAgent: toBoolean(getOptionValue(optionBag, 'rotateUserAgent', 'rotate-user-agent'), true),
1127
+ randomizeHeaders: toBoolean(getOptionValue(optionBag, 'randomizeHeaders', 'randomize-headers'), true),
1128
+ serpConfig: parseSpiderSerpConfig(options),
699
1129
  });
700
1130
  });
701
1131
  }