recker 1.0.73 → 1.0.75-next.2e5a94f

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 (100) hide show
  1. package/README.md +5 -18
  2. package/dist/browser/core/client.d.ts +14 -8
  3. package/dist/browser/core/client.js +199 -17
  4. package/dist/browser/core/errors.d.ts +15 -1
  5. package/dist/browser/core/errors.js +140 -9
  6. package/dist/browser/core/request.d.ts +5 -0
  7. package/dist/browser/core/request.js +33 -2
  8. package/dist/browser/core-runtime/plugin-manifest.d.ts +24 -0
  9. package/dist/browser/core-runtime/plugin-manifest.js +159 -0
  10. package/dist/browser/core-runtime/request-context.d.ts +13 -0
  11. package/dist/browser/core-runtime/request-context.js +24 -0
  12. package/dist/browser/core-runtime/typed-events.d.ts +89 -0
  13. package/dist/browser/core-runtime/typed-events.js +34 -0
  14. package/dist/browser/index.iife.min.js +79 -79
  15. package/dist/browser/index.min.js +79 -79
  16. package/dist/browser/index.mini.iife.js +913 -97
  17. package/dist/browser/index.mini.iife.min.js +46 -46
  18. package/dist/browser/index.mini.min.js +46 -46
  19. package/dist/browser/index.mini.umd.js +913 -97
  20. package/dist/browser/index.mini.umd.min.js +46 -46
  21. package/dist/browser/index.umd.min.js +79 -79
  22. package/dist/browser/plugins/auth/aws-sigv4.d.ts +1 -0
  23. package/dist/browser/plugins/auth/aws-sigv4.js +19 -2
  24. package/dist/browser/plugins/retry.js +29 -1
  25. package/dist/browser/presets/aws.d.ts +1 -0
  26. package/dist/browser/presets/aws.js +62 -1
  27. package/dist/browser/runner/request-runner.d.ts +15 -5
  28. package/dist/browser/runner/request-runner.js +164 -30
  29. package/dist/browser/scrape/parser/nodes/html.d.ts +6 -0
  30. package/dist/browser/scrape/parser/nodes/html.js +70 -18
  31. package/dist/browser/scrape/parser/nodes/node.d.ts +1 -0
  32. package/dist/browser/scrape/parser/nodes/node.js +5 -0
  33. package/dist/browser/scrape/spider.d.ts +1 -0
  34. package/dist/browser/scrape/spider.js +39 -26
  35. package/dist/browser/seo/analyzer.d.ts +1 -1
  36. package/dist/browser/seo/analyzer.js +73 -42
  37. package/dist/browser/seo/index.d.ts +1 -1
  38. package/dist/browser/seo/rules/types.d.ts +2 -0
  39. package/dist/browser/seo/seo-spider.d.ts +2 -3
  40. package/dist/browser/seo/seo-spider.js +26 -202
  41. package/dist/browser/seo/types.d.ts +4 -0
  42. package/dist/browser/seo/validators/sitemap.js +9 -2
  43. package/dist/browser/transport/fetch.js +38 -5
  44. package/dist/browser/transport/undici.js +73 -11
  45. package/dist/browser/transport/worker.d.ts +0 -1
  46. package/dist/browser/transport/worker.js +1 -3
  47. package/dist/browser/types/index.d.ts +24 -0
  48. package/dist/cli/commands/mcp.js +5 -3
  49. package/dist/core/client.d.ts +14 -8
  50. package/dist/core/client.js +199 -17
  51. package/dist/core/errors.d.ts +15 -1
  52. package/dist/core/errors.js +140 -9
  53. package/dist/core/request.d.ts +5 -0
  54. package/dist/core/request.js +33 -2
  55. package/dist/core-runtime/plugin-manifest.d.ts +24 -0
  56. package/dist/core-runtime/plugin-manifest.js +159 -0
  57. package/dist/core-runtime/request-context.d.ts +13 -0
  58. package/dist/core-runtime/request-context.js +24 -0
  59. package/dist/core-runtime/typed-events.d.ts +89 -0
  60. package/dist/core-runtime/typed-events.js +34 -0
  61. package/dist/index.d.ts +2 -1
  62. package/dist/index.js +2 -1
  63. package/dist/mcp/cli.js +10 -8
  64. package/dist/mcp/profiles.d.ts +1 -1
  65. package/dist/mcp/profiles.js +31 -6
  66. package/dist/mcp/tools/categories.js +0 -1
  67. package/dist/plugins/auth/aws-sigv4.d.ts +1 -0
  68. package/dist/plugins/auth/aws-sigv4.js +19 -2
  69. package/dist/plugins/retry.js +29 -1
  70. package/dist/presets/aws.d.ts +1 -0
  71. package/dist/presets/aws.js +62 -1
  72. package/dist/recker.d.ts +3 -0
  73. package/dist/recker.js +5 -0
  74. package/dist/runner/request-runner.d.ts +15 -5
  75. package/dist/runner/request-runner.js +164 -30
  76. package/dist/scrape/parser/nodes/html.d.ts +6 -0
  77. package/dist/scrape/parser/nodes/html.js +70 -18
  78. package/dist/scrape/parser/nodes/node.d.ts +1 -0
  79. package/dist/scrape/parser/nodes/node.js +5 -0
  80. package/dist/scrape/spider.d.ts +1 -0
  81. package/dist/scrape/spider.js +39 -26
  82. package/dist/search/google.d.ts +67 -0
  83. package/dist/search/google.js +480 -0
  84. package/dist/search/index.d.ts +3 -0
  85. package/dist/search/index.js +1 -0
  86. package/dist/seo/analyzer.d.ts +1 -1
  87. package/dist/seo/analyzer.js +73 -42
  88. package/dist/seo/index.d.ts +1 -1
  89. package/dist/seo/rules/types.d.ts +2 -0
  90. package/dist/seo/seo-spider.d.ts +2 -3
  91. package/dist/seo/seo-spider.js +26 -202
  92. package/dist/seo/types.d.ts +4 -0
  93. package/dist/seo/validators/sitemap.js +9 -2
  94. package/dist/transport/fetch.js +38 -5
  95. package/dist/transport/undici.js +73 -11
  96. package/dist/transport/worker.d.ts +0 -1
  97. package/dist/transport/worker.js +1 -3
  98. package/dist/types/index.d.ts +24 -0
  99. package/dist/version.js +1 -1
  100. package/package.json +9 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Nine protocols unified: HTTP, WebSocket, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS.
