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
@@ -5,6 +5,8 @@ import { createHash } from 'node:crypto';
5
5
  import { createClient } from '../../core/client.js';
6
6
  import { analyzeSeo } from '../../seo/analyzer.js';
7
7
  import { seoSpider } from '../../seo/seo-spider.js';
8
+ import { analyzeKeywordCampaign, extractKeywordCampaignSeedsFromReport } from '../../seo/index.js';
9
+ import { parseSpiderSerpConfig, DEFAULT_SERP_KEYWORD_LIMIT, DEFAULT_SERP_MAX_RESULTS, } from '../../cli/utils/serp-config.js';
8
10
  const DEFAULT_SEO_CACHE_TTL_SEC = 6 * 60 * 60;
9
11
  const SEO_REPORT_DIR = join(tmpdir(), 'recker', 'seo');
10
12
  function normalizeStringArray(value) {
@@ -68,6 +70,180 @@ function resolveReportPersistence(args, options) {
68
70
  forceRefresh,
69
71
  };
70
72
  }
73
+ function toSpiderTransport(value) {
74
+ const raw = typeof value === 'string' ? value.toLowerCase().trim() : '';
75
+ if (raw === 'auto' || raw === 'undici' || raw === 'curl') {
76
+ return raw;
77
+ }
78
+ return 'auto';
79
+ }
80
+ function toNonNegativeInt(value, fallback = 0) {
81
+ const parsed = typeof value === 'number'
82
+ ? value
83
+ : typeof value === 'string'
84
+ ? Number(value.trim())
85
+ : fallback;
86
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
87
+ }
88
+ function summarizeCrawlerSecurity(pages) {
89
+ let blockedPages = 0;
90
+ let captchaPages = 0;
91
+ let attempts = 0;
92
+ let attemptsSamples = 0;
93
+ let retries = 0;
94
+ let ttfbSum = 0;
95
+ let ttfbSamples = 0;
96
+ let totalSum = 0;
97
+ let totalSamples = 0;
98
+ let downloadSum = 0;
99
+ let downloadSamples = 0;
100
+ const transportUsage = {
101
+ undici: 0,
102
+ curl: 0,
103
+ };
104
+ for (const page of pages) {
105
+ const security = page.security;
106
+ if (!security) {
107
+ continue;
108
+ }
109
+ const pageAttempts = typeof security.attempts === 'number' && security.attempts >= 1
110
+ ? security.attempts
111
+ : 1;
112
+ attempts += pageAttempts;
113
+ attemptsSamples += 1;
114
+ retries += Math.max(0, pageAttempts - 1);
115
+ if (security.blocked) {
116
+ blockedPages += 1;
117
+ }
118
+ if (security.captchaDetected) {
119
+ captchaPages += 1;
120
+ }
121
+ if (security.transport === 'undici' || security.transport === 'curl') {
122
+ transportUsage[security.transport] += 1;
123
+ }
124
+ const ttfb = page.timings?.ttfb;
125
+ if (typeof ttfb === 'number' && Number.isFinite(ttfb)) {
126
+ ttfbSum += ttfb;
127
+ ttfbSamples += 1;
128
+ }
129
+ const total = page.timings?.total;
130
+ if (typeof total === 'number' && Number.isFinite(total)) {
131
+ totalSum += total;
132
+ totalSamples += 1;
133
+ }
134
+ const download = page.timings?.download;
135
+ if (typeof download === 'number' && Number.isFinite(download)) {
136
+ downloadSum += download;
137
+ downloadSamples += 1;
138
+ }
139
+ }
140
+ return {
141
+ pages: pages.length,
142
+ blockedPages,
143
+ captchaPages,
144
+ attempts,
145
+ retries,
146
+ transportUsage,
147
+ avgAttempts: attemptsSamples > 0 ? Number((attempts / attemptsSamples).toFixed(2)) : 0,
148
+ avgTtfbMs: ttfbSamples > 0 ? Math.round(ttfbSum / ttfbSamples) : undefined,
149
+ avgTotalMs: totalSamples > 0 ? Math.round(totalSum / totalSamples) : undefined,
150
+ avgDownloadMs: downloadSamples > 0 ? Math.round(downloadSum / downloadSamples) : undefined,
151
+ };
152
+ }
153
+ function summarizeCrawlerResources(pages) {
154
+ const totals = {
155
+ images: 0,
156
+ scripts: 0,
157
+ stylesheets: 0,
158
+ };
159
+ for (const page of pages) {
160
+ if (!page.resources) {
161
+ continue;
162
+ }
163
+ totals.images += page.resources.images || 0;
164
+ totals.scripts += page.resources.scripts || 0;
165
+ totals.stylesheets += page.resources.stylesheets || 0;
166
+ }
167
+ return totals;
168
+ }
169
+ function buildSerpCampaignSeeds(pages, topKeywordsPerPage, maxSeeds) {
170
+ const aggregate = new Map();
171
+ for (const page of pages) {
172
+ if (!page.seoReport)
173
+ continue;
174
+ const seeds = extractKeywordCampaignSeedsFromReport(page.seoReport, {
175
+ maxKeywords: topKeywordsPerPage,
176
+ minKeywordLength: 5,
177
+ includeLongTail: false,
178
+ sourcePage: page.url,
179
+ });
180
+ for (const seed of seeds) {
181
+ const current = aggregate.get(seed.normalizedKeyword);
182
+ if (current) {
183
+ current.count += seed.weight;
184
+ if (!current.sourcePage)
185
+ current.sourcePage = seed.sourcePage;
186
+ if (seed.weight > current.sourceWeight) {
187
+ current.sourceWeight = Math.max(1, Math.round(seed.weight));
188
+ }
189
+ continue;
190
+ }
191
+ aggregate.set(seed.normalizedKeyword, {
192
+ keyword: seed.keyword,
193
+ sourcePage: seed.sourcePage,
194
+ sourceWeight: Math.max(1, Math.round(seed.weight)),
195
+ count: seed.weight,
196
+ });
197
+ }
198
+ }
199
+ return [...aggregate.values()]
200
+ .sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword))
201
+ .slice(0, maxSeeds)
202
+ .map(({ keyword, sourcePage, sourceWeight }) => ({
203
+ keyword,
204
+ sourcePage,
205
+ sourceWeight,
206
+ }));
207
+ }
208
+ async function runCrawlerSerpCampaign(targetUrl, pages, config) {
209
+ const seeds = buildSerpCampaignSeeds(pages, config.topKeywordsLimit, config.queriesLimit);
210
+ if (seeds.length === 0)
211
+ return undefined;
212
+ const discoveredKeywords = seeds.map(seed => ({
213
+ keyword: seed.keyword,
214
+ source: 'discovered',
215
+ sourcePage: seed.sourcePage,
216
+ weight: seed.sourceWeight,
217
+ }));
218
+ return analyzeKeywordCampaign({
219
+ targetUrl,
220
+ discoveredKeywords,
221
+ maxQueries: config.queriesLimit,
222
+ maxResultsPerQuery: config.resultsPerQuery,
223
+ minKeywordLength: 2,
224
+ searchParams: config.searchOptions,
225
+ });
226
+ }
227
+ function formatSerpSummaryRows(campaign) {
228
+ return campaign.results.slice(0, 12).map((item) => ({
229
+ keyword: item.keyword,
230
+ found: item.found,
231
+ position: item.bestPosition,
232
+ targetUrl: item.matchedUrl,
233
+ searchUrl: item.searchUrl,
234
+ }));
235
+ }
236
+ function getSerpComparisonRows(campaign) {
237
+ return campaign.pageComparison.map((item) => ({
238
+ pageUrl: item.pageUrl,
239
+ tracked: item.tracked,
240
+ found: item.found,
241
+ appearanceRate: `${item.appearanceRate.toFixed(1)}%`,
242
+ avgPosition: item.avgPosition === null ? 'n/a' : String(item.avgPosition),
243
+ top3: item.top3,
244
+ top10: item.top10,
245
+ }));
246
+ }
71
247
  async function readCachedReport(path, ttlSec) {
72
248
  if (!path || ttlSec <= 0)
73
249
  return null;
@@ -343,7 +519,19 @@ async function seoSpiderCrawl(args) {
343
519
  const maxPages = Number(args.maxPages) || 100;
344
520
  const maxDepth = Number(args.maxDepth) || 5;
345
521
  const concurrency = Number(args.concurrency) || 3;
346
- const transport = args.transport || 'auto';
522
+ const transport = toSpiderTransport(args.transport);
523
+ const preferCurlFirst = args.preferCurlFirst !== false;
524
+ const timeout = toNonNegativeInt(args.timeout, 30000);
525
+ const delay = toNonNegativeInt(args.delay, 200);
526
+ const maxRetryAttempts = toNonNegativeInt(args.maxRetryAttempts, 3);
527
+ const baseRetryDelayMs = toNonNegativeInt(args.baseRetryDelayMs, 1000);
528
+ const maxRetryDelayMs = toNonNegativeInt(args.maxRetryDelayMs, 12000);
529
+ const retryBackoffMultiplier = toNonNegativeInt(args.retryBackoffMultiplier, 2);
530
+ const retryJitterMs = toNonNegativeInt(args.retryJitterMs, 250);
531
+ const maxDomainBlockStrikes = toNonNegativeInt(args.maxDomainBlockStrikes, 2);
532
+ const rotateUserAgent = args.rotateUserAgent !== false;
533
+ const randomizeHeaders = args.randomizeHeaders !== false;
534
+ const serpConfig = parseSpiderSerpConfig(args);
347
535
  if (!url) {
348
536
  return {
349
537
  content: [{ type: 'text', text: 'Error: url is required' }],
@@ -360,6 +548,42 @@ async function seoSpiderCrawl(args) {
360
548
  maxDepth,
361
549
  concurrency,
362
550
  transport,
551
+ preferCurlFirst,
552
+ timeout,
553
+ delay,
554
+ maxRetryAttempts,
555
+ baseRetryDelayMs,
556
+ maxRetryDelayMs,
557
+ retryBackoffMultiplier,
558
+ retryJitterMs,
559
+ maxDomainBlockStrikes,
560
+ rotateUserAgent,
561
+ randomizeHeaders,
562
+ serp: serpConfig.enabled,
563
+ serpTopKeywords: serpConfig.topKeywordsLimit,
564
+ serpQueryLimit: serpConfig.queriesLimit,
565
+ serpResultsPerQuery: serpConfig.resultsPerQuery,
566
+ serpTransport: serpConfig.searchOptions.transport,
567
+ serpTimeout: serpConfig.searchOptions.timeout,
568
+ serpCountry: serpConfig.searchOptions.country,
569
+ serpRegion: serpConfig.searchOptions.country,
570
+ serpGl: serpConfig.searchOptions.gl,
571
+ serpHl: serpConfig.searchOptions.hl,
572
+ serpSafe: serpConfig.searchOptions.safe,
573
+ serpLr: serpConfig.searchOptions.lr,
574
+ serpCr: serpConfig.searchOptions.cr,
575
+ serpTbs: serpConfig.searchOptions.tbs,
576
+ serpTbm: serpConfig.searchOptions.tbm,
577
+ serpAsQ: serpConfig.searchOptions.as_q,
578
+ serpAsEpq: serpConfig.searchOptions.as_epq,
579
+ serpAsOq: serpConfig.searchOptions.as_oq,
580
+ serpAsEq: serpConfig.searchOptions.as_eq,
581
+ serpAsSitesearch: serpConfig.searchOptions.as_sitesearch,
582
+ serpAsFiletype: serpConfig.searchOptions.as_filetype,
583
+ serpAsRights: serpConfig.searchOptions.as_rights,
584
+ serpAsNlo: serpConfig.searchOptions.as_nlo,
585
+ serpAsNhi: serpConfig.searchOptions.as_nhi,
586
+ serpExtra: serpConfig.searchOptions.extraParams,
363
587
  },
364
588
  });
365
589
  if (persistence.enabled && persistence.cacheEnabled && !persistence.forceRefresh && persistence.reportPath) {
@@ -391,13 +615,37 @@ async function seoSpiderCrawl(args) {
391
615
  maxPages,
392
616
  maxDepth,
393
617
  concurrency,
394
- delay: 200,
618
+ delay,
395
619
  transport,
620
+ preferCurlFirst,
621
+ timeout,
622
+ maxRetryAttempts,
623
+ baseRetryDelayMs,
624
+ maxRetryDelayMs,
625
+ retryBackoffMultiplier,
626
+ retryJitterMs,
627
+ maxDomainBlockStrikes,
628
+ rotateUserAgent,
629
+ randomizeHeaders,
396
630
  });
631
+ let serpCampaign;
632
+ if (serpConfig.enabled) {
633
+ const pagesForCampaign = result.pages
634
+ .filter((p) => Boolean(p.seoReport))
635
+ .map((p) => ({
636
+ url: p.url,
637
+ seoReport: p.seoReport,
638
+ }));
639
+ serpCampaign = await runCrawlerSerpCampaign(url, pagesForCampaign, serpConfig);
640
+ }
641
+ const crawlSecuritySummary = summarizeCrawlerSecurity(result.pages);
642
+ const crawlResources = summarizeCrawlerResources(result.pages);
397
643
  const output = {
398
644
  url,
399
645
  crawlDuration: result.duration,
400
646
  summary: result.summary,
647
+ security: crawlSecuritySummary,
648
+ resources: crawlResources,
401
649
  };
402
650
  if (result.siteWideIssues.length > 0) {
403
651
  output.siteWideIssues = result.siteWideIssues.map(issue => ({
@@ -447,6 +695,18 @@ async function seoSpiderCrawl(args) {
447
695
  if (recommendations.length > 0) {
448
696
  output.recommendations = recommendations;
449
697
  }
698
+ let serpWarning;
699
+ if (serpCampaign) {
700
+ output.serp = {
701
+ summary: serpCampaign.summary,
702
+ campaign: serpCampaign.campaign,
703
+ results: formatSerpSummaryRows(serpCampaign),
704
+ pageComparison: getSerpComparisonRows(serpCampaign),
705
+ };
706
+ }
707
+ else if (serpConfig.enabled) {
708
+ serpWarning = 'SERP was enabled but no valid keyword seeds were found in crawl pages.';
709
+ }
450
710
  const reportPayload = JSON.stringify(output, null, 2);
451
711
  let reportMeta = buildReportMeta({
452
712
  reportPath: persistence.reportPath,
@@ -493,6 +753,9 @@ async function seoSpiderCrawl(args) {
493
753
  source: 'fresh',
494
754
  });
495
755
  }
756
+ if (serpWarning) {
757
+ note = `${note} ${serpWarning}`;
758
+ }
496
759
  return {
497
760
  content: [{
498
761
  type: 'text',
@@ -678,6 +941,168 @@ Returns per-page scores and prioritized recommendations for improving overall si
678
941
  description: 'HTTP transport: auto (try undici, fallback to curl on WAF block), undici (fast), curl (curl-impersonate for protected sites)',
679
942
  default: 'auto',
680
943
  },
944
+ preferCurlFirst: {
945
+ type: 'boolean',
946
+ description: 'Prefer curl-impersonate before undici in auto mode',
947
+ default: true,
948
+ },
949
+ timeout: {
950
+ type: 'number',
951
+ description: 'Request timeout in ms',
952
+ default: 30000,
953
+ },
954
+ delay: {
955
+ type: 'number',
956
+ description: 'Delay between requests in ms',
957
+ default: 200,
958
+ },
959
+ maxRetryAttempts: {
960
+ type: 'number',
961
+ description: 'Max retries for failed/blocked requests',
962
+ default: 3,
963
+ },
964
+ baseRetryDelayMs: {
965
+ type: 'number',
966
+ description: 'Base retry delay in ms',
967
+ default: 1000,
968
+ },
969
+ maxRetryDelayMs: {
970
+ type: 'number',
971
+ description: 'Maximum retry delay in ms',
972
+ default: 12000,
973
+ },
974
+ retryBackoffMultiplier: {
975
+ type: 'number',
976
+ description: 'Retry backoff multiplier',
977
+ default: 2,
978
+ },
979
+ retryJitterMs: {
980
+ type: 'number',
981
+ description: 'Retry jitter in ms',
982
+ default: 250,
983
+ },
984
+ maxDomainBlockStrikes: {
985
+ type: 'number',
986
+ description: 'Number of strikes before forcing curl in auto mode',
987
+ default: 2,
988
+ },
989
+ rotateUserAgent: {
990
+ type: 'boolean',
991
+ description: 'Rotate user-agent per request',
992
+ default: true,
993
+ },
994
+ randomizeHeaders: {
995
+ type: 'boolean',
996
+ description: 'Randomize request headers per request',
997
+ default: true,
998
+ },
999
+ serp: {
1000
+ type: 'boolean',
1001
+ description: 'Run SERP keyword checks for extracted top keywords',
1002
+ default: false,
1003
+ },
1004
+ serpTopKeywords: {
1005
+ type: 'number',
1006
+ description: 'Max keywords extracted per page to seed SERP',
1007
+ default: DEFAULT_SERP_KEYWORD_LIMIT,
1008
+ },
1009
+ serpQueryLimit: {
1010
+ type: 'number',
1011
+ description: 'Max number of keyword queries to run',
1012
+ default: DEFAULT_SERP_MAX_RESULTS,
1013
+ },
1014
+ serpResultsPerQuery: {
1015
+ type: 'number',
1016
+ description: 'Number of Google results per query',
1017
+ default: DEFAULT_SERP_MAX_RESULTS,
1018
+ },
1019
+ serpTransport: {
1020
+ type: 'string',
1021
+ enum: ['auto', 'undici', 'curl'],
1022
+ description: 'SERP transport: auto | undici | curl',
1023
+ default: 'auto',
1024
+ },
1025
+ serpTimeout: {
1026
+ type: 'number',
1027
+ description: 'SERP request timeout in ms',
1028
+ default: 0,
1029
+ },
1030
+ serpCountry: {
1031
+ type: 'string',
1032
+ description: 'Country code or country name for SERP geolocation',
1033
+ },
1034
+ serpRegion: {
1035
+ type: 'string',
1036
+ description: 'Alias for country to pass into SERP country',
1037
+ },
1038
+ serpGl: {
1039
+ type: 'string',
1040
+ description: 'Google host country code',
1041
+ },
1042
+ serpHl: {
1043
+ type: 'string',
1044
+ description: 'Search language/locale (e.g., pt-BR)',
1045
+ },
1046
+ serpSafe: {
1047
+ type: 'string',
1048
+ description: 'SafeSearch setting',
1049
+ },
1050
+ serpLr: {
1051
+ type: 'string',
1052
+ description: 'Language restrict parameter',
1053
+ },
1054
+ serpCr: {
1055
+ type: 'string',
1056
+ description: 'Country restrict parameter',
1057
+ },
1058
+ serpTbs: {
1059
+ type: 'string',
1060
+ description: 'Time/custom restrictions',
1061
+ },
1062
+ serpTbm: {
1063
+ type: 'string',
1064
+ description: 'Search type filter',
1065
+ },
1066
+ serpAsQ: {
1067
+ type: 'string',
1068
+ description: 'All words query for Google advanced search',
1069
+ },
1070
+ serpAsEpq: {
1071
+ type: 'string',
1072
+ description: 'Exact phrase query',
1073
+ },
1074
+ serpAsOq: {
1075
+ type: 'string',
1076
+ description: 'Any words query',
1077
+ },
1078
+ serpAsEq: {
1079
+ type: 'string',
1080
+ description: 'Excluding words query',
1081
+ },
1082
+ serpAsSitesearch: {
1083
+ type: 'string',
1084
+ description: 'Restrict results to a site/domain',
1085
+ },
1086
+ serpAsFiletype: {
1087
+ type: 'string',
1088
+ description: 'Restrict by file type',
1089
+ },
1090
+ serpAsRights: {
1091
+ type: 'string',
1092
+ description: 'Rights/license filter',
1093
+ },
1094
+ serpAsNlo: {
1095
+ type: 'string',
1096
+ description: 'Numeric lower bound for search results',
1097
+ },
1098
+ serpAsNhi: {
1099
+ type: 'string',
1100
+ description: 'Numeric upper bound for search results',
1101
+ },
1102
+ serpExtra: {
1103
+ type: 'string',
1104
+ description: 'Additional SERP params as comma-separated key=value list',
1105
+ },
681
1106
  output: {
682
1107
  type: 'string',
683
1108
  description: 'Save report to explicit file path',
@@ -1,3 +1,4 @@
1
+ import type { ReckerRequest, ReckerResponse } from '../types/index.js';
1
2
  export interface JsonRpcRequest {
2
3
  jsonrpc: '2.0';
3
4
  id?: string | number;
@@ -144,10 +145,13 @@ export interface MCPProgressNotification {
144
145
  progress: number;
145
146
  total?: number;
146
147
  }
148
+ export interface MCPTransport {
149
+ dispatch(req: ReckerRequest): Promise<ReckerResponse>;
150
+ }
147
151
  export interface MCPTransportOptions {
148
152
  endpoint: string;
149
153
  headers?: Record<string, string>;
150
154
  timeout?: number;
151
155
  retries?: number;
152
- transport?: any;
156
+ transport?: MCPTransport;
153
157
  }
@@ -1,6 +1,6 @@
1
- import { Plugin } from '../types/index.js';
1
+ import type { Plugin, ProxyOptions } from '../types/index.js';
2
2
  export interface ProxyRotatorOptions {
3
- proxies: string[];
3
+ proxies: (string | ProxyOptions)[];
4
4
  strategy?: 'round-robin' | 'random';
5
5
  failover?: boolean;
6
6
  }
@@ -1,35 +1,13 @@
1
- import { ProxyAgent } from 'undici';
2
1
  export function proxyRotatorPlugin(options) {
3
- const proxies = options.proxies.map(url => ({
4
- url,
5
- agent: new ProxyAgent(url),
6
- failures: 0
7
- }));
8
- let index = 0;
9
- const getNextProxy = () => {
10
- if (proxies.length === 0)
11
- return null;
12
- let selected;
2
+ return (client) => {
13
3
  if (options.strategy === 'random') {
14
- selected = proxies[Math.floor(Math.random() * proxies.length)];
4
+ const shuffled = [...options.proxies].sort(() => Math.random() - 0.5);
5
+ client.defaults = client.defaults ?? {};
6
+ client.defaults.proxy = shuffled;
15
7
  }
16
8
  else {
17
- selected = proxies[index];
18
- index = (index + 1) % proxies.length;
9
+ client.defaults = client.defaults ?? {};
10
+ client.defaults.proxy = options.proxies;
19
11
  }
20
- return selected;
21
- };
22
- return (client) => {
23
- client.beforeRequest((req) => {
24
- const proxy = getNextProxy();
25
- if (proxy) {
26
- req._dispatcher = proxy.agent;
27
- req._proxyUrl = proxy.url;
28
- }
29
- });
30
- client.onError((err, req) => {
31
- if (options.failover && req._proxyUrl) {
32
- }
33
- });
34
12
  };
35
13
  }
@@ -0,0 +1,38 @@
1
+ import { ReckerWebSocket } from '../websocket/client.js';
2
+ import type { RaffelClientOptions, RaffelCallOptions, ChannelEventHandler } from './types.js';
3
+ type Listener = (...args: any[]) => void;
4
+ declare class SimpleEmitter {
5
+ private listeners;
6
+ on(event: string, listener: Listener): this;
7
+ once(event: string, listener: Listener): this;
8
+ off(event: string, listener: Listener): this;
9
+ emit(event: string, ...args: any[]): boolean;
10
+ }
11
+ export declare class RaffelClient extends SimpleEmitter {
12
+ private ws;
13
+ private pendingCalls;
14
+ private subscribedChannels;
15
+ private idCounter;
16
+ private defaultTimeout;
17
+ private onEvent?;
18
+ constructor(url: string, options?: RaffelClientOptions);
19
+ connect(): Promise<void>;
20
+ close(code?: number, reason?: string): void;
21
+ get isConnected(): boolean;
22
+ get raw(): ReckerWebSocket;
23
+ call<T = unknown>(procedure: string, payload?: unknown, options?: RaffelCallOptions): Promise<T>;
24
+ notify(procedure: string, payload?: unknown): void;
25
+ subscribe(channel: string, handler?: ChannelEventHandler): void;
26
+ unsubscribe(channel: string): void;
27
+ publish(channel: string, event: string, data?: unknown): void;
28
+ cancel(id: string): void;
29
+ private handleMessage;
30
+ private handleEnvelope;
31
+ private handleChannelMessage;
32
+ private nextId;
33
+ private extractBaseId;
34
+ private sendChannelSubscribe;
35
+ private sendRaw;
36
+ }
37
+ export declare function createRaffelClient(url: string, options?: RaffelClientOptions): RaffelClient;
38
+ export {};