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,4 +1,5 @@
1
1
  import { CommandEmitter } from '../events/emitter.js';
2
+ import { AbortError, ValidationError } from '../../core/errors.js';
2
3
  const DEFAULT_PORTS = {
3
4
  http: 3000,
4
5
  webhook: 3000,
@@ -56,7 +57,7 @@ export class ServerRunner extends CommandEmitter {
56
57
  this.emitStart(`${type} server`, { port, host });
57
58
  try {
58
59
  if (signal?.aborted) {
59
- throw new Error('Aborted');
60
+ throw new AbortError('Server start aborted');
60
61
  }
61
62
  const abortHandler = () => {
62
63
  this.aborted = true;
@@ -104,7 +105,7 @@ export class ServerRunner extends CommandEmitter {
104
105
  return result;
105
106
  }
106
107
  catch (err) {
107
- if (signal?.aborted || this.aborted || err.message === 'Aborted') {
108
+ if (signal?.aborted || this.aborted || err instanceof AbortError) {
108
109
  this.emitError('Server stopped by user', { code: 'ABORT' });
109
110
  return {
110
111
  type,
@@ -145,121 +146,99 @@ export class ServerRunner extends CommandEmitter {
145
146
  async createServer(type, options) {
146
147
  switch (type) {
147
148
  case 'http': {
148
- const { MockHttpServer } = await import('../../testing/mock-http-server.js');
149
- const server = await MockHttpServer.create({
149
+ const { createMockHttpServer } = await import('../../testing/index.js');
150
+ const server = await createMockHttpServer({
150
151
  port: options.port,
151
152
  host: options.host,
152
153
  delay: options.delay,
153
154
  cors: options.cors !== false,
154
155
  });
155
156
  if (options.echo) {
156
- server.any('/*', (req) => ({
157
+ const echoHandler = (req) => ({
157
158
  status: 200,
158
159
  body: { method: req.method, path: req.path, query: req.query, headers: req.headers, body: req.body },
159
- }));
160
+ });
161
+ server.get('/*', echoHandler);
162
+ server.post('/*', echoHandler);
163
+ server.put('/*', echoHandler);
164
+ server.patch('/*', echoHandler);
165
+ server.delete('/*', echoHandler);
160
166
  }
161
167
  this.attachHttpEvents(server);
162
168
  return server;
163
169
  }
164
170
  case 'webhook': {
165
- const { createWebhookServer } = await import('../../testing/mock-http-server.js');
166
- const server = await createWebhookServer({
167
- port: options.port,
168
- host: options.host,
169
- status: options.status || 204,
170
- log: false,
171
- });
171
+ const { createMockHttpServer } = await import('../../testing/index.js');
172
+ const webhookStatus = options.status || 204;
173
+ const webhookHandler = () => ({ status: webhookStatus });
174
+ const server = await createMockHttpServer({ port: options.port, host: options.host });
175
+ server.get('/*', webhookHandler);
176
+ server.post('/*', webhookHandler);
177
+ server.put('/*', webhookHandler);
178
+ server.patch('/*', webhookHandler);
179
+ server.delete('/*', webhookHandler);
172
180
  this.attachHttpEvents(server);
173
181
  return server;
174
182
  }
175
183
  case 'websocket': {
176
- const { MockWebSocketServer } = await import('../../testing/mock-websocket-server.js');
177
- const server = await MockWebSocketServer.create({
184
+ const { createMockWebSocketServer } = await import('../../testing/index.js');
185
+ const server = await createMockWebSocketServer({
178
186
  port: options.port,
179
187
  host: options.host,
180
- echo: options.echo !== false,
181
- delay: options.delay,
182
188
  });
189
+ if (options.echo === false) {
190
+ server.setMessageHandler(() => undefined);
191
+ }
183
192
  this.attachWebSocketEvents(server);
184
193
  return server;
185
194
  }
186
195
  case 'sse': {
187
- const { MockSSEServer } = await import('../../testing/mock-sse-server.js');
188
- const server = await MockSSEServer.create({
196
+ const { createMockSSEServer } = await import('../../testing/index.js');
197
+ const server = await createMockSSEServer({
189
198
  port: options.port,
190
199
  host: options.host,
191
200
  path: options.path || '/events',
192
201
  });
193
202
  if (options.heartbeat) {
194
- server.startPeriodicEvents('heartbeat', options.heartbeat);
203
+ server.startPeriodicEvents('heartbeat', options.heartbeat, () => ({ data: 'heartbeat', event: 'heartbeat' }));
195
204
  }
196
205
  this.attachSSEEvents(server);
197
206
  return server;
198
207
  }
199
208
  case 'hls': {
200
- const { MockHlsServer } = await import('../../testing/mock-hls-server.js');
201
- const http = await import('node:http');
202
- const baseUrl = `http://${options.host}:${options.port}`;
203
- const hlsServer = await MockHlsServer.create({
204
- baseUrl,
209
+ const { createMockHlsServer } = await import('../../testing/index.js');
210
+ const hlsServer = await createMockHlsServer({
211
+ port: options.port,
212
+ host: options.host,
205
213
  mode: options.mode || 'vod',
206
214
  segmentCount: options.segments || 10,
207
215
  segmentDuration: options.duration || 6,
208
- multiQuality: true,
209
- });
210
- const httpServer = http.createServer(async (req, res) => {
211
- const url = `${baseUrl}${req.url}`;
212
- this.stats.totalRequests++;
213
- this.emit('data', {
214
- type: 'data',
215
- category: 'server:request',
216
- timestamp: Date.now(),
217
- command: 'server',
218
- level: 'debug',
219
- payload: { method: req.method, path: req.url },
220
- });
221
- try {
222
- const response = await hlsServer.transport.dispatch({ url, method: req.method || 'GET' });
223
- res.statusCode = response.status;
224
- response.headers.forEach((value, key) => res.setHeader(key, value));
225
- const body = await response.arrayBuffer();
226
- res.end(Buffer.from(body));
227
- }
228
- catch {
229
- res.statusCode = 404;
230
- res.end('Not Found');
231
- }
232
- });
233
- await new Promise((resolve) => {
234
- httpServer.listen(options.port, options.host, () => resolve());
235
216
  });
236
217
  return {
237
- port: options.port,
218
+ port: hlsServer.port,
238
219
  address: options.host,
239
- url: hlsServer.manifestUrl,
240
- isRunning: true,
241
- stop: async () => {
242
- httpServer.close();
243
- await hlsServer.stop();
244
- },
245
- on: () => this,
246
- emit: () => false,
220
+ url: hlsServer.masterUrl,
221
+ isRunning: hlsServer.isRunning,
222
+ stop: () => hlsServer.stop(),
223
+ on: hlsServer.on.bind(hlsServer),
224
+ emit: hlsServer.emit.bind(hlsServer),
247
225
  };
248
226
  }
249
227
  case 'udp': {
250
- const { MockUDPServer } = await import('../../testing/mock-udp-server.js');
251
- const server = new MockUDPServer({
228
+ const { createMockUdpServer } = await import('../../testing/index.js');
229
+ const server = await createMockUdpServer({
252
230
  port: options.port,
253
231
  host: options.host,
254
- echo: options.echo !== false,
255
232
  });
256
- await server.start();
233
+ if (options.echo === false) {
234
+ server.setMessageHandler(() => undefined);
235
+ }
257
236
  this.attachUDPEvents(server);
258
237
  return server;
259
238
  }
260
239
  case 'dns': {
261
- const { MockDnsServer } = await import('../../testing/mock-dns-server.js');
262
- const server = await MockDnsServer.create({
240
+ const { createMockDnsServer } = await import('../../testing/index.js');
241
+ const server = await createMockDnsServer({
263
242
  port: options.port,
264
243
  host: options.host,
265
244
  delay: options.delay,
@@ -268,41 +247,39 @@ export class ServerRunner extends CommandEmitter {
268
247
  return server;
269
248
  }
270
249
  case 'whois': {
271
- const { MockWhoisServer } = await import('../../testing/mock-whois-server.js');
272
- const server = await MockWhoisServer.create({
250
+ const { createMockWhoisServer } = await import('../../testing/index.js');
251
+ const server = await createMockWhoisServer({
273
252
  port: options.port,
274
253
  host: options.host,
275
- delay: options.delay,
254
+ responseDelay: options.delay,
276
255
  });
277
256
  this.attachWhoisEvents(server);
278
257
  return server;
279
258
  }
280
259
  case 'telnet': {
281
- const { MockTelnetServer } = await import('../../testing/mock-telnet-server.js');
282
- const server = await MockTelnetServer.create({
260
+ const { createMockTelnetServer } = await import('../../testing/index.js');
261
+ const server = await createMockTelnetServer({
283
262
  port: options.port,
284
263
  host: options.host,
285
- echo: options.echo !== false,
286
- delay: options.delay,
264
+ responseDelay: options.delay,
287
265
  });
288
266
  this.attachTelnetEvents(server);
289
267
  return server;
290
268
  }
291
269
  case 'ftp': {
292
- const { MockFtpServer } = await import('../../testing/mock-ftp-server.js');
293
- const server = await MockFtpServer.create({
270
+ const { createMockFtpServer } = await import('../../testing/index.js');
271
+ const server = await createMockFtpServer({
294
272
  port: options.port,
295
273
  host: options.host,
296
- username: options.username || 'user',
297
- password: options.password || 'pass',
298
- anonymous: options.anonymous !== false,
299
- delay: options.delay,
300
274
  });
301
275
  this.attachFTPEvents(server);
302
276
  return server;
303
277
  }
304
278
  default:
305
- throw new Error(`Unknown server type: ${type}`);
279
+ throw new ValidationError(`Unknown server type: ${type}`, {
280
+ field: 'serverType',
281
+ value: type,
282
+ });
306
283
  }
307
284
  }
308
285
  async waitForStop(signal) {
@@ -441,7 +418,7 @@ export class ServerRunner extends CommandEmitter {
441
418
  command: 'server',
442
419
  level: 'debug',
443
420
  payload: {
444
- domain: query.domain,
421
+ domain: query.name,
445
422
  recordType: query.type,
446
423
  },
447
424
  });
@@ -1,4 +1,7 @@
1
1
  import { CommandEmitter } from '../events/emitter.js';
2
+ import type { SeoReport } from '../../seo/types.js';
3
+ import type { SpiderPageResult, SpiderTransport } from '../../scrape/spider.js';
4
+ import type { SpiderFocusMode } from '../utils/option-helpers.js';
2
5
  export interface SpiderRunnerOptions {
3
6
  depth?: number;
4
7
  limit?: number;
@@ -6,7 +9,19 @@ export interface SpiderRunnerOptions {
6
9
  robots?: boolean;
7
10
  seo?: boolean;
8
11
  useSitemap?: boolean;
9
- focus?: 'all' | 'links' | 'duplicates' | 'security' | 'ai' | 'resources';
12
+ transport?: SpiderTransport;
13
+ preferCurlFirst?: boolean;
14
+ timeout?: number;
15
+ delay?: number;
16
+ maxRetryAttempts?: number;
17
+ baseRetryDelayMs?: number;
18
+ maxRetryDelayMs?: number;
19
+ retryBackoffMultiplier?: number;
20
+ retryJitterMs?: number;
21
+ maxDomainBlockStrikes?: number;
22
+ rotateUserAgent?: boolean;
23
+ randomizeHeaders?: boolean;
24
+ focus?: SpiderFocusMode;
10
25
  signal?: AbortSignal;
11
26
  extract?: string[];
12
27
  include?: string[];
@@ -17,6 +32,19 @@ export interface SpiderRunnerResult {
17
32
  pagesVisited: number;
18
33
  duration: number;
19
34
  errors: number;
35
+ security?: {
36
+ pages: number;
37
+ blockedPages: number;
38
+ captchaPages: number;
39
+ captchaProviders: Record<string, number>;
40
+ attempts: number;
41
+ retries: number;
42
+ transportUsage: Record<Exclude<SpiderTransport, 'auto'>, number>;
43
+ avgAttempts: number;
44
+ avgTtfbMs?: number;
45
+ avgTotalMs?: number;
46
+ avgDownloadMs?: number;
47
+ };
20
48
  internalLinks: number;
21
49
  externalLinks: number;
22
50
  images: number;
@@ -29,10 +57,18 @@ export interface SpiderRunnerResult {
29
57
  depth: number;
30
58
  links: number;
31
59
  duration: number;
60
+ security?: SpiderPageResult['security'];
61
+ timings?: SpiderPageResult['timings'];
62
+ resources?: {
63
+ images: number;
64
+ scripts: number;
65
+ stylesheets: number;
66
+ };
32
67
  seoScore?: number;
33
68
  seoGrade?: string;
34
69
  seoErrors?: number;
35
70
  seoWarnings?: number;
71
+ seoReport?: SeoReport;
36
72
  extracted?: Record<string, unknown>;
37
73
  }>;
38
74
  extraction?: {
@@ -1,10 +1,11 @@
1
1
  import { CommandEmitter } from '../events/emitter.js';
2
+ import { AbortError } from '../../core/errors.js';
2
3
  export class SpiderRunner extends CommandEmitter {
3
4
  constructor() {
4
5
  super('spider');
5
6
  }
6
7
  async run(url, options = {}) {
7
- const { depth = 5, limit = 100, concurrency = 5, robots = true, seo = false, focus = 'all', signal, extract, include, exclude, } = options;
8
+ const { depth = 5, limit = 100, concurrency = 5, robots = true, seo = false, focus = 'all', signal, extract, include, exclude, transport, preferCurlFirst, timeout, delay, maxRetryAttempts, baseRetryDelayMs, maxRetryDelayMs, retryBackoffMultiplier, retryJitterMs, maxDomainBlockStrikes, rotateUserAgent, randomizeHeaders, } = options;
8
9
  let startUrl = url;
9
10
  if (!startUrl.startsWith('http')) {
10
11
  startUrl = `https://${startUrl}`;
@@ -17,9 +18,67 @@ export class SpiderRunner extends CommandEmitter {
17
18
  let scripts = 0;
18
19
  let stylesheets = 0;
19
20
  let errorCount = 0;
21
+ let blockedPages = 0;
22
+ let captchaPages = 0;
23
+ let captchaProviders = {};
24
+ let attemptsTotal = 0;
25
+ let retriesTotal = 0;
26
+ let transportUsage = { undici: 0, curl: 0 };
27
+ let timingSum = { ttfb: 0, total: 0, download: 0 };
28
+ let timingCount = 0;
29
+ const collectPageMetrics = (page) => {
30
+ const resources = page.resources;
31
+ if (resources) {
32
+ images += resources.images || 0;
33
+ scripts += resources.scripts || 0;
34
+ stylesheets += resources.stylesheets || 0;
35
+ }
36
+ if (page.security) {
37
+ if (page.security.blocked)
38
+ blockedPages += 1;
39
+ if (page.security.captchaDetected)
40
+ captchaPages += 1;
41
+ if (page.security.captchaDetected) {
42
+ const provider = (page.security.captchaProvider || '').trim().toLowerCase();
43
+ const key = provider.length > 0 ? provider : 'unknown';
44
+ captchaProviders[key] = (captchaProviders[key] || 0) + 1;
45
+ }
46
+ attemptsTotal += Number(page.security.attempts ?? 1);
47
+ retriesTotal += Number(page.security.retryCount ?? 0);
48
+ const transport = page.security.transport;
49
+ if (transport && transport !== 'auto') {
50
+ transportUsage[transport] += 1;
51
+ }
52
+ }
53
+ else {
54
+ attemptsTotal += 1;
55
+ }
56
+ const ttfb = page.timings?.ttfb;
57
+ const total = page.timings?.total;
58
+ const download = page.timings?.download;
59
+ if (typeof ttfb === 'number' || typeof total === 'number' || typeof download === 'number') {
60
+ timingSum.ttfb += typeof ttfb === 'number' ? ttfb : 0;
61
+ timingSum.total += typeof total === 'number' ? total : 0;
62
+ timingSum.download += typeof download === 'number' ? download : 0;
63
+ timingCount += 1;
64
+ }
65
+ };
66
+ const getSecuritySummary = () => ({
67
+ pages: pages.length,
68
+ blockedPages,
69
+ captchaPages,
70
+ captchaProviders,
71
+ attempts: attemptsTotal,
72
+ retries: retriesTotal,
73
+ transportUsage,
74
+ avgAttempts: pages.length > 0 ? Number((attemptsTotal / pages.length).toFixed(2)) : 0,
75
+ avgTtfbMs: timingCount > 0 ? Math.round(timingSum.ttfb / timingCount) : undefined,
76
+ avgTotalMs: timingCount > 0 ? Math.round(timingSum.total / timingCount) : undefined,
77
+ avgDownloadMs: timingCount > 0 ? Math.round(timingSum.download / timingCount) : undefined,
78
+ });
20
79
  try {
21
80
  if (seo) {
22
- return await this.runSeoSpider(startUrl, options, pages);
81
+ return await this.runSeoSpider(startUrl, options, pages, collectPageMetrics, getSecuritySummary);
23
82
  }
24
83
  const { Spider } = await import('../../scrape/spider.js');
25
84
  const extractionByPage = {};
@@ -28,10 +87,23 @@ export class SpiderRunner extends CommandEmitter {
28
87
  maxPages: limit,
29
88
  concurrency,
30
89
  respectRobotsTxt: robots,
90
+ transport,
91
+ preferCurlFirst,
92
+ timeout,
93
+ delay,
94
+ maxRetryAttempts,
95
+ baseRetryDelayMs,
96
+ maxRetryDelayMs,
97
+ retryBackoffMultiplier,
98
+ retryJitterMs,
99
+ maxDomainBlockStrikes,
100
+ rotateUserAgent,
101
+ randomizeHeaders,
31
102
  extract,
32
103
  include: include?.map(p => new RegExp(p)),
33
104
  exclude: exclude?.map(p => new RegExp(p)),
34
105
  onPage: (page) => {
106
+ collectPageMetrics(page);
35
107
  pages.push({
36
108
  url: page.url,
37
109
  status: page.status,
@@ -39,6 +111,9 @@ export class SpiderRunner extends CommandEmitter {
39
111
  depth: page.depth,
40
112
  links: page.links?.length || 0,
41
113
  duration: page.duration,
114
+ security: page.security,
115
+ timings: page.timings,
116
+ resources: page.resources,
42
117
  extracted: page.extracted,
43
118
  });
44
119
  if (page.extracted && Object.keys(page.extracted).length > 0) {
@@ -70,9 +145,12 @@ export class SpiderRunner extends CommandEmitter {
70
145
  title: page.title,
71
146
  links: page.links?.length || 0,
72
147
  duration: page.duration,
148
+ security: page.security,
149
+ timings: page.timings,
150
+ resources: page.resources,
73
151
  });
74
152
  if (signal?.aborted) {
75
- throw new Error('Aborted');
153
+ throw new AbortError('Spider crawl aborted');
76
154
  }
77
155
  },
78
156
  onProgress: (progress) => {
@@ -121,6 +199,7 @@ export class SpiderRunner extends CommandEmitter {
121
199
  images,
122
200
  scripts,
123
201
  stylesheets,
202
+ security: getSecuritySummary(),
124
203
  pages,
125
204
  extraction,
126
205
  };
@@ -134,24 +213,25 @@ export class SpiderRunner extends CommandEmitter {
134
213
  images,
135
214
  scripts,
136
215
  stylesheets,
216
+ security: getSecuritySummary(),
137
217
  });
138
218
  return summary;
139
219
  }
140
220
  catch (err) {
141
- if (signal?.aborted || err.message === 'Aborted') {
221
+ if (signal?.aborted || err instanceof AbortError) {
142
222
  this.emitError('Spider stopped by user', { code: 'ABORT' });
143
223
  }
144
224
  else {
145
- this.emitError(err.message || 'Spider failed', {
146
- stack: err.stack,
225
+ this.emitError(err instanceof Error ? err.message : 'Spider failed', {
226
+ stack: err instanceof Error ? err.stack : undefined,
147
227
  context: startUrl,
148
228
  });
149
229
  }
150
230
  throw err;
151
231
  }
152
232
  }
153
- async runSeoSpider(url, options, pages) {
154
- const { depth = 5, limit = 100, concurrency = 5, robots = true, focus = 'all', signal, useSitemap = true } = options;
233
+ async runSeoSpider(url, options, pages, collectPageMetrics, getSecuritySummary) {
234
+ const { depth = 5, limit = 100, concurrency = 5, robots = true, focus = 'all', signal, useSitemap = true, transport, preferCurlFirst, timeout, delay, maxRetryAttempts, baseRetryDelayMs, maxRetryDelayMs, retryBackoffMultiplier, retryJitterMs, maxDomainBlockStrikes, rotateUserAgent, randomizeHeaders, } = options;
155
235
  const focusCategories = {
156
236
  links: ['links'],
157
237
  duplicates: ['title', 'meta', 'content'],
@@ -171,7 +251,18 @@ export class SpiderRunner extends CommandEmitter {
171
251
  maxPages: limit,
172
252
  concurrency,
173
253
  sameDomain: true,
174
- delay: 100,
254
+ delay,
255
+ timeout,
256
+ transport,
257
+ preferCurlFirst,
258
+ maxRetryAttempts,
259
+ baseRetryDelayMs,
260
+ maxRetryDelayMs,
261
+ retryBackoffMultiplier,
262
+ retryJitterMs,
263
+ maxDomainBlockStrikes,
264
+ rotateUserAgent,
265
+ randomizeHeaders,
175
266
  seo: true,
176
267
  respectRobotsTxt: robots,
177
268
  useSitemap,
@@ -186,7 +277,7 @@ export class SpiderRunner extends CommandEmitter {
186
277
  depth: progress.depth,
187
278
  });
188
279
  if (signal?.aborted) {
189
- throw new Error('Aborted');
280
+ throw new AbortError('Spider crawl aborted');
190
281
  }
191
282
  },
192
283
  });
@@ -200,11 +291,16 @@ export class SpiderRunner extends CommandEmitter {
200
291
  depth: page.depth,
201
292
  links: page.links?.length || 0,
202
293
  duration: page.duration,
294
+ security: page.security,
295
+ timings: page.timings,
296
+ resources: page.resources,
203
297
  seoScore: seoReport?.score,
204
298
  seoGrade: seoReport?.grade,
205
299
  seoErrors: seoReport?.summary?.errors,
206
300
  seoWarnings: seoReport?.summary?.warnings,
301
+ seoReport,
207
302
  });
303
+ collectPageMetrics(page);
208
304
  if (page.links) {
209
305
  const pageHost = new URL(page.url).hostname;
210
306
  for (const link of page.links) {
@@ -231,6 +327,9 @@ export class SpiderRunner extends CommandEmitter {
231
327
  title: page.title,
232
328
  links: page.links?.length || 0,
233
329
  duration: page.duration,
330
+ security: page.security,
331
+ timings: page.timings,
332
+ resources: page.resources,
234
333
  });
235
334
  }
236
335
  const summary = {
@@ -243,6 +342,7 @@ export class SpiderRunner extends CommandEmitter {
243
342
  images,
244
343
  scripts,
245
344
  stylesheets,
345
+ security: getSecuritySummary(),
246
346
  pages,
247
347
  seo: result.summary ? {
248
348
  avgScore: result.summary.avgScore,
@@ -273,6 +373,10 @@ export class SpiderRunner extends CommandEmitter {
273
373
  errors: result.errors?.length || 0,
274
374
  internalLinks,
275
375
  externalLinks,
376
+ images,
377
+ scripts,
378
+ stylesheets,
379
+ security: getSecuritySummary(),
276
380
  });
277
381
  return summary;
278
382
  }
@@ -340,6 +444,10 @@ export async function runSpiderWithEvents(url, options = {}) {
340
444
  concurrency: spiderOptions.concurrency ?? 5,
341
445
  seo: spiderOptions.seo ?? false,
342
446
  extract: spiderOptions.extract,
447
+ transport: spiderOptions.transport,
448
+ preferCurlFirst: spiderOptions.preferCurlFirst,
449
+ timeout: spiderOptions.timeout,
450
+ delay: spiderOptions.delay,
343
451
  },
344
452
  });
345
453
  runner.on('spider:page', (event) => {
@@ -351,6 +459,12 @@ export async function runSpiderWithEvents(url, options = {}) {
351
459
  title: event.title,
352
460
  links: event.links,
353
461
  duration: event.duration,
462
+ blocked: event.security?.blocked,
463
+ blockedByCaptcha: event.security?.blockedByCaptcha,
464
+ captchaDetected: event.security?.captchaDetected,
465
+ transport: event.security?.transport,
466
+ timing: event.timings,
467
+ resources: event.resources,
354
468
  });
355
469
  });
356
470
  }
@@ -369,10 +483,16 @@ export async function runSpiderWithEvents(url, options = {}) {
369
483
  depth: page.depth,
370
484
  links: page.links,
371
485
  duration: page.duration,
486
+ security: page.security,
487
+ timings: page.timings,
488
+ resources: page.resources,
372
489
  seoScore: page.seoScore,
373
490
  seoGrade: page.seoGrade,
374
491
  seoErrors: page.seoErrors,
375
492
  seoWarnings: page.seoWarnings,
493
+ attempts: page.security?.attempts,
494
+ retryCount: page.security?.retryCount,
495
+ retryAfterMs: page.security?.retryAfterMs,
376
496
  extracted: page.extracted,
377
497
  });
378
498
  }
@@ -386,6 +506,10 @@ export async function runSpiderWithEvents(url, options = {}) {
386
506
  internalLinks: result.internalLinks,
387
507
  externalLinks: result.externalLinks,
388
508
  seo: result.seo,
509
+ security: result.security,
510
+ images: result.images,
511
+ scripts: result.scripts,
512
+ stylesheets: result.stylesheets,
389
513
  extraction: result.extraction,
390
514
  });
391
515
  await jsonlWriter.close();
@@ -1,11 +1,14 @@
1
1
  import { RekCommand as Command } from '../router.js';
2
+ import { parseSpiderSerpConfig } from '../utils/serp-config.js';
3
+ import { type SpiderTransport } from '../utils/option-helpers.js';
4
+ type SpiderFocusMode = 'all' | 'links' | 'duplicates' | 'security' | 'ai' | 'resources';
2
5
  export interface SpiderOptions {
3
6
  url: string;
4
7
  depth?: number;
5
8
  limit?: number;
6
9
  concurrency?: number;
7
10
  output?: string;
8
- focus?: string;
11
+ focus?: SpiderFocusMode;
9
12
  seo?: boolean;
10
13
  robots?: boolean;
11
14
  json?: boolean;
@@ -15,6 +18,20 @@ export interface SpiderOptions {
15
18
  exclude?: string | string[];
16
19
  noSitemap?: boolean;
17
20
  disableTui?: boolean;
21
+ transport?: SpiderTransport;
22
+ preferCurlFirst?: boolean;
23
+ timeout?: number;
24
+ delay?: number;
25
+ maxRetryAttempts?: number;
26
+ baseRetryDelayMs?: number;
27
+ maxRetryDelayMs?: number;
28
+ retryBackoffMultiplier?: number;
29
+ retryJitterMs?: number;
30
+ maxDomainBlockStrikes?: number;
31
+ rotateUserAgent?: boolean;
32
+ randomizeHeaders?: boolean;
33
+ serpConfig?: ReturnType<typeof parseSpiderSerpConfig>;
18
34
  }
19
35
  export declare function runSpider(opts: SpiderOptions): Promise<void>;
20
36
  export declare function registerSpiderCommand(program: Command): void;
37
+ export {};