recker 1.0.29 → 1.0.30

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.
Files changed (68) hide show
  1. package/README.md +28 -1
  2. package/dist/ai/client-ai.d.ts +41 -0
  3. package/dist/ai/client-ai.js +391 -0
  4. package/dist/ai/index.d.ts +2 -0
  5. package/dist/ai/index.js +2 -0
  6. package/dist/ai/memory.d.ts +35 -0
  7. package/dist/ai/memory.js +136 -0
  8. package/dist/browser/ai/client-ai.d.ts +41 -0
  9. package/dist/browser/ai/client-ai.js +391 -0
  10. package/dist/browser/ai/memory.d.ts +35 -0
  11. package/dist/browser/ai/memory.js +136 -0
  12. package/dist/browser/core/client.d.ts +6 -1
  13. package/dist/browser/core/client.js +18 -0
  14. package/dist/browser/transport/undici.js +11 -2
  15. package/dist/browser/types/ai-client.d.ts +32 -0
  16. package/dist/browser/types/ai-client.js +1 -0
  17. package/dist/browser/types/ai.d.ts +1 -1
  18. package/dist/cli/index.js +261 -1
  19. package/dist/cli/tui/scroll-buffer.js +4 -4
  20. package/dist/cli/tui/shell.d.ts +3 -0
  21. package/dist/cli/tui/shell.js +166 -19
  22. package/dist/core/client.d.ts +6 -1
  23. package/dist/core/client.js +18 -0
  24. package/dist/mcp/server.js +15 -0
  25. package/dist/mcp/tools/scrape.d.ts +3 -0
  26. package/dist/mcp/tools/scrape.js +156 -0
  27. package/dist/mcp/tools/security.d.ts +3 -0
  28. package/dist/mcp/tools/security.js +471 -0
  29. package/dist/mcp/tools/seo.d.ts +3 -0
  30. package/dist/mcp/tools/seo.js +427 -0
  31. package/dist/presets/anthropic.d.ts +3 -1
  32. package/dist/presets/anthropic.js +11 -1
  33. package/dist/presets/azure-openai.d.ts +3 -1
  34. package/dist/presets/azure-openai.js +11 -1
  35. package/dist/presets/cohere.d.ts +3 -1
  36. package/dist/presets/cohere.js +8 -2
  37. package/dist/presets/deepseek.d.ts +3 -1
  38. package/dist/presets/deepseek.js +8 -2
  39. package/dist/presets/fireworks.d.ts +3 -1
  40. package/dist/presets/fireworks.js +8 -2
  41. package/dist/presets/gemini.d.ts +3 -1
  42. package/dist/presets/gemini.js +8 -1
  43. package/dist/presets/groq.d.ts +3 -1
  44. package/dist/presets/groq.js +8 -2
  45. package/dist/presets/huggingface.d.ts +3 -1
  46. package/dist/presets/huggingface.js +8 -1
  47. package/dist/presets/mistral.d.ts +3 -1
  48. package/dist/presets/mistral.js +8 -2
  49. package/dist/presets/openai.d.ts +3 -1
  50. package/dist/presets/openai.js +9 -2
  51. package/dist/presets/perplexity.d.ts +3 -1
  52. package/dist/presets/perplexity.js +8 -2
  53. package/dist/presets/registry.d.ts +4 -0
  54. package/dist/presets/registry.js +48 -0
  55. package/dist/presets/replicate.d.ts +3 -1
  56. package/dist/presets/replicate.js +8 -1
  57. package/dist/presets/together.d.ts +3 -1
  58. package/dist/presets/together.js +8 -2
  59. package/dist/presets/xai.d.ts +3 -1
  60. package/dist/presets/xai.js +8 -2
  61. package/dist/scrape/spider.js +1 -1
  62. package/dist/transport/undici.js +11 -2
  63. package/dist/types/ai-client.d.ts +32 -0
  64. package/dist/types/ai-client.js +1 -0
  65. package/dist/types/ai.d.ts +1 -1
  66. package/dist/utils/colors.d.ts +2 -0
  67. package/dist/utils/colors.js +4 -0
  68. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ import type { AIResponse, AIStream, ChatMessage, AIProvider } from './ai.js';
