recker 1.0.21-next.fa763a6 → 1.0.23

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.
package/README.md CHANGED
@@ -77,6 +77,28 @@ const api = createClient({
77
77
  const user = await api.get('/users/:id', { params: { id: '123' } }).json();
78
78
  ```
79
79
 
80
+ ### Mini Client (Maximum Performance)
81
+
82
+ Need raw speed? Use `recker-mini` for ~2% overhead vs raw undici:
83
+
84
+ ```typescript
85
+ import { createMiniClient, miniGet } from 'recker/mini';
86
+
87
+ // Client instance
88
+ const fast = createMiniClient({ baseUrl: 'https://api.example.com' });
89
+ const data = await fast.get('/users').then(r => r.json());
90
+
91
+ // Or direct function (even faster)
92
+ const users = await miniGet('https://api.example.com/users').then(r => r.json());
93
+ ```
94
+
95
+ | Mode | Speed | Features |
96
+ |------|-------|----------|
97
+ | `recker-mini` | ~146µs (2% overhead) | Base URL, headers, JSON |
98
+ | `recker` | ~265µs (86% overhead) | Retry, cache, auth, hooks, plugins |
99
+
100
+ See [Mini Client documentation](./docs/http/18-mini-client.md) for more.
101
+
80
102
  ## Features
81
103
 
82
104
  | Feature | Description |
@@ -163,6 +185,7 @@ See [CLI Documentation](./docs/cli/01-overview.md) for more.
163
185
  ## Documentation
164
186
 
165
187
  - **[Quick Start](./docs/http/01-quickstart.md)** - Get running in 2 minutes
188
+ - **[Mini Client](./docs/http/18-mini-client.md)** - Maximum performance mode
166
189
  - **[CLI Guide](./docs/cli/01-overview.md)** - Terminal client documentation
167
190
  - **[API Reference](./docs/reference/01-api.md)** - Complete API documentation
168
191
  - **[Configuration](./docs/http/05-configuration.md)** - Client options
@@ -170,6 +193,7 @@ See [CLI Documentation](./docs/cli/01-overview.md) for more.
170
193
  - **[AI Integration](./docs/ai/01-overview.md)** - OpenAI, Anthropic, and more
171
194
  - **[Protocols](./docs/protocols/01-websocket.md)** - WebSocket, DNS, WHOIS
172
195
  - **[Mock Servers](./docs/cli/08-mock-servers.md)** - Built-in test servers
196
+ - **[Benchmarks](./docs/benchmarks.md)** - Performance comparisons
173
197
 
174
198
  ## License
175
199
 
package/dist/cli/index.js CHANGED
@@ -993,6 +993,60 @@ ${colors.bold(colors.yellow('Examples:'))}
993
993
  process.exit(0);
994
994
  });
995
995
  });
996
+ serve
997
+ .command('webhook')
998
+ .alias('wh')
999
+ .description('Start a webhook receiver server')
1000
+ .option('-p, --port <number>', 'Port to listen on', '3000')
1001
+ .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
1002
+ .option('-s, --status <code>', 'Response status code (200 or 204)', '204')
1003
+ .option('-q, --quiet', 'Disable logging', false)
1004
+ .addHelpText('after', `
1005
+ ${colors.bold(colors.yellow('Examples:'))}
1006
+ ${colors.green('$ rek serve webhook')} ${colors.gray('Start on port 3000')}
1007
+ ${colors.green('$ rek serve wh -p 8080')} ${colors.gray('Start on port 8080')}
1008
+ ${colors.green('$ rek serve webhook --status 200')} ${colors.gray('Return 200 instead of 204')}
1009
+
1010
+ ${colors.bold(colors.yellow('Endpoints:'))}
1011
+ * / ${colors.gray('Receive webhook without ID')}
1012
+ * /:id ${colors.gray('Receive webhook with custom ID')}
1013
+
1014
+ ${colors.bold(colors.yellow('Methods:'))}
1015
+ ${colors.gray('GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS')}
1016
+ `)
1017
+ .action(async (options) => {
1018
+ const { createWebhookServer } = await import('../testing/mock-http-server.js');
1019
+ const status = parseInt(options.status);
1020
+ if (status !== 200 && status !== 204) {
1021
+ console.error(colors.red('Status must be 200 or 204'));
1022
+ process.exit(1);
1023
+ }
1024
+ const server = await createWebhookServer({
1025
+ port: parseInt(options.port),
1026
+ host: options.host,
1027
+ status,
1028
+ log: !options.quiet,
1029
+ });
1030
+ console.log(colors.green(`
1031
+ ┌─────────────────────────────────────────────┐
1032
+ │ ${colors.bold('Recker Webhook Receiver')} │
1033
+ ├─────────────────────────────────────────────┤
1034
+ │ URL: ${colors.cyan(server.url.padEnd(37))}│
1035
+ │ Status: ${colors.yellow(String(status).padEnd(34))}│
1036
+ ├─────────────────────────────────────────────┤
1037
+ │ ${colors.cyan('*')} ${colors.cyan('/')} ${colors.gray('Webhook without ID')} │
1038
+ │ ${colors.cyan('*')} ${colors.cyan('/:id')} ${colors.gray('Webhook with custom ID')} │
1039
+ ├─────────────────────────────────────────────┤
1040
+ │ Press ${colors.bold('Ctrl+C')} to stop │
1041
+ └─────────────────────────────────────────────┘
1042
+ `));
1043
+ process.on('SIGINT', async () => {
1044
+ console.log(colors.yellow('\nShutting down...'));
1045
+ console.log(colors.gray(`Total webhooks received: ${server.webhooks.length}`));
1046
+ await server.stop();
1047
+ process.exit(0);
1048
+ });
1049
+ });
996
1050
  serve
997
1051
  .command('websocket')
998
1052
  .alias('ws')
@@ -1,4 +1,4 @@
1
- import { createHybridSearch } from '../../mcp/search/index.js';
1
+ import { createHybridSearch, createEmbedder, isFastembedAvailable } from '../../mcp/search/index.js';
2
2
  import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
3
3
  import { join, relative, extname, basename, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
@@ -24,6 +24,9 @@ export class ShellSearch {
24
24
  return;
25
25
  }
26
26
  this.hybridSearch = createHybridSearch({ debug: false });
27
+ if (await isFastembedAvailable()) {
28
+ this.hybridSearch.setEmbedder(createEmbedder());
29
+ }
27
30
  this.buildIndex();
28
31
  await this.hybridSearch.initialize(this.docsIndex);
29
32
  this.initialized = true;
@@ -0,0 +1,9 @@
1
+ export declare function unloadEmbedder(): void;
2
+ export declare function embed(text: string): Promise<number[]>;
3
+ export declare function embedBatch(texts: string[]): Promise<number[][]>;
4
+ export declare function createEmbedder(): (text: string, model?: string) => Promise<number[]>;
5
+ export declare function isFastembedAvailable(): Promise<boolean>;
6
+ export declare function getModelInfo(): {
7
+ name: string;
8
+ dimensions: number;
9
+ };
@@ -0,0 +1,83 @@
1
+ const MODEL_NAME = 'BGESmallENV15';
2
+ const MODEL_DIMENSIONS = 384;
3
+ let embedderInstance = null;
4
+ let embedderPromise = null;
5
+ let idleTimer = null;
6
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
7
+ function resetIdleTimer() {
8
+ if (idleTimer) {
9
+ clearTimeout(idleTimer);
10
+ }
11
+ idleTimer = setTimeout(() => {
12
+ unloadEmbedder();
13
+ }, IDLE_TIMEOUT_MS);
14
+ }
15
+ export function unloadEmbedder() {
16
+ if (idleTimer) {
17
+ clearTimeout(idleTimer);
18
+ idleTimer = null;
19
+ }
20
+ embedderInstance = null;
21
+ embedderPromise = null;
22
+ }
23
+ async function getEmbedder() {
24
+ if (embedderInstance) {
25
+ resetIdleTimer();
26
+ return embedderInstance;
27
+ }
28
+ if (embedderPromise) {
29
+ return embedderPromise;
30
+ }
31
+ embedderPromise = (async () => {
32
+ try {
33
+ const { EmbeddingModel, FlagEmbedding } = await import('fastembed');
34
+ embedderInstance = await FlagEmbedding.init({
35
+ model: EmbeddingModel[MODEL_NAME],
36
+ });
37
+ resetIdleTimer();
38
+ return embedderInstance;
39
+ }
40
+ catch (error) {
41
+ embedderPromise = null;
42
+ throw new Error(`Failed to initialize embedder: ${error}`);
43
+ }
44
+ })();
45
+ return embedderPromise;
46
+ }
47
+ export async function embed(text) {
48
+ const embedder = await getEmbedder();
49
+ const embeddings = await embedder.embed([text]);
50
+ for await (const embedding of embeddings) {
51
+ const vec = Array.isArray(embedding) ? embedding[0] : embedding;
52
+ return Array.from(vec);
53
+ }
54
+ throw new Error('No embedding generated');
55
+ }
56
+ export async function embedBatch(texts) {
57
+ const embedder = await getEmbedder();
58
+ const embeddings = await embedder.embed(texts);
59
+ const results = [];
60
+ for await (const embedding of embeddings) {
61
+ const vec = Array.isArray(embedding) ? embedding[0] : embedding;
62
+ results.push(Array.from(vec));
63
+ }
64
+ return results;
65
+ }
66
+ export function createEmbedder() {
67
+ return (text, _model) => embed(text);
68
+ }
69
+ export async function isFastembedAvailable() {
70
+ try {
71
+ await import('fastembed');
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ export function getModelInfo() {
79
+ return {
80
+ name: MODEL_NAME,
81
+ dimensions: MODEL_DIMENSIONS,
82
+ };
83
+ }
@@ -7,6 +7,7 @@ export declare class HybridSearch {
7
7
  private initialized;
8
8
  private config;
9
9
  constructor(config?: HybridSearchConfig);
10
+ setEmbedder(embedder: (text: string, model?: string) => Promise<number[]>): void;
10
11
  initialize(docs: IndexedDoc[]): Promise<void>;
11
12
  private loadPrecomputedEmbeddings;
12
13
  search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
@@ -16,8 +16,12 @@ export class HybridSearch {
16
16
  fuzzyWeight: config.fuzzyWeight ?? 0.5,
17
17
  semanticWeight: config.semanticWeight ?? 0.5,
18
18
  debug: config.debug ?? false,
19
+ embedder: config.embedder,
19
20
  };
20
21
  }
22
+ setEmbedder(embedder) {
23
+ this.config.embedder = embedder;
24
+ }
21
25
  async initialize(docs) {
22
26
  this.docs = docs;
23
27
  this.fuse = new Fuse(docs, {
@@ -55,8 +59,13 @@ export class HybridSearch {
55
59
  }
56
60
  if (this.embeddingsData) {
57
61
  for (const entry of this.embeddingsData.documents) {
58
- if (entry.vector && entry.vector.length > 0) {
59
- this.vectors.set(entry.id, entry.vector);
62
+ if (entry.vector) {
63
+ const vec = Array.isArray(entry.vector)
64
+ ? entry.vector
65
+ : Object.values(entry.vector);
66
+ if (vec.length > 0) {
67
+ this.vectors.set(entry.id, vec);
68
+ }
60
69
  }
61
70
  }
62
71
  this.log(`Loaded ${this.vectors.size} pre-computed embeddings (model: ${this.embeddingsData.model})`);
@@ -86,7 +95,7 @@ export class HybridSearch {
86
95
  this.log(`Fuzzy search found ${fuzzyResults.length} results`);
87
96
  }
88
97
  if ((mode === 'hybrid' || mode === 'semantic') && this.vectors.size > 0) {
89
- const semanticResults = this.semanticSearch(searchQuery, limit * 2, category);
98
+ const semanticResults = await this.semanticSearch(searchQuery, limit * 2, category);
90
99
  for (const result of semanticResults) {
91
100
  const existing = results.get(result.id);
92
101
  if (existing) {
@@ -152,10 +161,53 @@ export class HybridSearch {
152
161
  source: 'fuzzy',
153
162
  }));
154
163
  }
155
- semanticSearch(query, limit, category) {
164
+ async semanticSearch(query, limit, category) {
156
165
  if (!this.embeddingsData || this.vectors.size === 0) {
157
166
  return [];
158
167
  }
168
+ if (this.config.embedder) {
169
+ try {
170
+ const model = this.embeddingsData.model;
171
+ const queryVector = await this.config.embedder(query, model);
172
+ this.log(`Generated query vector using provided embedder (model: ${model})`);
173
+ const scores = [];
174
+ for (const [id, vector] of this.vectors) {
175
+ if (category) {
176
+ const entry = this.embeddingsData.documents.find((e) => e.id === id);
177
+ if (!entry || !entry.category.toLowerCase().includes(category.toLowerCase())) {
178
+ continue;
179
+ }
180
+ }
181
+ if (vector.length !== queryVector.length)
182
+ continue;
183
+ const score = cosineSimilarity(queryVector, vector);
184
+ if (score > 0.05) {
185
+ scores.push({ id, score });
186
+ }
187
+ }
188
+ const results = [];
189
+ for (const s of scores.sort((a, b) => b.score - a.score).slice(0, limit)) {
190
+ const doc = this.docs.find((d) => d.id === s.id);
191
+ const entry = this.embeddingsData.documents.find((e) => e.id === s.id);
192
+ if (!doc && !entry)
193
+ continue;
194
+ const content = doc?.content || '';
195
+ results.push({
196
+ id: s.id,
197
+ path: doc?.path || entry?.path || '',
198
+ title: doc?.title || entry?.title || 'Unknown',
199
+ content,
200
+ snippet: this.extractSnippet(content, query),
201
+ score: s.score,
202
+ source: 'semantic',
203
+ });
204
+ }
205
+ return results;
206
+ }
207
+ catch (error) {
208
+ this.log(`Embedder failed: ${error}. Falling back to synthetic vectors.`);
209
+ }
210
+ }
159
211
  const queryTerms = this.tokenize(query);
160
212
  const scores = [];
161
213
  for (const entry of this.embeddingsData.documents) {
@@ -1,3 +1,4 @@
1
1
  export { HybridSearch, createHybridSearch } from './hybrid-search.js';
2
2
  export { cosineSimilarity, levenshtein, stringSimilarity, reciprocalRankFusion, combineScores, } from './math.js';
3
+ export { embed, embedBatch, createEmbedder, isFastembedAvailable, getModelInfo, unloadEmbedder, } from './embedder.js';
3
4
  export type { IndexedDoc, SearchResult, SearchOptions, HybridSearchConfig, EmbeddingsData, EmbeddingEntry, } from './types.js';
@@ -1,2 +1,3 @@
1
1
  export { HybridSearch, createHybridSearch } from './hybrid-search.js';
2
2
  export { cosineSimilarity, levenshtein, stringSimilarity, reciprocalRankFusion, combineScores, } from './math.js';
3
+ export { embed, embedBatch, createEmbedder, isFastembedAvailable, getModelInfo, unloadEmbedder, } from './embedder.js';
@@ -45,4 +45,5 @@ export interface HybridSearchConfig {
45
45
  fuzzyWeight?: number;
46
46
  semanticWeight?: number;
47
47
  debug?: boolean;
48
+ embedder?: (text: string, model?: string) => Promise<number[]>;
48
49
  }
@@ -22,9 +22,13 @@ export declare class MCPServer {
22
22
  private sseClients;
23
23
  private initialized;
24
24
  private toolRegistry;
25
+ private aiClient?;
25
26
  constructor(options?: MCPServerOptions);
26
27
  private indexReady;
27
28
  private ensureIndexReady;
29
+ private initAI;
30
+ private fastEmbedModel;
31
+ private generateEmbedding;
28
32
  private log;
29
33
  private findDocsPath;
30
34
  private findExamplesPath;
@@ -3,6 +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 { createAI } from '../ai/index.js';
6
7
  import { createHybridSearch } from './search/index.js';
7
8
  import { UnsupportedError } from '../core/errors.js';
8
9
  import { getIpInfo, isValidIP, isGeoIPAvailable, isBogon, isIPv6 } from './ip-intel.js';
@@ -19,6 +20,7 @@ export class MCPServer {
19
20
  sseClients = new Set();
20
21
  initialized = false;
21
22
  toolRegistry;
23
+ aiClient;
22
24
  constructor(options = {}) {
23
25
  this.options = {
24
26
  name: options.name || 'recker-docs',
@@ -32,7 +34,11 @@ export class MCPServer {
32
34
  toolsFilter: options.toolsFilter || [],
33
35
  toolPaths: options.toolPaths || [],
34
36
  };
35
- this.hybridSearch = createHybridSearch({ debug: this.options.debug });
37
+ this.aiClient = this.initAI();
38
+ this.hybridSearch = createHybridSearch({
39
+ debug: this.options.debug,
40
+ embedder: (text, model) => this.generateEmbedding(text, model || 'BGESmallENV15'),
41
+ });
36
42
  this.toolRegistry = new ToolRegistry();
37
43
  this.registerInternalTools();
38
44
  this.toolRegistry.registerModule({
@@ -47,6 +53,56 @@ export class MCPServer {
47
53
  }
48
54
  await this.indexReady;
49
55
  }
56
+ initAI() {
57
+ const hasKeys = process.env.OPENAI_API_KEY || process.env.GOOGLE_API_KEY;
58
+ const hasLocal = process.env.OLLAMA_HOST || true;
59
+ if (!hasKeys) {
60
+ return undefined;
61
+ }
62
+ try {
63
+ return createAI({ debug: this.options.debug });
64
+ }
65
+ catch (e) {
66
+ this.log('Failed to initialize AI client:', e);
67
+ return undefined;
68
+ }
69
+ }
70
+ fastEmbedModel = null;
71
+ async generateEmbedding(text, model) {
72
+ if (model.toLowerCase().includes('bge')) {
73
+ if (!this.fastEmbedModel) {
74
+ try {
75
+ const fastembed = await import('fastembed');
76
+ const { FlagEmbedding } = fastembed;
77
+ this.fastEmbedModel = await FlagEmbedding.init({
78
+ model: model,
79
+ showDownloadProgress: false
80
+ });
81
+ }
82
+ catch (e) {
83
+ throw new Error(`FastEmbed required for model ${model}. Install with: pnpm add fastembed`);
84
+ }
85
+ }
86
+ const vectors = this.fastEmbedModel.embed([text]);
87
+ for await (const v of vectors)
88
+ return Array.from(v);
89
+ throw new Error('No vector generated');
90
+ }
91
+ if (!this.aiClient) {
92
+ throw new Error('No AI client available for external embeddings');
93
+ }
94
+ let provider = 'openai';
95
+ if (model.includes('google') || model.includes('gecko'))
96
+ provider = 'google';
97
+ else if (model.includes('nomic') || model.includes('llama'))
98
+ provider = 'ollama';
99
+ const res = await this.aiClient.embed({
100
+ input: text,
101
+ provider,
102
+ model
103
+ });
104
+ return res.embeddings[0];
105
+ }
50
106
  log(message, data) {
51
107
  if (this.options.debug) {
52
108
  if (this.options.transport === 'stdio') {
package/dist/mini.d.ts CHANGED
@@ -16,6 +16,12 @@ export interface MiniClient {
16
16
  put<T = unknown>(path: string, body?: unknown): Promise<MiniResponse<T>>;
17
17
  patch<T = unknown>(path: string, body?: unknown): Promise<MiniResponse<T>>;
18
18
  delete<T = unknown>(path: string): Promise<MiniResponse<T>>;
19
+ head<T = unknown>(path: string): Promise<MiniResponse<T>>;
20
+ options<T = unknown>(path: string): Promise<MiniResponse<T>>;
21
+ trace<T = unknown>(path: string): Promise<MiniResponse<T>>;
22
+ purge<T = unknown>(path: string): Promise<MiniResponse<T>>;
23
+ request<T = unknown>(method: string, path: string, body?: unknown): Promise<MiniResponse<T>>;
24
+ close(): Promise<void>;
19
25
  }
20
26
  export declare function createMiniClient(options: MiniClientOptions): MiniClient;
21
27
  export declare function miniGet<T = unknown>(url: string, headers?: Record<string, string>): Promise<MiniResponse<T>>;
package/dist/mini.js CHANGED
@@ -1,8 +1,9 @@
1
- import { request as undiciRequest } from 'undici';
1
+ import { Client, request as undiciRequest } from 'undici';
2
2
  export function createMiniClient(options) {
3
3
  const base = options.baseUrl.endsWith('/')
4
4
  ? options.baseUrl.slice(0, -1)
5
5
  : options.baseUrl;
6
+ const undiciClient = new Client(base);
6
7
  const defaultHeaders = options.headers || {};
7
8
  const jsonHeaders = {
8
9
  ...defaultHeaders,
@@ -16,44 +17,48 @@ export function createMiniClient(options) {
16
17
  arrayBuffer: () => body.arrayBuffer(),
17
18
  blob: async () => new Blob([await body.arrayBuffer()])
18
19
  });
20
+ const doRequest = async (method, path, body, useJsonHeaders = false) => {
21
+ const { statusCode, headers, body: resBody } = await undiciClient.request({
22
+ path,
23
+ method,
24
+ headers: useJsonHeaders ? jsonHeaders : defaultHeaders,
25
+ body: body !== undefined ? JSON.stringify(body) : undefined
26
+ });
27
+ return wrapResponse(statusCode, headers, resBody);
28
+ };
19
29
  return {
20
30
  async get(path) {
21
- const { statusCode, headers, body } = await undiciRequest(base + path, {
22
- method: 'GET',
23
- headers: defaultHeaders
24
- });
25
- return wrapResponse(statusCode, headers, body);
31
+ return doRequest('GET', path);
26
32
  },
27
33
  async post(path, data) {
28
- const { statusCode, headers, body } = await undiciRequest(base + path, {
29
- method: 'POST',
30
- headers: jsonHeaders,
31
- body: data !== undefined ? JSON.stringify(data) : undefined
32
- });
33
- return wrapResponse(statusCode, headers, body);
34
+ return doRequest('POST', path, data, true);
34
35
  },
35
36
  async put(path, data) {
36
- const { statusCode, headers, body } = await undiciRequest(base + path, {
37
- method: 'PUT',
38
- headers: jsonHeaders,
39
- body: data !== undefined ? JSON.stringify(data) : undefined
40
- });
41
- return wrapResponse(statusCode, headers, body);
37
+ return doRequest('PUT', path, data, true);
42
38
  },
43
39
  async patch(path, data) {
44
- const { statusCode, headers, body } = await undiciRequest(base + path, {
45
- method: 'PATCH',
46
- headers: jsonHeaders,
47
- body: data !== undefined ? JSON.stringify(data) : undefined
48
- });
49
- return wrapResponse(statusCode, headers, body);
40
+ return doRequest('PATCH', path, data, true);
50
41
  },
51
42
  async delete(path) {
52
- const { statusCode, headers, body } = await undiciRequest(base + path, {
53
- method: 'DELETE',
54
- headers: defaultHeaders
55
- });
56
- return wrapResponse(statusCode, headers, body);
43
+ return doRequest('DELETE', path);
44
+ },
45
+ async head(path) {
46
+ return doRequest('HEAD', path);
47
+ },
48
+ async options(path) {
49
+ return doRequest('OPTIONS', path);
50
+ },
51
+ async trace(path) {
52
+ return doRequest('TRACE', path);
53
+ },
54
+ async purge(path) {
55
+ return doRequest('PURGE', path);
56
+ },
57
+ async request(method, path, body) {
58
+ return doRequest(method.toUpperCase(), path, body, body !== undefined);
59
+ },
60
+ async close() {
61
+ await undiciClient.close();
57
62
  }
58
63
  };
59
64
  }
@@ -8,8 +8,8 @@ export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocke
8
8
  export type { MockWebSocketServerOptions, MockWebSocketClient, MockWebSocketMessage, MockWebSocketStats, } from './mock-websocket-server.js';
9
9
  export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
10
10
  export type { MockSSEServerOptions, SSEEvent, MockSSEClient, MockSSEStats, } from './mock-sse-server.js';
11
- export { MockHttpServer, createMockHttpServer, } from './mock-http-server.js';
12
- export type { MockHttpServerOptions, MockHttpResponse, MockHttpRequest, MockHttpHandler, MockHttpStats, } from './mock-http-server.js';
11
+ export { MockHttpServer, createMockHttpServer, createWebhookServer, } from './mock-http-server.js';
12
+ export type { MockHttpServerOptions, MockHttpResponse, MockHttpRequest, MockHttpHandler, MockHttpStats, WebhookServerOptions, WebhookPayload, } from './mock-http-server.js';
13
13
  export { MockDnsServer, } from './mock-dns-server.js';
14
14
  export type { MockDnsServerOptions, DnsRecordType, DnsRecord, DnsMxRecord, DnsSoaRecord, DnsSrvRecord, MockDnsStats, } from './mock-dns-server.js';
15
15
  export { MockWhoisServer, } from './mock-whois-server.js';
@@ -3,7 +3,7 @@ export { MockUDPServer, createMockUDPServer, } from './mock-udp-server.js';
3
3
  export { MockHlsServer, createMockHlsVod, createMockHlsLive, createMockHlsMultiQuality, } from './mock-hls-server.js';
4
4
  export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocket-server.js';
5
5
  export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
6
- export { MockHttpServer, createMockHttpServer, } from './mock-http-server.js';
6
+ export { MockHttpServer, createMockHttpServer, createWebhookServer, } from './mock-http-server.js';
7
7
  export { MockDnsServer, } from './mock-dns-server.js';
8
8
  export { MockWhoisServer, } from './mock-whois-server.js';
9
9
  export { MockTelnetServer, } from './mock-telnet-server.js';
@@ -81,6 +81,15 @@ export declare class MockHttpServer extends EventEmitter {
81
81
  optionsRoute(path: string, handler: MockHttpResponse | MockHttpHandler, options?: {
82
82
  times?: number;
83
83
  }): this;
84
+ trace(path: string, handler: MockHttpResponse | MockHttpHandler, options?: {
85
+ times?: number;
86
+ }): this;
87
+ connect(path: string, handler: MockHttpResponse | MockHttpHandler, options?: {
88
+ times?: number;
89
+ }): this;
90
+ purge(path: string, handler: MockHttpResponse | MockHttpHandler, options?: {
91
+ times?: number;
92
+ }): this;
84
93
  any(path: string, handler: MockHttpResponse | MockHttpHandler, options?: {
85
94
  times?: number;
86
95
  }): this;
@@ -97,3 +106,19 @@ export declare class MockHttpServer extends EventEmitter {
97
106
  static create(options?: MockHttpServerOptions): Promise<MockHttpServer>;
98
107
  }
99
108
  export declare function createMockHttpServer(routes?: Record<string, MockHttpResponse>, options?: MockHttpServerOptions): Promise<MockHttpServer>;
109
+ export interface WebhookServerOptions extends MockHttpServerOptions {
110
+ log?: boolean;
111
+ logger?: (webhook: WebhookPayload) => void;
112
+ status?: 200 | 204;
113
+ }
114
+ export interface WebhookPayload {
115
+ id: string | null;
116
+ timestamp: Date;
117
+ method: string;
118
+ path: string;
119
+ headers: Record<string, string | string[] | undefined>;
120
+ body: any;
121
+ }
122
+ export declare function createWebhookServer(options?: WebhookServerOptions): Promise<MockHttpServer & {
123
+ webhooks: WebhookPayload[];
124
+ }>;
@@ -113,6 +113,15 @@ export class MockHttpServer extends EventEmitter {
113
113
  optionsRoute(path, handler, options) {
114
114
  return this.route('OPTIONS', path, handler, options);
115
115
  }
116
+ trace(path, handler, options) {
117
+ return this.route('TRACE', path, handler, options);
118
+ }
119
+ connect(path, handler, options) {
120
+ return this.route('CONNECT', path, handler, options);
121
+ }
122
+ purge(path, handler, options) {
123
+ return this.route('PURGE', path, handler, options);
124
+ }
116
125
  any(path, handler, options) {
117
126
  return this.route('*', path, handler, options);
118
127
  }
@@ -296,3 +305,39 @@ export async function createMockHttpServer(routes, options) {
296
305
  await server.start();
297
306
  return server;
298
307
  }
308
+ export async function createWebhookServer(options = {}) {
309
+ const { log = true, logger, status = 204, ...serverOptions } = options;
310
+ const server = new MockHttpServer(serverOptions);
311
+ const webhooks = [];
312
+ const handleWebhook = (req, id) => {
313
+ const payload = {
314
+ id,
315
+ timestamp: new Date(),
316
+ method: req.method,
317
+ path: req.path,
318
+ headers: req.headers,
319
+ body: req.body,
320
+ };
321
+ webhooks.push(payload);
322
+ if (logger) {
323
+ logger(payload);
324
+ }
325
+ else if (log) {
326
+ const idStr = id ? ` [${id}]` : '';
327
+ console.log(`\n📥 Webhook received${idStr} at ${payload.timestamp.toISOString()}`);
328
+ console.log(` Path: ${req.path}`);
329
+ if (req.body !== undefined) {
330
+ console.log(` Body:`, typeof req.body === 'object' ? JSON.stringify(req.body, null, 2) : req.body);
331
+ }
332
+ }
333
+ return { status };
334
+ };
335
+ server.any('/', (req) => handleWebhook(req, null));
336
+ server.any('/:id', (req) => {
337
+ const match = req.path.match(/^\/([^/]+)$/);
338
+ const id = match ? match[1] : null;
339
+ return handleWebhook(req, id);
340
+ });
341
+ await server.start();
342
+ return Object.assign(server, { webhooks });
343
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.21-next.fa763a6",
3
+ "version": "1.0.23",
4
4
  "description": "AI & DevX focused HTTP client for Node.js 18+",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -160,9 +160,7 @@
160
160
  "@types/ws": "^8.18.1",
161
161
  "@vitest/coverage-v8": "^3.2.4",
162
162
  "axios": "^1.13.2",
163
- "bent": "^7.3.12",
164
163
  "cardinal": "^2.1.1",
165
- "centra": "^2.7.0",
166
164
  "cheerio": "^1.0.0",
167
165
  "commander": "^14.0.0",
168
166
  "cross-fetch": "^4.1.0",
@@ -170,7 +168,6 @@
170
168
  "fastembed": "^2.0.0",
171
169
  "got": "^14.6.5",
172
170
  "husky": "^9.1.7",
173
- "hyperquest": "^2.1.3",
174
171
  "ky": "^1.14.0",
175
172
  "make-fetch-happen": "^15.0.3",
176
173
  "minipass-fetch": "^5.0.0",
@@ -178,14 +175,11 @@
178
175
  "needle": "^3.3.1",
179
176
  "node-fetch": "^3.3.2",
180
177
  "ora": "^9.0.0",
181
- "phin": "^3.7.1",
182
178
  "picocolors": "^1.1.1",
183
179
  "popsicle": "^12.1.2",
184
180
  "serve": "^14.2.5",
185
- "simple-get": "^4.0.1",
186
181
  "ssh2-sftp-client": "^12.0.1",
187
182
  "superagent": "^10.2.3",
188
- "tiny-json-http": "^7.5.1",
189
183
  "tsx": "^4.20.6",
190
184
  "typescript": "^5.9.3",
191
185
  "vitest": "^3.2.4",