8
8
  <br>
9
- AI-native: OpenAI, Anthropic, Google, Ollama + MCP server with 65 tools.
9
+ AI-native: OpenAI, Anthropic, Google, Ollama + MCP server with 70 tools.
10
10
  <br>
11
11
  48 service presets built-in. Zero config.
12
12
 
@@ -121,7 +121,7 @@ recker.ws('wss://example.com/socket'); // WebSocket
121
121
  | **SEO** | 400+ rules, 19 categories, site-wide spider crawler |
122
122
  | **Testing** | 10 mock servers (HTTP, Proxy, WebSocket, DNS, FTP, HLS, SSE...) |
123
123
  | **CLI** | `rek` - curl replacement with superpowers |
124
- | **MCP** | 65 tools for AI assistants (Claude, Cursor, Windsurf) |
124
+ | **MCP** | 70 tools for AI assistants (Claude, Cursor, Windsurf) |
125
125
 
126
126
  ## Highlights
127
127
 
@@ -253,7 +253,7 @@ rek shell
253
253
 
254
254
  ## MCP Server
255
255
 
256
- 65 tools for AI assistants like Claude Code, Cursor, and Windsurf:
256
+ 70 tools for AI assistants like Claude Code, Cursor, and Windsurf:
257
257
 
258
258
  ```bash
259
259
  # One-liner for Claude Code (uses minimal category by default)
@@ -262,11 +262,11 @@ claude mcp add recker npx recker@latest mcp
262
262
  # Add more categories as needed
263
263
  claude mcp add recker npx recker@latest mcp --category=minimal,video,seo
264
264
 
265
- # Enable all 65 tools
265
+ # Enable all 70 tools
266
266
  claude mcp add recker npx recker@latest mcp --category=full
267
267
  ```
268
268
 
269
- **Categories:** `minimal` (default) `docs` `network` `dns` `security` `seo` `scrape` `video` `ai` `protocols` `parsing` `streaming` `full`
269
+ **Categories:** `minimal` (default) `docs` `network` `dns` `security` `seo` `scrape` `video` `ai` `protocols` `parsing` `streaming` `template` `full`
270
270
 
