recker 1.0.22 → 1.0.25-next.566f437

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.
@@ -1,9 +1,8 @@
1
1
  import { createClient } from '../core/client.js';
2
2
  import { requireOptional } from '../utils/optional-require.js';
3
3
  import colors from '../utils/colors.js';
4
- import oraImport from 'ora';
4
+ import { createSpinner } from './tui/spinner.js';
5
5
  let highlight;
6
- const ora = oraImport;
7
6
  async function initDependencies() {
8
7
  if (!highlight) {
9
8
  try {
@@ -24,10 +23,9 @@ export async function handleRequest(options) {
24
23
  }
25
24
  });
26
25
  }
27
- const spinner = options.quiet ? null : ora({
26
+ const spinner = options.quiet ? null : createSpinner({
28
27
  text: `${colors.bold(options.method)} ${colors.cyan(options.url)}`,
29
- color: 'cyan',
30
- spinner: 'dots'
28
+ color: 'cyan'
31
29
  }).start();
32
30
  const start = performance.now();
33
31
  try {
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')
@@ -13,6 +13,8 @@ export declare class SearchPanel {
13
13
  private rightPanelWidth;
14
14
  private contentHeight;
15
15
  private previewContent;
16
+ private keyHandler;
17
+ private resizeHandler;
16
18
  constructor(options?: SearchPanelOptions);
17
19
  private findDocsPath;
18
20
  open(): Promise<void>;
@@ -34,6 +34,8 @@ export class SearchPanel {
34
34
  rightPanelWidth = 0;
35
35
  contentHeight = 0;
36
36
  previewContent = [];
37
+ keyHandler = null;
38
+ resizeHandler = null;
37
39
  constructor(options = {}) {
38
40
  this.state = {
39
41
  query: options.initialQuery || '',
@@ -69,11 +71,13 @@ export class SearchPanel {
69
71
  if (process.stdin.isTTY) {
70
72
  process.stdin.setRawMode(true);
71
73
  }
72
- process.stdout.on('resize', () => {
74
+ this.resizeHandler = () => {
73
75
  this.updateDimensions();
74
76
  this.render();
75
- });
76
- process.stdin.on('data', this.handleKeyInput.bind(this));
77
+ };
78
+ process.stdout.on('resize', this.resizeHandler);
79
+ this.keyHandler = this.handleKeyInput.bind(this);
80
+ process.stdin.on('data', this.keyHandler);
77
81
  if (this.state.query) {
78
82
  await this.performSearch();
79
83
  }
@@ -99,8 +103,14 @@ export class SearchPanel {
99
103
  this.rl.close();
100
104
  this.rl = null;
101
105
  }
102
- process.stdin.removeAllListeners('data');
103
- process.stdout.removeAllListeners('resize');
106
+ if (this.keyHandler) {
107
+ process.stdin.removeListener('data', this.keyHandler);
108
+ this.keyHandler = null;
109
+ }
110
+ if (this.resizeHandler) {
111
+ process.stdout.removeListener('resize', this.resizeHandler);
112
+ this.resizeHandler = null;
113
+ }
104
114
  }
105
115
  updateDimensions() {
106
116
  this.termWidth = process.stdout.columns || 80;
@@ -1,21 +1,27 @@
1
1
  import type { SearchResult } from '../../mcp/search/types.js';
2
+ export type ProgressCallback = (stage: string, percent?: number) => void;
2
3
  export declare class ShellSearch {
3
4
  private hybridSearch;
4
5
  private docsIndex;
5
6
  private codeExamples;
6
7
  private typeDefinitions;
7
8
  private initialized;
9
+ private initializing;
8
10
  private idleTimer;
9
11
  private docsPath;
10
12
  private examplesPath;
11
13
  private srcPath;
14
+ private spinner;
15
+ private hasSemanticSearch;
12
16
  constructor();
17
+ private updateSpinner;
13
18
  private ensureInitialized;
14
19
  private resetIdleTimer;
15
20
  private unload;
16
21
  search(query: string, options?: {
17
22
  limit?: number;
18
23
  category?: string;
24
+ silent?: boolean;
19
25
  }): Promise<SearchResult[]>;
20
26
  suggest(useCase: string): Promise<string>;
21
27
  getExamples(feature: string, options?: {
@@ -1,7 +1,8 @@
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';
5
+ import { createSpinner } from './spinner.js';
5
6
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
6
7
  export class ShellSearch {
7
8
  hybridSearch = null;
@@ -9,24 +10,61 @@ export class ShellSearch {
9
10
  codeExamples = [];
10
11
  typeDefinitions = [];
11
12
  initialized = false;
13
+ initializing = false;
12
14
  idleTimer = null;
13
15
  docsPath;
14
16
  examplesPath;
15
17
  srcPath;
18
+ spinner = null;
19
+ hasSemanticSearch = false;
16
20
  constructor() {
17
21
  this.docsPath = this.findDocsPath();
18
22
  this.examplesPath = this.findExamplesPath();
19
23
  this.srcPath = this.findSrcPath();
20
24
  }
25
+ updateSpinner(text) {
26
+ if (this.spinner) {
27
+ this.spinner.text = text;
28
+ }
29
+ }
21
30
  async ensureInitialized() {
22
31
  this.resetIdleTimer();
23
32
  if (this.initialized && this.hybridSearch) {
24
33
  return;
25
34
  }
26
- this.hybridSearch = createHybridSearch({ debug: false });
27
- this.buildIndex();
28
- await this.hybridSearch.initialize(this.docsIndex);
29
- this.initialized = true;
35
+ if (this.initializing) {
36
+ while (this.initializing) {
37
+ await new Promise(resolve => setTimeout(resolve, 100));
38
+ }
39
+ return;
40
+ }
41
+ this.initializing = true;
42
+ this.spinner = createSpinner({ text: 'Initializing search...' }).start();
43
+ try {
44
+ this.updateSpinner('Creating search index...');
45
+ this.hybridSearch = createHybridSearch({ debug: false });
46
+ this.updateSpinner('Checking semantic search availability...');
47
+ const fastembedAvailable = await isFastembedAvailable();
48
+ if (fastembedAvailable) {
49
+ this.updateSpinner('Loading AI embedding model (first time may take a while)...');
50
+ this.hybridSearch.setEmbedder(createEmbedder());
51
+ this.hasSemanticSearch = true;
52
+ }
53
+ this.updateSpinner('Indexing documentation...');
54
+ this.buildIndex();
55
+ this.updateSpinner('Finalizing search index...');
56
+ await this.hybridSearch.initialize(this.docsIndex);
57
+ this.initialized = true;
58
+ this.spinner.succeed(`Search ready (${this.docsIndex.length} docs${this.hasSemanticSearch ? ', semantic enabled' : ''})`);
59
+ }
60
+ catch (error) {
61
+ this.spinner.fail(`Search initialization failed: ${error}`);
62
+ throw error;
63
+ }
64
+ finally {
65
+ this.initializing = false;
66
+ this.spinner = null;
67
+ }
30
68
  }
31
69
  resetIdleTimer() {
32
70
  if (this.idleTimer) {
@@ -45,22 +83,46 @@ export class ShellSearch {
45
83
  }
46
84
  async search(query, options = {}) {
47
85
  await this.ensureInitialized();
48
- const { limit = 5, category } = options;
86
+ const { limit = 5, category, silent = false } = options;
49
87
  if (!this.hybridSearch) {
50
88
  return [];
51
89
  }
52
- return this.hybridSearch.search(query, { limit, category, mode: 'hybrid' });
90
+ let searchSpinner = null;
91
+ if (!silent && this.hasSemanticSearch) {
92
+ searchSpinner = createSpinner({ text: 'Generating query embedding...' }).start();
93
+ }
94
+ try {
95
+ if (searchSpinner) {
96
+ searchSpinner.text = 'Searching documentation...';
97
+ }
98
+ const results = await this.hybridSearch.search(query, { limit, category, mode: 'hybrid' });
99
+ if (searchSpinner) {
100
+ searchSpinner.succeed(`Found ${results.length} result${results.length !== 1 ? 's' : ''}`);
101
+ }
102
+ return results;
103
+ }
104
+ catch (error) {
105
+ if (searchSpinner) {
106
+ searchSpinner.fail(`Search failed: ${error}`);
107
+ }
108
+ throw error;
109
+ }
53
110
  }
54
111
  async suggest(useCase) {
55
- await this.ensureInitialized();
56
- const results = await this.search(useCase, { limit: 3 });
57
- if (results.length === 0) {
58
- return `No suggestions found for: "${useCase}"\n\nTry searching for specific features like:\n - retry\n - cache\n - streaming\n - websocket\n - pagination`;
59
- }
60
- const useCaseLower = useCase.toLowerCase();
61
- const suggestions = [];
62
- if (useCaseLower.includes('retry') || useCaseLower.includes('fail') || useCaseLower.includes('error')) {
63
- suggestions.push(`\n**Retry Configuration:**
112
+ const spinner = createSpinner({ text: 'Generating suggestions...' }).start();
113
+ try {
114
+ await this.ensureInitialized();
115
+ spinner.text = 'Finding relevant documentation...';
116
+ const results = await this.search(useCase, { limit: 3, silent: true });
117
+ if (results.length === 0) {
118
+ spinner.info('No suggestions found');
119
+ return `No suggestions found for: "${useCase}"\n\nTry searching for specific features like:\n - retry\n - cache\n - streaming\n - websocket\n - pagination`;
120
+ }
121
+ spinner.text = 'Building suggestions...';
122
+ const useCaseLower = useCase.toLowerCase();
123
+ const suggestions = [];
124
+ if (useCaseLower.includes('retry') || useCaseLower.includes('fail') || useCaseLower.includes('error')) {
125
+ suggestions.push(`\n**Retry Configuration:**
64
126
  \`\`\`typescript
65
127
  import { createClient } from 'recker';
66
128
 
@@ -74,9 +136,9 @@ const client = createClient({
74
136
  }
75
137
  });
76
138
  \`\`\``);
77
- }
78
- if (useCaseLower.includes('cache') || useCaseLower.includes('storage')) {
79
- suggestions.push(`\n**Cache Configuration:**
139
+ }
140
+ if (useCaseLower.includes('cache') || useCaseLower.includes('storage')) {
141
+ suggestions.push(`\n**Cache Configuration:**
80
142
  \`\`\`typescript
81
143
  import { createClient } from 'recker';
82
144
 
@@ -89,9 +151,9 @@ const client = createClient({
89
151
  }
90
152
  });
91
153
  \`\`\``);
92
- }
93
- if (useCaseLower.includes('stream') || useCaseLower.includes('sse') || useCaseLower.includes('ai') || useCaseLower.includes('openai')) {
94
- suggestions.push(`\n**Streaming Configuration:**
154
+ }
155
+ if (useCaseLower.includes('stream') || useCaseLower.includes('sse') || useCaseLower.includes('ai') || useCaseLower.includes('openai')) {
156
+ suggestions.push(`\n**Streaming Configuration:**
95
157
  \`\`\`typescript
96
158
  import { createClient } from 'recker';
97
159
 
@@ -102,9 +164,9 @@ for await (const event of client.post('/v1/chat/completions', { body, stream: tr
102
164
  console.log(event.data);
103
165
  }
104
166
  \`\`\``);
105
- }
106
- if (useCaseLower.includes('parallel') || useCaseLower.includes('batch') || useCaseLower.includes('concurrent')) {
107
- suggestions.push(`\n**Batch/Parallel Requests:**
167
+ }
168
+ if (useCaseLower.includes('parallel') || useCaseLower.includes('batch') || useCaseLower.includes('concurrent')) {
169
+ suggestions.push(`\n**Batch/Parallel Requests:**
108
170
  \`\`\`typescript
109
171
  import { createClient } from 'recker';
110
172
 
@@ -119,19 +181,25 @@ const { results, stats } = await client.batch([
119
181
  { path: '/users/3' }
120
182
  ], { mapResponse: r => r.json() });
121
183
  \`\`\``);
122
- }
123
- let output = `**Suggestion for: "${useCase}"**\n`;
124
- if (suggestions.length > 0) {
125
- output += suggestions.join('\n');
126
- }
127
- output += `\n\n**Related Documentation:**\n`;
128
- for (const result of results) {
129
- output += ` - ${result.title} (${result.path})\n`;
130
- if (result.snippet) {
131
- output += ` ${result.snippet.slice(0, 100)}...\n`;
132
184
  }
185
+ let output = `**Suggestion for: "${useCase}"**\n`;
186
+ if (suggestions.length > 0) {
187
+ output += suggestions.join('\n');
188
+ }
189
+ output += `\n\n**Related Documentation:**\n`;
190
+ for (const result of results) {
191
+ output += ` - ${result.title} (${result.path})\n`;
192
+ if (result.snippet) {
193
+ output += ` ${result.snippet.slice(0, 100)}...\n`;
194
+ }
195
+ }
196
+ spinner.succeed('Suggestions ready');
197
+ return output;
198
+ }
199
+ catch (error) {
200
+ spinner.fail(`Failed to generate suggestions: ${error}`);
201
+ throw error;
133
202
  }
134
- return output;
135
203
  }
136
204
  async getExamples(feature, options = {}) {
137
205
  await this.ensureInitialized();
@@ -13,7 +13,7 @@ import { ScrapeDocument } from '../../scrape/document.js';
13
13
  import colors from '../../utils/colors.js';
14
14
  import { getShellSearch } from './shell-search.js';
15
15
  import { openSearchPanel } from './search-panel.js';
16
- import { ScrollBuffer, parseScrollKey, parseMouseScroll, enableMouseReporting, disableMouseReporting } from './scroll-buffer.js';
16
+ import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
17
17
  let highlight;
18
18
  async function initDependencies() {
19
19
  if (!highlight) {
@@ -106,7 +106,7 @@ export class RekShell {
106
106
  console.clear();
107
107
  console.log(colors.bold(colors.cyan('Rek Console')));
108
108
  console.log(colors.gray('Chat with your APIs. Type "help" for magic.'));
109
- console.log(colors.gray('Page Up/Down or mouse scroll to view history.'));
109
+ console.log(colors.gray('Use Page Up/Down to view history.'));
110
110
  console.log(colors.gray('--------------------------------------------\n'));
111
111
  this.prompt();
112
112
  this.rl.on('line', async (line) => {
@@ -150,7 +150,6 @@ export class RekShell {
150
150
  disableMouseReporting();
151
151
  }
152
152
  setupScrollKeyHandler() {
153
- enableMouseReporting();
154
153
  if (process.stdin.isTTY) {
155
154
  const originalEmit = process.stdin.emit.bind(process.stdin);
156
155
  const self = this;
@@ -2060,10 +2059,12 @@ ${colors.bold('Network:')}
2060
2059
  this.rl.pause();
2061
2060
  await openSearchPanel(query.trim() || undefined);
2062
2061
  this.rl.resume();
2062
+ this.prompt();
2063
2063
  }
2064
2064
  catch (error) {
2065
2065
  console.error(colors.red(`Search failed: ${error.message}`));
2066
2066
  this.rl.resume();
2067
+ this.prompt();
2067
2068
  }
2068
2069
  }
2069
2070
  async runSuggest(useCase) {
@@ -2209,7 +2210,6 @@ ${colors.bold('Network:')}
2209
2210
  ${colors.bold('Navigation:')}
2210
2211
  ${colors.green('Page Up/Down')} Scroll through command history.
2211
2212
  ${colors.green('Home/End')} Jump to top/bottom of history.
2212
- ${colors.green('Mouse Scroll')} Scroll with mouse wheel.
2213
2213
  ${colors.green('Escape')} Exit scroll mode.
2214
2214
 
2215
2215
  ${colors.bold('Examples:')}
@@ -0,0 +1,16 @@
1
+ export interface SpinnerOptions {
2
+ text?: string;
3
+ color?: 'cyan' | 'green' | 'yellow' | 'red' | 'blue' | 'magenta' | 'white' | 'gray';
4
+ }
5
+ export interface Spinner {
6
+ text: string;
7
+ start(): Spinner;
8
+ stop(): Spinner;
9
+ succeed(text?: string): Spinner;
10
+ fail(text?: string): Spinner;
11
+ info(text?: string): Spinner;
12
+ warn(text?: string): Spinner;
13
+ isSpinning: boolean;
14
+ }
15
+ export declare function createSpinner(options?: SpinnerOptions | string): Spinner;
16
+ export default createSpinner;
@@ -0,0 +1,97 @@
1
+ import colors from '../../utils/colors.js';
2
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3
+ const FRAME_INTERVAL = 80;
4
+ class TerminalSpinner {
5
+ _text;
6
+ color;
7
+ frameIndex = 0;
8
+ intervalId = null;
9
+ stream = process.stderr;
10
+ isSpinning = false;
11
+ constructor(options = {}) {
12
+ this._text = options.text || '';
13
+ this.color = this.getColorFn(options.color || 'cyan');
14
+ }
15
+ getColorFn(color) {
16
+ switch (color) {
17
+ case 'green': return colors.green;
18
+ case 'yellow': return colors.yellow;
19
+ case 'red': return colors.red;
20
+ case 'blue': return colors.blue;
21
+ case 'magenta': return colors.magenta;
22
+ case 'gray': return colors.gray;
23
+ case 'white': return (s) => s;
24
+ case 'cyan':
25
+ default: return colors.cyan;
26
+ }
27
+ }
28
+ get text() {
29
+ return this._text;
30
+ }
31
+ set text(value) {
32
+ this._text = value;
33
+ }
34
+ start() {
35
+ if (this.isSpinning)
36
+ return this;
37
+ this.isSpinning = true;
38
+ this.frameIndex = 0;
39
+ this.stream.write('\x1b[?25l');
40
+ this.intervalId = setInterval(() => {
41
+ this.render();
42
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
43
+ }, FRAME_INTERVAL);
44
+ return this;
45
+ }
46
+ render() {
47
+ const frame = SPINNER_FRAMES[this.frameIndex];
48
+ const line = `${this.color(frame)} ${this._text}`;
49
+ this.stream.write(`\r\x1b[K${line}`);
50
+ }
51
+ clearLine() {
52
+ this.stream.write('\r\x1b[K');
53
+ }
54
+ stop() {
55
+ if (!this.isSpinning)
56
+ return this;
57
+ if (this.intervalId) {
58
+ clearInterval(this.intervalId);
59
+ this.intervalId = null;
60
+ }
61
+ this.isSpinning = false;
62
+ this.clearLine();
63
+ this.stream.write('\x1b[?25h');
64
+ return this;
65
+ }
66
+ succeed(text) {
67
+ this.stop();
68
+ const finalText = text ?? this._text;
69
+ console.log(`${colors.green('✔')} ${finalText}`);
70
+ return this;
71
+ }
72
+ fail(text) {
73
+ this.stop();
74
+ const finalText = text ?? this._text;
75
+ console.log(`${colors.red('✖')} ${finalText}`);
76
+ return this;
77
+ }
78
+ info(text) {
79
+ this.stop();
80
+ const finalText = text ?? this._text;
81
+ console.log(`${colors.blue('ℹ')} ${finalText}`);
82
+ return this;
83
+ }
84
+ warn(text) {
85
+ this.stop();
86
+ const finalText = text ?? this._text;
87
+ console.log(`${colors.yellow('⚠')} ${finalText}`);
88
+ return this;
89
+ }
90
+ }
91
+ export function createSpinner(options) {
92
+ if (typeof options === 'string') {
93
+ return new TerminalSpinner({ text: options });
94
+ }
95
+ return new TerminalSpinner(options);
96
+ }
97
+ export default createSpinner;
@@ -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.25-next.566f437",
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",
@@ -131,7 +131,6 @@
131
131
  "cheerio": "^1.0.0",
132
132
  "commander": "^14.0.2",
133
133
  "fuse.js": "^7.1.0",
134
- "ora": "^9.0.0",
135
134
  "undici": "^7.16.0",
136
135
  "zod": "^4.1.13"
137
136
  },
@@ -174,7 +173,6 @@
174
173
  "mitata": "^1.0.34",
175
174
  "needle": "^3.3.1",
176
175
  "node-fetch": "^3.3.2",
177
- "ora": "^9.0.0",
178
176
  "picocolors": "^1.1.1",
179
177
  "popsicle": "^12.1.2",
180
178
  "serve": "^14.2.5",