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
@@ -2,6 +2,18 @@ import { EventEmitter } from 'events';
2
2
  import { createClient } from '../core/client.js';
3
3
  import { StateError, ProtocolError } from '../core/errors.js';
4
4
  import { consoleLogger } from '../types/logger.js';
5
+ function normalizeJsonRpcParams(params) {
6
+ if (params === undefined)
7
+ return undefined;
8
+ if (params === null)
9
+ return { value: null };
10
+ if (Array.isArray(params))
11
+ return params;
12
+ if (typeof params === 'object') {
13
+ return params;
14
+ }
15
+ return { value: params };
16
+ }
5
17
  export class MCPClient extends EventEmitter {
6
18
  client;
7
19
  endpoint;
@@ -137,7 +149,7 @@ export class MCPClient extends EventEmitter {
137
149
  jsonrpc: '2.0',
138
150
  id: ++this.requestId,
139
151
  method,
140
- params: params,
152
+ params: normalizeJsonRpcParams(params),
141
153
  };
142
154
  this.emit('request', request);
143
155
  const response = await this.client
@@ -154,6 +166,9 @@ export class MCPClient extends EventEmitter {
154
166
  error.data = response.error.data;
155
167
  throw error;
156
168
  }
169
+ if (response.result === undefined) {
170
+ return {};
171
+ }
157
172
  return response.result;
158
173
  }
159
174
  async connectSSE() {
@@ -173,8 +188,11 @@ export class MCPClient extends EventEmitter {
173
188
  }
174
189
  }
