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.
- package/dist/cli/handler.js +3 -5
- package/dist/cli/index.js +54 -0
- package/dist/cli/tui/search-panel.d.ts +2 -0
- package/dist/cli/tui/search-panel.js +15 -5
- package/dist/cli/tui/shell-search.d.ts +6 -0
- package/dist/cli/tui/shell-search.js +104 -36
- package/dist/cli/tui/shell.js +4 -4
- package/dist/cli/tui/spinner.d.ts +16 -0
- package/dist/cli/tui/spinner.js +97 -0
- package/dist/mcp/search/embedder.d.ts +9 -0
- package/dist/mcp/search/embedder.js +83 -0
- package/dist/mcp/search/hybrid-search.d.ts +1 -0
- package/dist/mcp/search/hybrid-search.js +56 -4
- package/dist/mcp/search/index.d.ts +1 -0
- package/dist/mcp/search/index.js +1 -0
- package/dist/mcp/search/types.d.ts +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +57 -1
- package/dist/mini.d.ts +6 -0
- package/dist/mini.js +34 -29
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +1 -1
- package/dist/testing/mock-http-server.d.ts +25 -0
- package/dist/testing/mock-http-server.js +45 -0
- package/package.json +1 -3
package/dist/cli/handler.js
CHANGED
|
@@ -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
|
|
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 :
|
|
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
|
-
|
|
74
|
+
this.resizeHandler = () => {
|
|
73
75
|
this.updateDimensions();
|
|
74
76
|
this.render();
|
|
75
|
-
}
|
|
76
|
-
process.
|
|
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
|
-
|
|
103
|
-
|
|
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.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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();
|
package/dist/cli/tui/shell.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
59
|
-
|
|
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';
|
package/dist/mcp/search/index.js
CHANGED
|
@@ -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';
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/testing/index.js
CHANGED
|
@@ -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.
|
|
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",
|