271
271
  [→ MCP Documentation](https://forattini-dev.github.io/recker/#/getting-started/mcp)
272
272
 
@@ -285,19 +285,6 @@ claude mcp add recker npx recker@latest mcp --category=full
285
285
  | Mock Servers | [→ Testing](https://forattini-dev.github.io/recker/#/cli/08-mock-servers) |
286
286
  | API Reference | [→ Full API](https://forattini-dev.github.io/recker/#/reference/01-api) |
287
287
 
288
- ## Numbers
289
-
290
- | Metric | Value |
291
- |:-------|:------|
292
- | Protocols | 9 (HTTP, WS, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS) |
293
- | Plugins | 30+ |
294
- | Auth Methods | 15 |
295
- | API Presets | 48 |
296
- | MCP Tools | 65 |
297
- | SEO Rules | 400+ |
298
- | Mock Servers | 10 |
299
- | Tests | 4200+ |
300
-
301
288
  ## License
302
289
 
303
290
  MIT © [Forattini](https://github.com/forattini-dev)
@@ -1,6 +1,7 @@
1
1
  import { ClientOptions, Middleware, ReckerRequest, ReckerResponse, RequestOptions, CacheStorage, PageResult } from '../types/index.js';
2
2
  import type { ClientAI, ClientOptionsWithAI } from '../types/ai-client.js';
3
3
  import { RequestPromise } from './request-promise.js';
4
+ import { type RuntimeEventName, type RuntimeEventPayloads, type TypedEventBus } from '../core-runtime/typed-events.js';
4
5
  import { type VersionInfo } from '../version.js';
5
6
  import { PaginationOptions } from '../plugins/pagination.js';
6
7
  import { RetryOptions } from '../plugins/retry.js';
@@ -15,6 +16,12 @@ interface ClientCacheConfig extends Omit<CacheOptions, 'storage'> {
15
16
  driver?: 'memory' | 'file';
16
17
  fileStoragePath?: string;
17
18
  }
19
+ interface BatchRequestOptions<T = ReckerResponse> {
20
+ concurrency?: number;
21
+ mapResponse?: (res: ReckerResponse) => Promise<T> | T;
22
+ signal?: AbortSignal;
23
+ deadlineMs?: number;
24
+ }
18
25
  export interface ExtendedClientOptions extends ClientOptions {
19
26
  retry?: RetryOptions;
20
27
  cache?: ClientCacheConfig;
@@ -41,6 +48,10 @@ export declare class Client {
41
48
  private concurrencyConfig;
42
49
  private requestPool?;
43
50
  private maxResponseSize?;
51
+ readonly runtimeEventBus: TypedEventBus;
52
+ private shouldEmitRuntimeEvents;
53
+ private runtimeEventBusProviderAttached;
54
+ private runtimeEventListenerCount;
44
55
  private cookieJar?;
45
56
  private cookieIgnoreInvalid;
46
57
  private defaultTimeout?;
@@ -63,6 +74,7 @@ export declare class Client {
63
74
  beforeRequest(hook: (req: ReckerRequest) => ReckerRequest | void | Promise<ReckerRequest | void>): this;
64
75
  afterResponse(hook: (req: ReckerRequest, res: ReckerResponse) => ReckerResponse | void | Promise<ReckerResponse | void>): this;
65
76
  onError(hook: (error: Error, req: ReckerRequest) => ReckerResponse | void | Promise<ReckerResponse | void>): this;
77
+ onRuntimeEvent<K extends RuntimeEventName>(name: K, handler: (event: RuntimeEventPayloads[K]) => void): () => void;
66
78
  private buildUrl;
67
79
  request<T = unknown>(path: string, options?: RequestOptions): RequestPromise<T>;
68
80
  get<T = unknown>(path: string, options?: Omit<RequestOptions, 'method'>): RequestPromise<T>;
@@ -70,10 +82,7 @@ export declare class Client {
70
82
  batch<T = ReckerResponse>(requests: Array<{
71
83
  path: string;
72
84
  options?: RequestOptions;
73
- }>, options?: {
74
- concurrency?: number;
75
- mapResponse?: (res: ReckerResponse) => Promise<T> | T;
76
- }): Promise<{
85
+ }>, options?: BatchRequestOptions<T>): Promise<{
77
86
  results: (T | Error)[];
78
87
  stats: {
79
88
  total: number;
@@ -85,10 +94,7 @@ export declare class Client {
85
94
  multi<T = ReckerResponse>(requests: Array<{
86
95
  path: string;
87
96
  options?: RequestOptions;
88
- }>, options?: {
89
- concurrency?: number;
90
- mapResponse?: (res: ReckerResponse) => Promise<T> | T;
91
- }): Promise<{
97
+ }>, options?: BatchRequestOptions<T>): Promise<{
92
98
  results: (Error | T)[];
93
99
  stats: {
94
100
  total: number;
@@ -5,6 +5,9 @@ import { RequestPromise } from './request-promise.js';
5
5
  import { HttpError, MaxSizeExceededError, ConfigurationError, ValidationError, TimeoutError, UnsupportedError } from '../core/errors.js';
6
6
  import { processBody, createFormData, isPlainObject } from '../utils/body.js';
7
7
  import { RequestPool } from '../utils/request-pool.js';
8
+ import { createRequestContext, attachRequestContext, getRequestContext } from '../core-runtime/request-context.js';
9
+ import { createNoopEventBus } from '../core-runtime/typed-events.js';
10
+ import { attachPluginManifest, getPluginManifest, normalizePlugins, toSortedPlugins } from '../core-runtime/plugin-manifest.js';
8
11
  import { normalizeConcurrency, expandHTTP2Options } from '../utils/concurrency.js';
9
12
  import { getGlobalProtocolCache } from '../utils/protocol-cache.js';
10
13
  import { getDefaultUserAgent } from '../utils/user-agent.js';
@@ -188,6 +191,10 @@ export class Client {
188
191
  concurrencyConfig;
189
192
  requestPool;
190
193
  maxResponseSize;
194
+ runtimeEventBus;
195
+ shouldEmitRuntimeEvents;
196
+ runtimeEventBusProviderAttached = false;
197
+ runtimeEventListenerCount = 0;
191
198
  cookieJar;
192
199
  cookieIgnoreInvalid = false;
193
200
  defaultTimeout;
@@ -215,6 +222,18 @@ export class Client {
215
222
  this.defaultParams = options.defaults?.params || {};
216
223
  this.paginationConfig = options.pagination;
217
224
  this.maxResponseSize = options.maxResponseSize;
225
+ const runtimeEventBus = options.runtimeEventBus;
226
+ if (runtimeEventBus) {
227
+ this.runtimeEventBus = {
228
+ on: (name, handler) => runtimeEventBus.on(name, handler),
229
+ emit: (name, event) => runtimeEventBus.emit(name, event),
230
+ };
231
+ }
232
+ else {
233
+ this.runtimeEventBus = createNoopEventBus();
234
+ }
235
+ this.shouldEmitRuntimeEvents = runtimeEventBus !== undefined;
236
+ this.runtimeEventBusProviderAttached = runtimeEventBus !== undefined;
218
237
  this.debugEnabled = options.debug === true;
219
238
  if (this.debugEnabled) {
220
239
  this.logger = options.logger ?? consoleLogger;
@@ -276,8 +295,20 @@ export class Client {
276
295
  this.transport = new FetchTransport();
277
296
  this.transportKind = 'fetch';
278
297
  }
298
+ const registerPlugin = (plugin, manifest = {}) => {
299
+ const existingManifest = getPluginManifest(plugin);
300
+ pluginRegistrations.push(attachPluginManifest(plugin, {
301
+ ...existingManifest,
302
+ ...manifest
303
+ }));
304
+ };
305
+ const pluginRegistrations = [];
279
306
  if (options.retry) {
280
- retryPlugin(options.retry)(this);
307
+ registerPlugin(retryPlugin(options.retry), {
308
+ name: 'recker:retry',
309
+ priority: 120,
310
+ scope: 'request'
311
+ });
281
312
  }
282
313
  if (this.concurrencyConfig.max < Infinity || this.concurrencyConfig.requestsPerInterval < Infinity) {
283
314
  this.requestPool = new RequestPool({
@@ -285,7 +316,13 @@ export class Client {
285
316
  requestsPerInterval: this.concurrencyConfig.requestsPerInterval,
286
317
  interval: this.concurrencyConfig.interval
287
318
  });
288
- this.middlewares.unshift(this.requestPool.asMiddleware());
319
+ registerPlugin((client) => {
320
+ client.middlewares.unshift(this.requestPool.asMiddleware());
321
+ }, {
322
+ name: 'recker:request-pool',
323
+ priority: 130,
324
+ scope: 'runtime'
325
+ });
289
326
  if (this.debugEnabled && this.logger) {
290
327
  this.logger.debug(`Global concurrency limit: ${this.concurrencyConfig.max} concurrent requests`);
291
328
  }
@@ -296,7 +333,11 @@ export class Client {
296
333
  }
297
334
  }
298
335
  if (options.dedup) {
299
- dedupPlugin(options.dedup)(this);
336
+ registerPlugin(dedupPlugin(options.dedup), {
337
+ name: 'recker:dedup',
338
+ priority: 110,
339
+ scope: 'request'
340
+ });
300
341
  }
301
342
  if (options.cache) {
302
343
  let storage;
@@ -309,13 +350,31 @@ export class Client {
309
350
  else {
310
351
  storage = createDefaultCacheStorage();
311
352
  }
312
- cachePlugin({
353
+ registerPlugin(cachePlugin({
313
354
  ...options.cache,
314
355
  storage
315
- })(this);
356
+ }), {
357
+ name: 'recker:cache',
358
+ priority: 100,
359
+ scope: 'request'
360
+ });
316
361
  }
317
362
  if (options.plugins) {
318
- options.plugins.forEach((plugin) => plugin(this));
363
+ options.plugins.forEach((plugin, index) => {
364
+ const existingManifest = getPluginManifest(plugin);
365
+ registerPlugin(plugin, {
366
+ name: existingManifest?.name || `user-plugin:${index}`,
367
+ priority: existingManifest?.priority ?? 90 - index,
368
+ scope: existingManifest?.scope || 'request'
369
+ });
370
+ });
371
+ }
372
+ const { ordered, debugOrder } = normalizePlugins(toSortedPlugins(pluginRegistrations));
373
+ ordered.forEach(({ plugin }) => {
374
+ plugin(this);
375
+ });
376
+ if (this.debugEnabled && this.logger) {
377
+ this.logger.debug(`Applied plugins in order: ${debugOrder.join(' | ')}`);
319
378
  }
320
379
  if (options.compression) {
321
380
  const compressionMiddleware = createCompressionMiddleware(options.compression);
@@ -458,13 +517,46 @@ export class Client {
458
517
  return header.split(/,(?=\s*[a-zA-Z0-9_-]+=)/g).map(s => s.trim());
459
518
  }
460
519
  async dispatch(req) {
461
- if (req.useCurl && this.transportKind !== 'curl') {
462
- if (!this.curlTransport) {
463
- this.curlTransport = createLazyCurlTransport();
520
+ const context = getRequestContext(req);
521
+ const transportStart = context ? Date.now() : 0;
522
+ if (context) {
523
+ this.runtimeEventBus.emit('transport:start', { context, req });
524
+ }
525
+ try {
526
+ if (req.useCurl && this.transportKind !== 'curl') {
527
+ if (!this.curlTransport) {
528
+ this.curlTransport = createLazyCurlTransport();
529
+ }
530
+ const response = await this.curlTransport.dispatch(req);
531
+ if (context) {
532
+ this.runtimeEventBus.emit('transport:finish', {
533
+ context,
534
+ req,
535
+ durationMs: Date.now() - transportStart
536
+ });
537
+ }
538
+ return response;
539
+ }
540
+ const response = await this.transport.dispatch(req);
541
+ if (context) {
542
+ this.runtimeEventBus.emit('transport:finish', {
543
+ context,
544
+ req,
545
+ durationMs: Date.now() - transportStart
546
+ });
464
547
  }
465
- return this.curlTransport.dispatch(req);
548
+ return response;
549
+ }
550
+ catch (error) {
551
+ if (context) {
552
+ this.runtimeEventBus.emit('transport:error', {
553
+ context,
554
+ req,
555
+ error: error instanceof Error ? error : new Error(String(error))
556
+ });
557
+ }
558
+ throw error;
466
559
  }
467
- return this.transport.dispatch(req);
468
560
  }
469
561
  composeMiddlewares() {
470
562
  const chain = [...this.middlewares];
@@ -563,6 +655,20 @@ export class Client {
563
655
  this.canFastPath = false;
564
656
  return this;
565
657
  }
658
+ onRuntimeEvent(name, handler) {
659
+ this.shouldEmitRuntimeEvents = true;
660
+ this.runtimeEventListenerCount += 1;
661
+ const unsubscribe = this.runtimeEventBus.on(name, handler);
662
+ return () => {
663
+ unsubscribe();
664
+ if (this.runtimeEventListenerCount > 0) {
665
+ this.runtimeEventListenerCount -= 1;
666
+ }
667
+ if (!this.runtimeEventBusProviderAttached && this.runtimeEventListenerCount === 0) {
668
+ this.shouldEmitRuntimeEvents = false;
669
+ }
670
+ };
671
+ }
566
672
  buildUrl(path, requestParams) {
567
673
  const hasRequestParams = requestParams && Object.keys(requestParams).length > 0;
568
674
  const hasDefaultParams = Object.keys(this.defaultParams).length > 0;
@@ -581,7 +687,7 @@ export class Client {
581
687
  const mergedParams = { ...this.defaultParams, ...requestParams };
582
688
  const usedParams = new Set();
583
689
  if (finalPath.includes(':')) {
584
- finalPath = finalPath.replace(/:([a-zA-Z0-9_]+)/g, (match, paramName) => {
690
+ finalPath = finalPath.replace(/:([a-zA-Z0-9_]+)/g, (_match, paramName) => {
585
691
  if (mergedParams && paramName in mergedParams) {
586
692
  usedParams.add(paramName);
587
693
  return encodeURIComponent(String(mergedParams[paramName]));
@@ -616,19 +722,62 @@ export class Client {
616
722
  }
617
723
  request(path, options = {}) {
618
724
  const url = this.buildUrl(path, options.params);
725
+ const canRequestMutateHeaders = (this.hooks.beforeRequest?.length ?? 0) > 0 ||
726
+ this.middlewares.length > 0;
727
+ const shouldAttachContext = this.shouldEmitRuntimeEvents ||
728
+ !!options.correlationId ||
729
+ !!options.tenant ||
730
+ !!options.traceId ||
731
+ !!options.policySource ||
732
+ (options.policyTags?.length ?? 0) > 0;
733
+ const createContextSeed = () => createRequestContext({
734
+ correlationId: options.correlationId,
735
+ tenant: options.tenant,
736
+ policyTags: options.policyTags,
737
+ policySource: options.policySource,
738
+ traceId: options.traceId
739
+ });
619
740
  const usesFastPath = this.canFastPath &&
620
741
  !options.headers &&
621
742
  !options.timeout &&
622
743
  !options.signal &&
623
744
  options.maxResponseSize === undefined;
624
745
  if (usesFastPath) {
625
- const req = new HttpRequest(url, {
746
+ let req = new HttpRequest(url, {
626
747
  method: options.method || 'GET',
627
748
  body: options.body,
628
749
  headers: this.defaultHeadersObj,
629
750
  throwHttpErrors: options.throwHttpErrors,
630
751
  });
631
- const responsePromise = this.fastHandler(req);
752
+ if (shouldAttachContext) {
753
+ req = attachRequestContext(req, createContextSeed());
754
+ }
755
+ const requestContext = getRequestContext(req);
756
+ const startTime = Date.now();
757
+ if (requestContext) {
758
+ this.runtimeEventBus.emit('request:start', { context: requestContext, req });
759
+ }
760
+ let responsePromise = this.fastHandler(req);
761
+ if (requestContext) {
762
+ responsePromise = responsePromise.then((response) => {
763
+ this.runtimeEventBus.emit('request:success', {
764
+ context: requestContext,
765
+ req,
766
+ res: response,
767
+ durationMs: Date.now() - startTime
768
+ });
769
+ return response;
770
+ }, (error) => {
771
+ const requestError = error instanceof Error ? error : new Error(String(error));
772
+ this.runtimeEventBus.emit('request:failed', {
773
+ context: requestContext,
774
+ req,
775
+ error: requestError,
776
+ durationMs: Date.now() - startTime
777
+ });
778
+ throw error;
779
+ });
780
+ }
632
781
  return new RequestPromise(responsePromise);
633
782
  }
634
783
  let mergedHeaders;
@@ -639,6 +788,9 @@ export class Client {
639
788
  : new Headers(options.headers);
640
789
  optHeaders.forEach((value, key) => mergedHeaders.set(key, value));
641
790
  }
791
+ else if (canRequestMutateHeaders) {
792
+ mergedHeaders = new Headers(this.defaultHeadersObj);
793
+ }
642
794
  else {
643
795
  mergedHeaders = this.defaultHeadersObj;
644
796
  }
@@ -672,13 +824,41 @@ export class Client {
672
824
  }
673
825
  }
674
826
  }
675
- const req = new HttpRequest(url, {
827
+ let req = new HttpRequest(url, {
676
828
  ...options,
677
829
  headers: mergedHeaders,
678
830
  signal,
679
831
  maxResponseSize: options.maxResponseSize ?? this.maxResponseSize
680
832
  });
681
- const responsePromise = this.handler(req);
833
+ if (shouldAttachContext) {
834
+ req = attachRequestContext(req, createContextSeed());
835
+ }
836
+ const requestContext = getRequestContext(req);
837
+ const startTime = Date.now();
838
+ if (requestContext) {
839
+ this.runtimeEventBus.emit('request:start', { context: requestContext, req });
840
+ }
841
+ let responsePromise = this.handler(req);
842
+ if (requestContext) {
843
+ responsePromise = responsePromise.then((response) => {
844
+ this.runtimeEventBus.emit('request:success', {
845
+ context: requestContext,
846
+ req,
847
+ res: response,
848
+ durationMs: Date.now() - startTime
849
+ });
850
+ return response;
851
+ }, (error) => {
852
+ const requestError = error instanceof Error ? error : new Error(String(error));
853
+ this.runtimeEventBus.emit('request:failed', {
854
+ context: requestContext,
855
+ req,
856
+ error: requestError,
857
+ durationMs: Date.now() - startTime
858
+ });
859
+ throw error;
860
+ });
861
+ }
682
862
  if (timeoutId || externalAbortCleanup) {
683
863
  responsePromise.finally(() => {
684
864
  if (timeoutId)
@@ -708,6 +888,9 @@ export class Client {
708
888
  const runnerResult = await runner.run(requests, async (item) => {
709
889
  const res = await this.request(item.path, item.options);
710
890
  return mapResponse(res);
891
+ }, {
892
+ signal: options.signal,
893
+ deadlineMs: options.deadlineMs
711
894
  });
712
895
  return runnerResult;
713
896
  }
@@ -857,7 +1040,6 @@ export class Client {
857
1040
  }
858
1041
  page(path, pageNumber, options = {}) {
859
1042
  const pageParam = options.pageParam || this.paginationConfig?.pageParam || 'page';
860
- const url = new URL(path.startsWith('http') ? path : `http://base${path}`);
861
1043
  const params = { ...options.params, [pageParam]: pageNumber };
862
1044
  return this.request(path, { ...options, params });
863
1045
  }
@@ -1,10 +1,23 @@
1
1
  import { ReckerRequest, ReckerResponse } from '../types/index.js';
2
+ export type CanonicalErrorCategory = 'http' | 'network' | 'timeout' | 'protocol' | 'validation' | 'state' | 'filesystem' | 'resource' | 'policy' | 'queue' | 'unknown';
3
+ export type ErrorSource = 'client' | 'transport' | 'server' | 'upstream';
4
+ export type CanonicalErrorSeverity = 'low' | 'medium' | 'high';
5
+ export interface CanonicalErrorMetadata {
6
+ category: CanonicalErrorCategory;
7
+ source: ErrorSource;
8
+ severity: CanonicalErrorSeverity;
9
+ canRetry: boolean;
10
+ reason: string;
11
+ statusCode?: number;
12
+ retryAfterMs?: number;
13
+ }
2
14
  export declare class ReckerError extends Error {
3
15
  request?: ReckerRequest;
4
16
  response?: ReckerResponse;
5
17
  suggestions: string[];
6
18
  retriable: boolean;
7
- constructor(message: string, request?: ReckerRequest, response?: ReckerResponse, suggestions?: string[], retriable?: boolean);
19
+ classification?: CanonicalErrorMetadata;
20
+ constructor(message: string, request?: ReckerRequest, response?: ReckerResponse, suggestions?: string[], retriable?: boolean, classification?: CanonicalErrorMetadata);
8
21
  }
9
22
  export declare class HttpError extends ReckerError {
10
23
  status: number;
@@ -40,6 +53,7 @@ export declare class Http2Error extends ReckerError {
40
53
  });
41
54
  }
42
55
  export declare function parseHttp2Error(error: Error): Http2Error | null;
56
+ export declare function classifyTransportError(error: unknown): CanonicalErrorMetadata | undefined;
43
57
  export declare class MaxSizeExceededError extends ReckerError {
44
58
  maxSize: number;
45
59
  actualSize?: number;