175
190
  handleSSEEvent(event) {
191
+ const payload = typeof event === 'string' ? event : event.data;
192
+ if (!payload)
193
+ return;
176
194
  try {
177
- const data = JSON.parse(event.data);
195
+ const data = JSON.parse(payload);
178
196
  switch (data.method) {
179
197
  case 'notifications/progress':
180
198
  this.emit('progress', data.params);
@@ -45,7 +45,7 @@ export declare function createMCPBatch<T extends MCPContractDefinition>(mcp: MCP
45
45
  args?: T[K]["inputSchema"] extends ZodSchema ? z.infer<T[K]["inputSchema"]> : Record<string, unknown>;
46
46
  }>) => Promise<Array<{
47
47
  success: boolean;
48
- result?: any;
48
+ result?: unknown;
49
49
  error?: Error;
50
50
  }>>;
51
51
  export interface SSEStreamOptions {
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from '../core/errors.js';
1
2
  export const categories = {
2
3
  minimal: {
3
4
  name: 'minimal',
@@ -216,7 +217,10 @@ export function resolveCategories(categoryNames) {
216
217
  for (const name of names) {
217
218
  const category = categories[name];
218
219
  if (!category) {
219
- throw new Error(`Unknown category: ${name}. Available: ${Object.keys(categories).join(', ')}`);
220
+ throw new ValidationError(`Unknown category: ${name}.`, {
221
+ field: 'categoryNames',
222
+ value: name,
223
+ });
220
224
  }
221
225
  for (const tool of category.tools) {
222
226
  toolSet.add(tool);
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from '../../core/errors.js';
1
2
  export class PromptRegistry {
2
3
  prompts = new Map();
3
4
  constructor() {
@@ -12,7 +13,7 @@ export class PromptRegistry {
12
13
  async getPrompt(name, args) {
13
14
  const prompt = this.prompts.get(name);
14
15
  if (!prompt) {
15
- throw new Error(`Prompt not found: ${name}`);
16
+ throw new ValidationError(`Prompt not found: ${name}`, { field: 'name', value: name });
16
17
  }
17
18
  return prompt.handler(args);
18
19
  }
@@ -508,10 +509,13 @@ const client = createClient();
508
509
  // Rotate user agents
509
510
  client.use(userAgent({ rotate: true, type: 'desktop' }));
510
511
 
511
- // Rotate proxies
512
+ // Rotate proxies (or pass the list directly to createClient)
513
+ const client2 = createClient({
514
+ proxy: ['http://proxy1:8080', 'socks5://proxy2:1080', 'socks5h://proxy3:1080'],
515
+ });
516
+ // Alternative via plugin
512
517
  client.use(proxyRotator({
513
- proxies: ['http://proxy1:8080', 'http://proxy2:8080'],
514
- strategy: 'round-robin',
518
+ proxies: ['http://proxy1:8080', 'socks5://proxy2:1080'],
515
519
  }));
516
520
 
517
521
  // Respect rate limits
@@ -1,4 +1,5 @@
1
1
  import { listCategories } from '../tools/categories.js';
2
+ import { ConfigurationError, NotFoundError } from '../../core/errors.js';
2
3
  export class ResourceRegistry {
3
4
  resources = new Map();
4
5
  templates = [];
@@ -41,7 +42,7 @@ export class ResourceRegistry {
41
42
  return template.handler(params);
42
43
  }
43
44
  }
44
- throw new Error(`Resource not found: ${uri}`);
45
+ throw new NotFoundError(`Resource not found: ${uri}`, { resource: uri });
45
46
  }
46
47
  recordRequest(method, url, status, duration) {
47
48
  this.requestHistory.push({
@@ -359,9 +360,9 @@ export class ResourceRegistry {
359
360
  async loadPresetDetails(name) {
360
361
  try {
361
362
  const { presetRegistry } = await import('../../presets/registry.js');
362
- const preset = presetRegistry.find(p => p.name === name);
363
+ const preset = presetRegistry.find((p) => p.name === name);
363
364
  if (!preset) {
364
- throw new Error(`Preset not found: ${name}`);
365
+ throw new NotFoundError(`Preset not found: ${name}`, { resource: name });
365
366
  }
366
367
  const envVar = `${name.toUpperCase().replace(/-/g, '_')}_API_KEY`;
367
368
  return {
@@ -386,7 +387,9 @@ const response = await client.get('/endpoint').json();
386
387
  };
387
388
  }
388
389
  catch (e) {
389
- throw new Error(`Failed to load preset '${name}': ${e.message}`);
390
+ throw new ConfigurationError(`Failed to load preset '${name}': ${getErrorMessage(e)}`, {
391
+ configKey: `preset.${name}`,
392
+ });
390
393
  }
391
394
  }
392
395
  async getReckerInfo() {
@@ -467,7 +470,7 @@ pnpm bench:json # JSON output
467
470
  };
468
471
  }
469
472
  catch (e) {
470
- results.checks.dns = { status: 'error', error: e.message };
473
+ results.checks.dns = { status: 'error', error: getErrorMessage(e) };
471
474
  }
472
475
  try {
473
476
  const { inspectTLS } = await import('../../utils/tls-inspector.js');
@@ -485,7 +488,7 @@ pnpm bench:json # JSON output
485
488
  };
486
489
  }
487
490
  catch (e) {
488
- results.checks.tls = { status: 'error', error: e.message };
491
+ results.checks.tls = { status: 'error', error: getErrorMessage(e) };
489
492
  }
490
493
  try {
491
494
  const { createClient } = await import('../../core/client.js');
@@ -505,7 +508,7 @@ pnpm bench:json # JSON output
505
508
  };
506
509
  }
507
510
  catch (e) {
508
- results.checks.http = { status: 'error', error: e.message };
511
+ results.checks.http = { status: 'error', error: getErrorMessage(e) };
509
512
  }
510
513
  return results;
511
514
  }
@@ -527,16 +530,16 @@ pnpm bench:json # JSON output
527
530
  return {
528
531
  url,
529
532
  status: 'healthy',
530
- statusCode: lastResponse.status,
533
+ statusCode: lastResponse?.status,
531
534
  latency: {
532
535
  avg: `${avgLatency}ms`,
533
536
  min: `${minLatency}ms`,
534
537
  max: `${maxLatency}ms`,
535
538
  },
536
539
  headers: {
537
- server: lastResponse.headers.get('server'),
538
- contentType: lastResponse.headers.get('content-type'),
539
- cacheControl: lastResponse.headers.get('cache-control'),
540
+ server: lastResponse?.headers.get('server'),
541
+ contentType: lastResponse?.headers.get('content-type'),
542
+ cacheControl: lastResponse?.headers.get('cache-control'),
540
543
  },
541
544
  checkedAt: new Date().toISOString(),
542
545
  };
@@ -545,7 +548,7 @@ pnpm bench:json # JSON output
545
548
  return {
546
549
  url,
547
550
  status: 'unhealthy',
548
- error: e.message,
551
+ error: getErrorMessage(e),
549
552
  latency: `${Date.now() - startTime}ms (failed)`,
550
553
  checkedAt: new Date().toISOString(),
551
554
  };
@@ -671,7 +674,12 @@ import { userAgent, proxyRotator } from 'recker/plugins';
671
674
 
672
675
  const client = createClient();
673
676
  client.use(userAgent({ rotate: true }));
674
- client.use(proxyRotator({ proxies: ['http://proxy1:8080', 'http://proxy2:8080'] }));
677
+ // Proxy list directly in createClient (recommended):
678
+ const client2 = createClient({
679
+ proxy: ['http://proxy1:8080', 'socks5://proxy2:1080', 'socks5h://proxy3:1080'],
680
+ });
681
+ // Or via plugin:
682
+ client.use(proxyRotator({ proxies: ['http://proxy1:8080', 'socks5://proxy2:1080'] }));
675
683
 
676
684
  const doc = await client.scrape('https://protected-site.com');
677
685
  \`\`\`
@@ -1003,19 +1011,20 @@ Make sure the URL is accessible and try again.
1003
1011
  const pricing = [];
1004
1012
  for (const provider of catalog.providers) {
1005
1013
  for (const model of provider.models) {
1014
+ const features = [
1015
+ model.vision && 'vision',
1016
+ model.tools && 'tools',
1017
+ model.reasoning && 'reasoning',
1018
+ model.fast && 'fast',
1019
+ model.coding && 'coding',
1020
+ ].filter((value) => Boolean(value));
1006
1021
  pricing.push({
1007
1022
  provider: provider.name,
1008
1023
  model: model.id,
1009
1024
  input: `$${model.inputPrice?.toFixed(2) || 'N/A'}`,
1010
1025
  output: `$${model.outputPrice?.toFixed(2) || 'N/A'}`,
1011
1026
  context: model.context?.toLocaleString() || 'N/A',
1012
- features: [
1013
- model.vision && 'vision',
1014
- model.tools && 'tools',
1015
- model.reasoning && 'reasoning',
1016
- model.fast && 'fast',
1017
- model.coding && 'coding',
1018
- ].filter(Boolean),
1027
+ features,
1019
1028
  });
1020
1029
  }
1021
1030
  }
@@ -1042,7 +1051,7 @@ Make sure the URL is accessible and try again.
1042
1051
  const provider = catalog.providers.find((p) => p.name === name);
1043
1052
  if (!provider) {
1044
1053
  const available = catalog.providers.map((p) => p.name).join(', ');
1045
- throw new Error(`Provider '${name}' not found. Available: ${available}`);
1054
+ throw new NotFoundError(`Provider '${name}' not found. Available: ${available}`, { resource: name });
1046
1055
  }
1047
1056
  return {
1048
1057
  name: provider.name,
@@ -1061,10 +1070,24 @@ Make sure the URL is accessible and try again.
1061
1070
  }[name] || null,
1062
1071
  features: {
1063
1072
  streaming: true,
1064
- functionCalling: provider.models.some((m) => m.tools),
1065
- vision: provider.models.some((m) => m.vision),
1073
+ functionCalling: provider.models.some((m) => Boolean(m.tools)),
1074
+ vision: provider.models.some((m) => Boolean(m.vision)),
1066
1075
  embeddings: ['openai', 'google', 'mistral'].includes(name),
1067
1076
  },
1068
1077
  };
1069
1078
  }
1070
1079
  }
1080
+ function getErrorMessage(error) {
1081
+ if (error instanceof Error) {
1082
+ return error.message;
1083
+ }
1084
+ if (typeof error === 'string') {
1085
+ return error;
1086
+ }
1087
+ try {
1088
+ return JSON.stringify(error);
1089
+ }
1090
+ catch {
1091
+ return String(error);
1092
+ }
1093
+ }
@@ -3,7 +3,7 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
3
3
  import { join, relative, extname, basename, dirname } from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
6
- import { UnsupportedError } from '../core/errors.js';
6
+ import { NotFoundError, UnsupportedError } from '../core/errors.js';
7
7
  import { getIpInfo, isValidIP, isGeoIPAvailable, isBogon, isIPv6 } from './ip-intel.js';
8
8
  import { networkTools, networkToolHandlers } from './tools/network.js';
9
9
  import { seoTools, seoToolHandlers } from './tools/seo.js';
@@ -298,8 +298,9 @@ export class MCPServer {
298
298
  return 'general';
299
299
  }
300
300
  inferComplexity(content, docComment) {
301
- if (/@complexity\s+(basic|intermediate|advanced)/i.test(docComment)) {
302
- return docComment.match(/@complexity\s+(basic|intermediate|advanced)/i)[1].toLowerCase();
301
+ const complexityMatch = docComment.match(/@complexity\s+(basic|intermediate|advanced)/i);
302
+ if (complexityMatch?.[1]) {
303
+ return complexityMatch[1].toLowerCase();
303
304
  }
304
305
  const lines = content.split('\n').length;
305
306
  const hasAsyncAwait = /async|await/.test(content);
@@ -518,7 +519,7 @@ export class MCPServer {
518
519
  const path = uri.slice(7);
519
520
  const doc = this.docsIndex.find(d => d.path === path);
520
521
  if (!doc) {
521
- throw new Error(`Documentation not found: ${path}`);
522
+ throw new NotFoundError(`Documentation not found: ${path}`, { resource: path });
522
523
  }
523
524
  return [{
524
525
  uri,
@@ -531,7 +532,7 @@ export class MCPServer {
531
532
  const id = uri.slice(10);
532
533
  const example = this.codeExamples.find(e => e.id === id);
533
534
  if (!example) {
534
- throw new Error(`Example not found: ${id}`);
535
+ throw new NotFoundError(`Example not found: ${id}`, { resource: id });
535
536
  }
536
537
  return [{
537
538
  uri,
@@ -540,7 +541,9 @@ export class MCPServer {
540
541
  type: 'resource',
541
542
  }];
542
543
  }
543
- throw new Error(`Unknown resource scheme: ${uri}`);
544
+ throw new UnsupportedError(`Unknown resource scheme: ${uri}`, {
545
+ feature: 'recker://, docs://, example://',
546
+ });
544
547
  }
545
548
  getResourceRegistry() {
546
549
  return this.resourceRegistry;
@@ -2,6 +2,7 @@ import { createFTP } from '../../protocols/ftp.js';
2
2
  import { createSFTP } from '../../protocols/sftp.js';
3
3
  import { createTelnet } from '../../protocols/telnet.js';
4
4
  import { createWebSocket } from '../../websocket/client.js';
5
+ import { ProtocolError } from '../../core/errors.js';
5
6
  export const ftpConnectTool = {
6
7
  name: 'rek_ftp_connect',
7
8
  description: 'Connect to an FTP server and list directory contents. Supports FTPS (implicit TLS).',
@@ -143,7 +144,10 @@ export const ftpDownloadTool = {
143
144
  await ftp.close();
144
145
  const content = downloadResult.data;
145
146
  if (!content) {
146
- throw new Error('Failed to download file');
147
+ throw new ProtocolError('Failed to download file', {
148
+ protocol: 'ftp',
149
+ phase: 'download',
150
+ });
147
151
  }
148
152
  let textContent;
149
153
  let isText = true;
@@ -334,7 +338,10 @@ export const sftpDownloadTool = {
334
338
  await sftp.close();
335
339
  const content = downloadResult.data;
336
340
  if (!content) {
337
- throw new Error('Failed to download file');
341
+ throw new ProtocolError('Failed to download file', {
342
+ protocol: 'sftp',
343
+ phase: 'download',
344
+ });
338
345
  }
339
346
  let textContent;
340
347
  let isText = true;
@@ -1,5 +1,6 @@
1
1
  import { matchesPattern } from '../profiles.js';
2
2
  import { getToolCategory, listCategories } from './categories.js';
3
+ import { StateError, ValidationError } from '../../core/errors.js';
3
4
  export class ToolRegistry {
4
5
  tools = new Map();
5
6
  handlers = new Map();
@@ -13,7 +14,10 @@ export class ToolRegistry {
13
14
  }
14
15
  registerTool(tool, handler) {
15
16
  if (this.tools.has(tool.name)) {
16
- throw new Error(`Tool already registered: ${tool.name}`);
17
+ throw new ValidationError(`Tool already registered: ${tool.name}`, {
18
+ field: 'tool.name',
19
+ value: tool.name,
20
+ });
17
21
  }
18
22
  this.tools.set(tool.name, tool);
19
23
  this.handlers.set(tool.name, handler);
@@ -22,7 +26,10 @@ export class ToolRegistry {
22
26
  for (const tool of module.tools) {
23
27
  const handler = module.handlers[tool.name];
24
28
  if (!handler) {
25
- throw new Error(`Handler missing for tool: ${tool.name}`);
29
+ throw new ValidationError(`Handler missing for tool: ${tool.name}`, {
30
+ field: `handlers["${tool.name}"]`,
31
+ value: undefined,
32
+ });
26
33
  }
27
34
  this.registerTool(tool, handler);
28
35
  }
@@ -100,7 +107,10 @@ export class ToolRegistry {
100
107
  async callTool(name, args) {
101
108
  const handler = this.handlers.get(name);
102
109
  if (!handler) {
103
- throw new Error(`Unknown tool: ${name}`);
110
+ throw new StateError(`Unknown tool: ${name}`, {
111
+ expectedState: 'registered tool',
112
+ actualState: name,
113
+ });
104
114
  }
105
115
  return handler(args);
106
116
  }