recker 1.0.22 → 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/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.22",
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",