2
+ export interface AIMemoryConfig {
3
+ maxPairs?: number;
4
+ systemPrompt?: string;
5
+ }
6
+ export interface PresetAIConfig {
7
+ provider: AIProvider;
8
+ apiKey?: string;
9
+ model: string;
10
+ baseUrl?: string;
11
+ memory?: AIMemoryConfig;
12
+ organization?: string;
13
+ headers?: Record<string, string>;
14
+ resourceName?: string;
15
+ deploymentName?: string;
16
+ apiVersion?: string;
17
+ }
18
+ export interface ClientAI {
19
+ chat(prompt: string): Promise<AIResponse>;
20
+ chatStream(prompt: string): Promise<AIStream>;
21
+ prompt(prompt: string): Promise<AIResponse>;
22
+ promptStream(prompt: string): Promise<AIStream>;
23
+ clearMemory(): void;
24
+ getMemory(): readonly ChatMessage[];
25
+ setMemoryConfig(config: Partial<AIMemoryConfig>): void;
26
+ getMemoryConfig(): AIMemoryConfig;
27
+ readonly provider: AIProvider;
28
+ readonly model: string;
29
+ }
30
+ export interface ClientOptionsWithAI {
31
+ _aiConfig?: PresetAIConfig;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- export type AIProvider = 'openai' | 'anthropic' | 'google' | 'replicate' | 'huggingface' | 'ollama' | 'custom';
1
+ export type AIProvider = 'openai' | 'anthropic' | 'google' | 'replicate' | 'huggingface' | 'ollama' | 'groq' | 'mistral' | 'cohere' | 'together' | 'perplexity' | 'deepseek' | 'fireworks' | 'xai' | 'azure-openai' | 'cloudflare-workers-ai' | 'custom';
2
2
  export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
3
3
  export interface ChatMessage {
4
4
  role: MessageRole;
package/dist/cli/index.js CHANGED
@@ -330,7 +330,17 @@ complete -F _rek_completions rek
330
330
  .alias('interactive')
331
331
  .alias('repl')
332
332
  .description('Start the interactive Rek Shell')
333
- .action(async () => {
333
+ .option('-e, --env [path]', 'Load .env file (auto-loads from cwd by default)')
334
+ .action(async (options) => {
335
+ if (options.env !== false) {
336
+ try {
337
+ const envPath = typeof options.env === 'string' ? options.env : join(process.cwd(), '.env');
338
+ await fs.access(envPath);
339
+ await loadEnvFile(options.env);
340
+ }
341
+ catch {
342
+ }
343
+ }
334
344
  const { RekShell } = await import('./tui/shell.js');
335
345
  const shell = new RekShell();
336
346
  shell.start();
@@ -520,6 +530,256 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
520
530
  process.exit(1);
521
531
  }
522
532
  });
533
+ program
534
+ .command('spider')
535
+ .description('Crawl a website following internal links')
536
+ .argument('<url>', 'Starting URL to crawl')
537
+ .option('-d, --depth <n>', 'Maximum depth to crawl', '5')
538
+ .option('-l, --limit <n>', 'Maximum pages to crawl', '100')
539
+ .option('-c, --concurrency <n>', 'Concurrent requests', '5')
540
+ .option('--delay <ms>', 'Delay between requests in ms', '100')
541
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
542
+ .option('-o, --output <file>', 'Write JSON results to file')
543
+ .addHelpText('after', `
544
+ ${colors.bold(colors.yellow('Examples:'))}
545
+ ${colors.green('$ rek spider example.com')} ${colors.gray('Crawl with defaults')}
546
+ ${colors.green('$ rek spider example.com -d 3 -l 50')} ${colors.gray('Depth 3, max 50 pages')}
547
+ ${colors.green('$ rek spider example.com --format json')} ${colors.gray('Output as JSON')}
548
+ ${colors.green('$ rek spider example.com -o results.json')} ${colors.gray('Save to file')}
549
+
550
+ ${colors.bold(colors.yellow('Options:'))}
551
+ ${colors.cyan('--depth, -d')} Max link depth to follow (default: 5)
552
+ ${colors.cyan('--limit, -l')} Max pages to crawl (default: 100)
553
+ ${colors.cyan('--concurrency, -c')} Parallel requests (default: 5)
554
+ ${colors.cyan('--delay')} Ms between requests (default: 100)
555
+ `)
556
+ .action(async (url, options) => {
557
+ if (!url.startsWith('http'))
558
+ url = `https://${url}`;
559
+ const isJson = options.format === 'json';
560
+ const maxDepth = parseInt(options.depth || '5', 10);
561
+ const maxPages = parseInt(options.limit || '100', 10);
562
+ const concurrency = parseInt(options.concurrency || '5', 10);
563
+ const delay = parseInt(options.delay || '100', 10);
564
+ const { Spider } = await import('../scrape/spider.js');
565
+ if (!isJson) {
566
+ console.log(colors.gray(`\nCrawling ${url}...`));
567
+ console.log(colors.gray(` Depth: ${maxDepth}, Limit: ${maxPages}, Concurrency: ${concurrency}\n`));
568
+ }
569
+ try {
570
+ const startTime = performance.now();
571
+ const spider = new Spider({
572
+ maxDepth,
573
+ maxPages,
574
+ concurrency,
575
+ delay,
576
+ onProgress: (progress) => {
577
+ if (!isJson) {
578
+ process.stdout.write(`\r${colors.cyan('⟳')} Crawled: ${colors.bold(String(progress.crawled))} | Queued: ${progress.queued} | Current: ${colors.gray(progress.currentUrl.slice(0, 50))}...`);
579
+ }
580
+ },
581
+ });
582
+ const result = await spider.crawl(url);
583
+ const duration = Math.round(performance.now() - startTime);
584
+ if (!isJson) {
585
+ process.stdout.write('\r' + ' '.repeat(100) + '\r');
586
+ }
587
+ if (isJson || options.output) {
588
+ const jsonOutput = {
589
+ startUrl: result.startUrl,
590
+ crawledAt: new Date().toISOString(),
591
+ duration,
592
+ summary: {
593
+ totalPages: result.pages.length,
594
+ successCount: result.pages.filter(p => !p.error).length,
595
+ errorCount: result.errors.length,
596
+ uniqueUrls: result.visited.size,
597
+ },
598
+ pages: result.pages.map(p => ({
599
+ url: p.url,
600
+ status: p.status,
601
+ title: p.title,
602
+ depth: p.depth,
603
+ linksCount: p.links.length,
604
+ duration: p.duration,
605
+ error: p.error,
606
+ })),
607
+ errors: result.errors,
608
+ };
609
+ if (options.output) {
610
+ await fs.writeFile(options.output, JSON.stringify(jsonOutput, null, 2));
611
+ console.log(colors.green(`✓ Results saved to ${options.output}`));
612
+ }
613
+ if (isJson) {
614
+ console.log(JSON.stringify(jsonOutput, null, 2));
615
+ return;
616
+ }
617
+ }
618
+ console.log(`${colors.bold(colors.cyan('🕷️ Spider Crawl Complete'))}`);
619
+ console.log(`${colors.gray('URL:')} ${url}`);
620
+ console.log(`${colors.gray('Duration:')} ${(duration / 1000).toFixed(1)}s`);
621
+ console.log('');
622
+ const successCount = result.pages.filter(p => !p.error).length;
623
+ console.log(`${colors.bold('Summary:')}`);
624
+ console.log(` ${colors.green('✓')} Pages crawled: ${colors.bold(String(successCount))}`);
625
+ console.log(` ${colors.red('✗')} Errors: ${colors.bold(String(result.errors.length))}`);
626
+ console.log(` ${colors.gray('○')} Unique URLs: ${result.visited.size}`);
627
+ console.log('');
628
+ const byDepth = new Map();
629
+ for (const page of result.pages) {
630
+ byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
631
+ }
632
+ console.log(`${colors.bold('Pages by Depth:')}`);
633
+ for (const [depth, count] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
634
+ console.log(` ${colors.gray(`Depth ${depth}:`)} ${count} pages`);
635
+ }
636
+ console.log('');
637
+ if (result.errors.length > 0) {
638
+ console.log(`${colors.bold(colors.red('Errors:'))}`);
639
+ for (const err of result.errors.slice(0, 10)) {
640
+ const shortUrl = err.url.replace(url, '');
641
+ const statusMatch = err.error.match(/status code (\d+)/);
642
+ const errorMsg = statusMatch ? `HTTP ${statusMatch[1]}` : err.error.slice(0, 50);
643
+ console.log(` ${colors.red('✗')} ${colors.gray(shortUrl || '/')} → ${errorMsg}`);
644
+ }
645
+ if (result.errors.length > 10) {
646
+ console.log(` ${colors.gray(` ... and ${result.errors.length - 10} more`)}`);
647
+ }
648
+ console.log('');
649
+ }
650
+ const successPages = result.pages.filter(p => !p.error).slice(0, 10);
651
+ if (successPages.length > 0) {
652
+ console.log(`${colors.bold('Sample Pages:')}`);
653
+ for (const page of successPages) {
654
+ const shortUrl = page.url.replace(url, '');
655
+ console.log(` ${colors.green('✓')} ${colors.gray((shortUrl || '/').padEnd(40))} ${page.title.slice(0, 40)}`);
656
+ }
657
+ if (result.pages.length > 10) {
658
+ console.log(` ${colors.gray(` ... and ${result.pages.length - 10} more`)}`);
659
+ }
660
+ }
661
+ }
662
+ catch (error) {
663
+ console.error(colors.red(`\nSpider failed: ${error.message}`));
664
+ process.exit(1);
665
+ }
666
+ });
667
+ program
668
+ .command('ai')
669
+ .description('Send a single AI prompt (no memory/context)')
670
+ .argument('<preset>', 'AI preset to use (e.g., @openai, @anthropic, @groq)')
671
+ .argument('<prompt...>', 'The prompt to send')
672
+ .option('-m, --model <model>', 'Override default model')
673
+ .option('-t, --temperature <temp>', 'Temperature (0-1)', '0.7')
674
+ .option('--max-tokens <tokens>', 'Max tokens in response', '2048')
675
+ .option('-w, --wait', 'Wait for full response (disable streaming)')
676
+ .option('-j, --json', 'Output raw JSON response')
677
+ .option('-e, --env [path]', 'Load .env file (auto-loads from cwd if exists)')
678
+ .addHelpText('after', `
679
+ ${colors.bold(colors.yellow('Examples:'))}
680
+ ${colors.green('$ rek ai @openai "What is the capital of France?"')}
681
+ ${colors.green('$ rek ai @anthropic "Explain quantum computing" -m claude-sonnet-4-20250514')}
682
+ ${colors.green('$ rek ai @groq "Write a haiku" --wait')}
683
+ ${colors.green('$ rek ai @openai "Translate to Spanish: Hello world"')}
684
+
685
+ ${colors.bold(colors.yellow('Available AI Presets:'))}
686
+ ${colors.cyan('@openai')} OpenAI (GPT-4o, GPT-5.1)
687
+ ${colors.cyan('@anthropic')} Anthropic (Claude)
688
+ ${colors.cyan('@groq')} Groq (fast inference)
689
+ ${colors.cyan('@google')} Google (Gemini)
690
+ ${colors.cyan('@xai')} xAI (Grok)
691
+ ${colors.cyan('@mistral')} Mistral AI
692
+ ${colors.cyan('@cohere')} Cohere
693
+ ${colors.cyan('@deepseek')} DeepSeek
694
+ ${colors.cyan('@perplexity')} Perplexity
695
+ ${colors.cyan('@together')} Together AI
696
+ ${colors.cyan('@fireworks')} Fireworks AI
697
+ ${colors.cyan('@replicate')} Replicate
698
+ ${colors.cyan('@huggingface')} Hugging Face
699
+
700
+ ${colors.bold(colors.yellow('Note:'))}
701
+ This command sends a single prompt without conversation memory.
702
+ For chat with memory, use: ${colors.cyan('rek shell')} then ${colors.cyan('@openai Your message')}
703
+ `)
704
+ .action(async (preset, promptParts, options) => {
705
+ if (options.env !== undefined) {
706
+ await loadEnvFile(options.env);
707
+ }
708
+ else {
709
+ try {
710
+ const envPath = join(process.cwd(), '.env');
711
+ await fs.access(envPath);
712
+ await loadEnvFile(true);
713
+ }
714
+ catch {
715
+ }
716
+ }
717
+ let presetName = preset;
718
+ if (presetName.startsWith('@')) {
719
+ presetName = presetName.slice(1);
720
+ }
721
+ const presetConfig = resolvePreset(presetName);
722
+ if (!presetConfig) {
723
+ console.error(colors.red(`Unknown AI preset: @${presetName}`));
724
+ console.log(colors.gray('Available AI presets: @openai, @anthropic, @groq, @google, @xai, @mistral, @cohere'));
725
+ process.exit(1);
726
+ }
727
+ if (!presetConfig._aiConfig) {
728
+ console.error(colors.red(`Preset @${presetName} does not support AI features.`));
729
+ console.log(colors.gray('Use an AI preset like @openai, @anthropic, @groq, etc.'));
730
+ process.exit(1);
731
+ }
732
+ const prompt = promptParts.join(' ');
733
+ if (!prompt.trim()) {
734
+ console.error(colors.red('Error: Prompt is required'));
735
+ process.exit(1);
736
+ }
737
+ try {
738
+ const { createClient } = await import('../core/client.js');
739
+ const client = createClient(presetConfig);
740
+ if (options.model) {
741
+ client.ai.setMemoryConfig({ systemPrompt: undefined });
742
+ client._aiConfig.model = options.model;
743
+ }
744
+ if (!options.json) {
745
+ console.log(colors.gray(`Using @${presetName} (${client._aiConfig.model})...\n`));
746
+ }
747
+ if (options.wait || options.json) {
748
+ const response = await client.ai.prompt(prompt);
749
+ if (options.json) {
750
+ console.log(JSON.stringify({
751
+ content: response.content,
752
+ model: response.model,
753
+ usage: response.usage,
754
+ finishReason: response.finishReason,
755
+ }, null, 2));
756
+ }
757
+ else {
758
+ console.log(response.content);
759
+ if (response.usage) {
760
+ console.log(colors.gray(`\n─────────────────────────────────────────`));
761
+ console.log(colors.gray(`Tokens: ${response.usage.inputTokens} in / ${response.usage.outputTokens} out`));
762
+ }
763
+ }
764
+ }
765
+ else {
766
+ const stream = await client.ai.promptStream(prompt);
767
+ for await (const event of stream) {
768
+ if (event.type === 'text') {
769
+ process.stdout.write(event.content);
770
+ }
771
+ }
772
+ console.log('');
773
+ }
774
+ }
775
+ catch (error) {
776
+ console.error(colors.red(`AI request failed: ${error.message}`));
777
+ if (error.cause) {
778
+ console.error(colors.gray(error.cause.message || error.cause));
779
+ }
780
+ process.exit(1);
781
+ }
782
+ });
523
783
  program
524
784
  .command('ip')
525
785
  .description('Get IP address intelligence using local GeoLite2 database')
@@ -119,17 +119,17 @@ export class ScrollBuffer extends EventEmitter {
119
119
  }
120
120
  export function parseScrollKey(data) {
121
121
  const str = data.toString();
122
- if (str === '\x1b[5~' || str === '\x1bOy')
122
+ if (str === '\x1b[5~' || str === '\x1bOy' || str === '\x1b[5;5~' || str === '\x1b[5;2~')
123
123
  return 'pageUp';
124
- if (str === '\x1b[6~' || str === '\x1bOs')
124
+ if (str === '\x1b[6~' || str === '\x1bOs' || str === '\x1b[6;5~' || str === '\x1b[6;2~')
125
125
  return 'pageDown';
126
126
  if (str === '\x1b[1;2A')
127
127
  return 'scrollUp';
128
128
  if (str === '\x1b[1;2B')
129
129
  return 'scrollDown';
130
- if (str === '\x1b[H' || str === '\x1b[1~' || str === '\x1bOH')
130
+ if (str === '\x1b[H' || str === '\x1b[1~' || str === '\x1bOH' || str === '\x1b[7~')
131
131
  return 'home';
132
- if (str === '\x1b[F' || str === '\x1b[4~' || str === '\x1bOF')
132
+ if (str === '\x1b[F' || str === '\x1b[4~' || str === '\x1bOF' || str === '\x1b[8~')
133
133
  return 'end';
134
134
  if (str === 'q' || str === 'Q')
135
135
  return 'quit';
@@ -13,6 +13,7 @@ export declare class RekShell {
13
13
  private scrollBuffer;
14
14
  private originalStdoutWrite;
15
15
  private inScrollMode;
16
+ private aiClients;
16
17
  constructor();
17
18
  private ensureInitialized;
18
19
  private getPrompt;
@@ -31,6 +32,8 @@ export declare class RekShell {
31
32
  private handleCommand;
32
33
  private runInteractiveMode;
33
34
  private runAIChat;
35
+ private runAIPresetChat;
36
+ private clearAIMemory;
34
37
  private runLoadTest;
35
38
  private parseLine;
36
39
  private setBaseUrl;
@@ -16,6 +16,7 @@ import { getShellSearch } from './shell-search.js';
16
16
  import { openSearchPanel } from './search-panel.js';
17
17
  import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
18
18
  import { analyzeSeo, SeoSpider } from '../../seo/index.js';
19
+ import { resolvePreset } from '../presets.js';
19
20
  let highlight;
20
21
  async function initDependencies() {
21
22
  if (!highlight) {
@@ -43,6 +44,7 @@ export class RekShell {
43
44
  scrollBuffer;
44
45
  originalStdoutWrite = null;
45
46
  inScrollMode = false;
47
+ aiClients = new Map();
46
48
  constructor() {
47
49
  this.client = createClient({
48
50
  baseUrl: 'http://localhost',
@@ -94,8 +96,10 @@ export class RekShell {
94
96
  const commands = [
95
97
  'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
96
98
  'ws', 'udp', 'load', 'chat', 'ai',
99
+ '@openai', '@anthropic', '@groq', '@google', '@xai', '@mistral', '@cohere', '@deepseek', '@fireworks', '@together', '@perplexity',
100
+ 'ai:clear',
97
101
  'whois', 'tls', 'ssl', 'security', 'ip', 'dns', 'dns:propagate', 'dns:email', 'rdap', 'ping',
98
- 'scrap', 'spider', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
102
+ 'scrap', 'spider', 'seo', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
99
103
  '?', 'search', 'suggest', 'example',
100
104
  'help', 'clear', 'exit', 'set', 'url', 'vars', 'env'
101
105
  ];
@@ -173,20 +177,36 @@ export class RekShell {
173
177
  }
174
178
  return true;
175
179
  }
176
- const scrollKey = parseScrollKey(data);
177
- if (scrollKey) {
178
- if (scrollKey === 'quit') {
179
- if (self.inScrollMode) {
180
+ try {
181
+ const scrollKey = parseScrollKey(data);
182
+ if (scrollKey) {
183
+ if (scrollKey === 'quit') {
184
+ if (self.inScrollMode) {
185
+ self.exitScrollMode();
186
+ return true;
187
+ }
188
+ return originalEmit(event, ...args);
189
+ }
190
+ self.handleScrollKey(scrollKey);
191
+ return true;
192
+ }
193
+ if (self.inScrollMode) {
194
+ if (str === '\x1b[A') {
195
+ self.handleScrollKey('scrollUp');
196
+ return true;
197
+ }
198
+ if (str === '\x1b[B') {
199
+ self.handleScrollKey('scrollDown');
200
+ return true;
201
+ }
202
+ if (str === '\x1b' || str === '\x1b\x1b') {
180
203
  self.exitScrollMode();
181
204
  return true;
182
205
  }
183
- return originalEmit(event, ...args);
206
+ return true;
184
207
  }
185
- self.handleScrollKey(scrollKey);
186
- return true;
187
208
  }
188
- if (self.inScrollMode) {
189
- return true;
209
+ catch {
190
210
  }
191
211
  }
192
212
  return originalEmit(event, ...args);
@@ -194,6 +214,9 @@ export class RekShell {
194
214
  }
195
215
  }
196
216
  handleScrollKey(key) {
217
+ if (!this.originalStdoutWrite) {
218
+ return;
219
+ }
197
220
  let needsRedraw = false;
198
221
  switch (key) {
199
222
  case 'pageUp':
@@ -250,11 +273,15 @@ export class RekShell {
250
273
  enterScrollMode() {
251
274
  if (this.inScrollMode)
252
275
  return;
276
+ if (!this.originalStdoutWrite)
277
+ return;
253
278
  this.inScrollMode = true;
254
- this.rl.pause();
255
- if (this.originalStdoutWrite) {
256
- this.originalStdoutWrite('\x1b[?25l');
279
+ try {
280
+ this.rl.pause();
281
+ }
282
+ catch {
257
283
  }
284
+ this.originalStdoutWrite('\x1b[?25l');
258
285
  this.renderScrollView();
259
286
  }
260
287
  exitScrollMode() {
@@ -288,7 +315,7 @@ export class RekShell {
288
315
  const scrollInfo = this.scrollBuffer.isScrolledUp
289
316
  ? colors.yellow(`↑ ${this.scrollBuffer.position} lines | ${info.percent}% | `)
290
317
  : '';
291
- const helpText = colors.gray('Page Up/Down • Home/End • Q to exit');
318
+ const helpText = colors.gray('↑↓/PgUp/PgDn • Home/End • Esc/Q to exit');
292
319
  const statusBar = `\x1b[${rows};1H\x1b[7m ${scrollInfo}${helpText} \x1b[0m`;
293
320
  this.originalStdoutWrite(statusBar);
294
321
  }
@@ -297,6 +324,21 @@ export class RekShell {
297
324
  this.rl.prompt();
298
325
  }
299
326
  async handleCommand(input) {
327
+ if (input.startsWith('@')) {
328
+ const spaceIdx = input.indexOf(' ');
329
+ if (spaceIdx > 1) {
330
+ const presetName = input.slice(1, spaceIdx).toLowerCase();
331
+ const message = input.slice(spaceIdx + 1).trim();
332
+ if (message) {
333
+ await this.runAIPresetChat(presetName, message);
334
+ return;
335
+ }
336
+ }
337
+ console.log(colors.yellow('Usage: @<preset> <message>'));
338
+ console.log(colors.gray('Example: @openai Hello, how are you?'));
339
+ console.log(colors.gray('Available AI presets: openai, anthropic, groq, google, xai, mistral, cohere'));
340
+ return;
341
+ }
300
342
  if (input.endsWith('?') && !input.startsWith('?') && input.length > 1) {
301
343
  await this.runSearch(input.slice(0, -1).trim());
302
344
  return;
@@ -312,6 +354,9 @@ export class RekShell {
312
354
  case 'clear':
313
355
  console.clear();
314
356
  return;
357
+ case 'ai:clear':
358
+ this.clearAIMemory(parts[1]);
359
+ return;
315
360
  case 'exit':
316
361
  case 'quit':
317
362
  this.rl.close();
@@ -507,6 +552,96 @@ export class RekShell {
507
552
  await startAIChat(rl, provider, apiKey, model);
508
553
  });
509
554
  }
555
+ async runAIPresetChat(presetName, message) {
556
+ try {
557
+ let client = this.aiClients.get(presetName);
558
+ if (!client) {
559
+ const presetConfig = resolvePreset(presetName);
560
+ if (!presetConfig) {
561
+ console.log(colors.red(`Unknown AI preset: @${presetName}`));
562
+ console.log(colors.gray('Available AI presets: openai, anthropic, groq, google, xai, mistral, cohere, deepseek, fireworks, together, perplexity'));
563
+ return;
564
+ }
565
+ if (!presetConfig._aiConfig) {
566
+ console.log(colors.red(`Preset @${presetName} does not support AI features.`));
567
+ console.log(colors.gray('Use an AI preset like @openai, @anthropic, @groq, etc.'));
568
+ return;
569
+ }
570
+ client = createClient(presetConfig);
571
+ this.aiClients.set(presetName, client);
572
+ }
573
+ if (!client.hasAI) {
574
+ console.log(colors.red(`Preset @${presetName} does not have AI capabilities.`));
575
+ return;
576
+ }
577
+ const model = client._aiConfig?.model || presetName;
578
+ console.log(colors.gray(`\n${presetName} (${model}) is thinking...`));
579
+ const stream = await client.ai.chatStream(message);
580
+ process.stdout.write('\n');
581
+ for await (const event of stream) {
582
+ if (event.type === 'text') {
583
+ process.stdout.write(colors.orange(event.content));
584
+ }
585
+ else if (event.type === 'error') {
586
+ console.log(colors.red(`\nError: ${event.error}`));
587
+ }
588
+ }
589
+ const memory = client.ai.getMemory();
590
+ const pairs = Math.floor(memory.length / 2);
591
+ console.log(colors.reset(''));
592
+ console.log(colors.gray(`Memory: ${pairs}/12 pairs (${memory.length} messages)`));
593
+ }
594
+ catch (error) {
595
+ if (error.message?.includes('API key')) {
596
+ console.log(colors.red(`\nMissing API key for @${presetName}`));
597
+ const envVarMap = {
598
+ openai: 'OPENAI_API_KEY',
599
+ anthropic: 'ANTHROPIC_API_KEY',
600
+ google: 'GOOGLE_API_KEY',
601
+ groq: 'GROQ_API_KEY',
602
+ xai: 'XAI_API_KEY',
603
+ mistral: 'MISTRAL_API_KEY',
604
+ cohere: 'COHERE_API_KEY',
605
+ deepseek: 'DEEPSEEK_API_KEY',
606
+ fireworks: 'FIREWORKS_API_KEY',
607
+ together: 'TOGETHER_API_KEY',
608
+ perplexity: 'PERPLEXITY_API_KEY',
609
+ };
610
+ const envVar = envVarMap[presetName] || `${presetName.toUpperCase()}_API_KEY`;
611
+ console.log(colors.gray(`Set ${envVar} environment variable to use this preset.`));
612
+ }
613
+ else {
614
+ console.log(colors.red(`\nError: ${error.message || error}`));
615
+ }
616
+ }
617
+ }
618
+ clearAIMemory(presetName) {
619
+ if (presetName) {
620
+ const client = this.aiClients.get(presetName);
621
+ if (client && client.hasAI) {
622
+ client.ai.clearMemory();
623
+ console.log(colors.green(`Cleared AI memory for @${presetName}`));
624
+ }
625
+ else {
626
+ console.log(colors.yellow(`No active AI session for @${presetName}`));
627
+ }
628
+ }
629
+ else {
630
+ let cleared = 0;
631
+ for (const [name, client] of this.aiClients) {
632
+ if (client.hasAI) {
633
+ client.ai.clearMemory();
634
+ cleared++;
635
+ }
636
+ }
637
+ if (cleared > 0) {
638
+ console.log(colors.green(`Cleared AI memory for ${cleared} preset(s)`));
639
+ }
640
+ else {
641
+ console.log(colors.yellow('No active AI sessions to clear'));
642
+ }
643
+ }
644
+ }
510
645
  async runLoadTest(args) {
511
646
  let targetUrl = '';
512
647
  let users = 50;
@@ -1494,7 +1629,7 @@ ${colors.bold('Network:')}
1494
1629
  }
1495
1630
  async runSpider(args) {
1496
1631
  let url = '';
1497
- let maxDepth = 3;
1632
+ let maxDepth = 5;
1498
1633
  let maxPages = 100;
1499
1634
  let concurrency = 5;
1500
1635
  let seoEnabled = false;
@@ -1502,7 +1637,7 @@ ${colors.bold('Network:')}
1502
1637
  for (let i = 0; i < args.length; i++) {
1503
1638
  const arg = args[i];
1504
1639
  if (arg.startsWith('depth=')) {
1505
- maxDepth = parseInt(arg.split('=')[1]) || 4;
1640
+ maxDepth = parseInt(arg.split('=')[1]) || 5;
1506
1641
  }
1507
1642
  else if (arg.startsWith('limit=')) {
1508
1643
  maxPages = parseInt(arg.split('=')[1]) || 100;
@@ -1524,7 +1659,7 @@ ${colors.bold('Network:')}
1524
1659
  if (!this.baseUrl) {
1525
1660
  console.log(colors.yellow('Usage: spider <url> [options]'));
1526
1661
  console.log(colors.gray(' Options:'));
1527
- console.log(colors.gray(' depth=4 Max crawl depth'));
1662
+ console.log(colors.gray(' depth=5 Max crawl depth'));
1528
1663
  console.log(colors.gray(' limit=100 Max pages to crawl'));
1529
1664
  console.log(colors.gray(' concurrency=5 Concurrent requests'));
1530
1665
  console.log(colors.gray(' seo Enable SEO analysis'));
@@ -2656,6 +2791,17 @@ ${colors.bold('Network:')}
2656
2791
  ${colors.green('ws <url>')} Start interactive WebSocket session.
2657
2792
  ${colors.green('udp <url>')} Send UDP packet.
2658
2793
 
2794
+ ${colors.bold('AI Chat:')}
2795
+ ${colors.green('@openai <message>')} Chat with OpenAI (GPT) with memory.
2796
+ ${colors.green('@anthropic <msg>')} Chat with Anthropic (Claude) with memory.
2797
+ ${colors.green('@groq <message>')} Chat with Groq (fast inference).
2798
+ ${colors.green('@google <message>')} Chat with Google (Gemini).
2799
+ ${colors.green('@xai <message>')} Chat with xAI (Grok).
2800
+ ${colors.green('@mistral <message>')} Chat with Mistral AI.
2801
+ ${colors.gray('Memory:')} ${colors.white('12 pairs (24 messages)')} preserved per preset.
2802
+ ${colors.gray('Env:')} Set ${colors.white('OPENAI_API_KEY')}, ${colors.white('ANTHROPIC_API_KEY')}, etc.
2803
+ ${colors.green('ai:clear [preset]')} Clear AI memory (all or specific preset).
2804
+
2659
2805
  ${colors.bold('Network Tools:')}
2660
2806
  ${colors.green('whois <domain>')} WHOIS lookup (domain or IP).
2661
2807
  ${colors.green('tls <host> [port]')} Inspect TLS/SSL certificate.
@@ -2687,7 +2833,7 @@ ${colors.bold('Network:')}
2687
2833
  ${colors.bold('Web Crawler:')}
2688
2834
  ${colors.green('spider <url>')} Crawl website following internal links.
2689
2835
  ${colors.gray('Options:')}
2690
- ${colors.white('--depth=4')} ${colors.gray('Maximum depth to crawl')}
2836
+ ${colors.white('--depth=5')} ${colors.gray('Maximum depth to crawl')}
2691
2837
  ${colors.white('--limit=100')} ${colors.gray('Maximum pages to crawl')}
2692
2838
  ${colors.white('--concurrency=5')} ${colors.gray('Parallel requests')}
2693
2839
 
@@ -2707,7 +2853,8 @@ ${colors.bold('Network:')}
2707
2853
  › get /json
2708
2854
  › post /post name="Neo" active:=true role:Admin
2709
2855
  › load /heavy-endpoint users=100 mode=stress
2710
- chat openai gpt-5.1
2856
+ @openai What is the capital of France?
2857
+ › @anthropic Explain quantum computing
2711
2858
  › spider example.com depth=2 limit=50
2712
2859
  `);
2713
2860
  }