recker 1.0.23 → 1.0.26

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 {
@@ -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?: {
@@ -2,6 +2,7 @@ import { createHybridSearch, createEmbedder, isFastembedAvailable } from '../../
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,27 +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
- if (await isFastembedAvailable()) {
28
- this.hybridSearch.setEmbedder(createEmbedder());
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;
29
67
  }
30
- this.buildIndex();
31
- await this.hybridSearch.initialize(this.docsIndex);
32
- this.initialized = true;
33
68
  }
34
69
  resetIdleTimer() {
35
70
  if (this.idleTimer) {
@@ -48,22 +83,46 @@ export class ShellSearch {
48
83
  }
49
84
  async search(query, options = {}) {
50
85
  await this.ensureInitialized();
51
- const { limit = 5, category } = options;
86
+ const { limit = 5, category, silent = false } = options;
52
87
  if (!this.hybridSearch) {
53
88
  return [];
54
89
  }
55
- 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
+ }
56
110
  }
57
111
  async suggest(useCase) {
58
- await this.ensureInitialized();
59
- const results = await this.search(useCase, { limit: 3 });
60
- if (results.length === 0) {
61
- return `No suggestions found for: "${useCase}"\n\nTry searching for specific features like:\n - retry\n - cache\n - streaming\n - websocket\n - pagination`;
62
- }
63
- const useCaseLower = useCase.toLowerCase();
64
- const suggestions = [];
65
- if (useCaseLower.includes('retry') || useCaseLower.includes('fail') || useCaseLower.includes('error')) {
66
- 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:**
67
126
  \`\`\`typescript
68
127
  import { createClient } from 'recker';
69
128
 
@@ -77,9 +136,9 @@ const client = createClient({
77
136
  }
78
137
  });
79
138
  \`\`\``);
80
- }
81
- if (useCaseLower.includes('cache') || useCaseLower.includes('storage')) {
82
- suggestions.push(`\n**Cache Configuration:**
139
+ }
140
+ if (useCaseLower.includes('cache') || useCaseLower.includes('storage')) {
141
+ suggestions.push(`\n**Cache Configuration:**
83
142
  \`\`\`typescript
84
143
  import { createClient } from 'recker';
85
144
 
@@ -92,9 +151,9 @@ const client = createClient({
92
151
  }
93
152
  });
94
153
  \`\`\``);
95
- }
96
- if (useCaseLower.includes('stream') || useCaseLower.includes('sse') || useCaseLower.includes('ai') || useCaseLower.includes('openai')) {
97
- 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:**
98
157
  \`\`\`typescript
99
158
  import { createClient } from 'recker';
100
159
 
@@ -105,9 +164,9 @@ for await (const event of client.post('/v1/chat/completions', { body, stream: tr
105
164
  console.log(event.data);
106
165
  }
107
166
  \`\`\``);
108
- }
109
- if (useCaseLower.includes('parallel') || useCaseLower.includes('batch') || useCaseLower.includes('concurrent')) {
110
- 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:**
111
170
  \`\`\`typescript
112
171
  import { createClient } from 'recker';
113
172
 
@@ -122,19 +181,25 @@ const { results, stats } = await client.batch([
122
181
  { path: '/users/3' }
123
182
  ], { mapResponse: r => r.json() });
124
183
  \`\`\``);
125
- }
126
- let output = `**Suggestion for: "${useCase}"**\n`;
127
- if (suggestions.length > 0) {
128
- output += suggestions.join('\n');
129
- }
130
- output += `\n\n**Related Documentation:**\n`;
131
- for (const result of results) {
132
- output += ` - ${result.title} (${result.path})\n`;
133
- if (result.snippet) {
134
- output += ` ${result.snippet.slice(0, 100)}...\n`;
135
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;
136
202
  }
137
- return output;
138
203
  }
139
204
  async getExamples(feature, options = {}) {
140
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.23",
3
+ "version": "1.0.26",
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",