grokcodecli 0.1.2 → 0.1.4
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/.claude/settings.local.json +2 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/manager.js +1 -1
- package/dist/config/manager.js.map +1 -1
- package/dist/conversation/chat.d.ts +1 -0
- package/dist/conversation/chat.d.ts.map +1 -1
- package/dist/conversation/chat.js +129 -224
- package/dist/conversation/chat.js.map +1 -1
- package/dist/utils/selector.d.ts +8 -0
- package/dist/utils/selector.d.ts.map +1 -0
- package/dist/utils/selector.js +138 -0
- package/dist/utils/selector.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/config/manager.ts +1 -1
- package/src/conversation/chat.ts +132 -224
- package/src/utils/selector.ts +165 -0
package/src/conversation/chat.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { PermissionManager } from '../permissions/manager.js';
|
|
|
11
11
|
import { HistoryManager, ConversationSession } from './history.js';
|
|
12
12
|
import { ConfigManager } from '../config/manager.js';
|
|
13
13
|
import { drawBox, randomTip, divider, formatCodeBlock, progressBar } from '../utils/ui.js';
|
|
14
|
+
import { interactiveSelect, SelectorOption } from '../utils/selector.js';
|
|
14
15
|
|
|
15
16
|
// Get version from package.json
|
|
16
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -76,12 +77,51 @@ export class GrokChat {
|
|
|
76
77
|
private sessionStartTime: Date = new Date();
|
|
77
78
|
private apiKey: string;
|
|
78
79
|
|
|
80
|
+
// All slash commands for autocomplete
|
|
81
|
+
private static SLASH_COMMANDS = [
|
|
82
|
+
'/help', '/h',
|
|
83
|
+
'/clear', '/c',
|
|
84
|
+
'/save', '/s',
|
|
85
|
+
'/exit', '/q',
|
|
86
|
+
'/history',
|
|
87
|
+
'/resume',
|
|
88
|
+
'/rename',
|
|
89
|
+
'/export',
|
|
90
|
+
'/compact',
|
|
91
|
+
'/config',
|
|
92
|
+
'/model',
|
|
93
|
+
'/stream',
|
|
94
|
+
'/permissions',
|
|
95
|
+
'/status',
|
|
96
|
+
'/context',
|
|
97
|
+
'/cost',
|
|
98
|
+
'/usage',
|
|
99
|
+
'/doctor',
|
|
100
|
+
'/version',
|
|
101
|
+
'/init',
|
|
102
|
+
'/review',
|
|
103
|
+
'/terminal-setup',
|
|
104
|
+
'/add-dir',
|
|
105
|
+
'/pwd',
|
|
106
|
+
];
|
|
107
|
+
|
|
79
108
|
constructor(options: ChatOptions) {
|
|
80
109
|
this.apiKey = options.apiKey;
|
|
81
|
-
this.client = new GrokClient(options.apiKey, options.model || 'grok-4-
|
|
110
|
+
this.client = new GrokClient(options.apiKey, options.model || 'grok-4-1-fast-reasoning');
|
|
111
|
+
|
|
112
|
+
// Autocomplete function for slash commands
|
|
113
|
+
const completer = (line: string): [string[], string] => {
|
|
114
|
+
if (line.startsWith('/')) {
|
|
115
|
+
const hits = GrokChat.SLASH_COMMANDS.filter(cmd => cmd.startsWith(line));
|
|
116
|
+
return [hits.length ? hits : GrokChat.SLASH_COMMANDS, line];
|
|
117
|
+
}
|
|
118
|
+
return [[], line];
|
|
119
|
+
};
|
|
120
|
+
|
|
82
121
|
this.rl = readline.createInterface({
|
|
83
122
|
input: process.stdin,
|
|
84
123
|
output: process.stdout,
|
|
124
|
+
completer,
|
|
85
125
|
});
|
|
86
126
|
this.permissions = new PermissionManager();
|
|
87
127
|
this.permissions.setReadlineInterface(this.rl);
|
|
@@ -90,28 +130,11 @@ export class GrokChat {
|
|
|
90
130
|
}
|
|
91
131
|
|
|
92
132
|
async start(): Promise<void> {
|
|
93
|
-
//
|
|
94
|
-
console.log(chalk.cyan(`
|
|
95
|
-
██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
96
|
-
██╔════╝ ██╔══██╗██╔═══██╗██║ ██╔╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
97
|
-
██║ ███╗██████╔╝██║ ██║█████╔╝ ██║ ██║ ██║██║ ██║█████╗
|
|
98
|
-
██║ ██║██╔══██╗██║ ██║██╔═██╗ ██║ ██║ ██║██║ ██║██╔══╝
|
|
99
|
-
╚██████╔╝██║ ██║╚██████╔╝██║ ██╗ ╚██████╗╚██████╔╝██████╔╝███████╗
|
|
100
|
-
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝`));
|
|
101
|
-
|
|
102
|
-
console.log();
|
|
103
|
-
console.log(drawBox([
|
|
104
|
-
`${chalk.bold('Grok Code CLI')} ${chalk.gray(`v${VERSION}`)}`,
|
|
105
|
-
'',
|
|
106
|
-
`${chalk.gray('Model:')} ${chalk.green(this.client.model)}`,
|
|
107
|
-
`${chalk.gray('CWD:')} ${chalk.blue(process.cwd())}`,
|
|
108
|
-
`${chalk.gray('Tools:')} ${chalk.cyan('8 available')} ${chalk.gray('(Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch)')}`,
|
|
109
|
-
'',
|
|
110
|
-
`${chalk.gray('Commands:')} ${chalk.cyan('/help')} ${chalk.gray('•')} ${chalk.cyan('/model')} ${chalk.gray('•')} ${chalk.cyan('/doctor')} ${chalk.gray('•')} ${chalk.yellow('exit')}`,
|
|
111
|
-
], { borderColor: chalk.cyan, padding: 0 }));
|
|
112
|
-
|
|
133
|
+
// Clean welcome like Claude Code
|
|
113
134
|
console.log();
|
|
114
|
-
console.log(
|
|
135
|
+
console.log(chalk.bold.cyan(' Grok Code') + chalk.dim(` v${VERSION}`));
|
|
136
|
+
console.log(chalk.dim(` ${this.client.model} • ${process.cwd()}`));
|
|
137
|
+
console.log(chalk.dim(' Type /help for commands, Tab for autocomplete'));
|
|
115
138
|
console.log();
|
|
116
139
|
|
|
117
140
|
// Create new session
|
|
@@ -465,8 +488,8 @@ export class GrokChat {
|
|
|
465
488
|
}
|
|
466
489
|
|
|
467
490
|
private async handleModel(modelName?: string): Promise<void> {
|
|
468
|
-
// Fetch latest models from xAI API
|
|
469
|
-
|
|
491
|
+
// Fetch latest models from xAI API
|
|
492
|
+
process.stdout.write(chalk.dim(' Fetching models...'));
|
|
470
493
|
|
|
471
494
|
let availableModels: string[] = [];
|
|
472
495
|
try {
|
|
@@ -478,175 +501,87 @@ export class GrokChat {
|
|
|
478
501
|
const data = await response.json() as { data: { id: string }[] };
|
|
479
502
|
availableModels = data.data.map(m => m.id).sort();
|
|
480
503
|
} else {
|
|
481
|
-
// Fallback to known models if API fails
|
|
482
504
|
availableModels = [
|
|
483
|
-
'grok-4-0709', 'grok-4-fast-reasoning', 'grok-4-fast-non-reasoning',
|
|
484
505
|
'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning',
|
|
485
|
-
'grok-
|
|
486
|
-
'grok-
|
|
506
|
+
'grok-4-0709', 'grok-4-fast-reasoning', 'grok-4-fast-non-reasoning',
|
|
507
|
+
'grok-3', 'grok-3-mini',
|
|
487
508
|
];
|
|
488
509
|
}
|
|
489
510
|
} catch {
|
|
490
|
-
// Fallback to known models
|
|
491
511
|
availableModels = [
|
|
492
|
-
'grok-4-
|
|
493
|
-
'grok-
|
|
512
|
+
'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning',
|
|
513
|
+
'grok-4-0709', 'grok-3', 'grok-3-mini',
|
|
494
514
|
];
|
|
495
515
|
}
|
|
496
516
|
|
|
497
|
-
|
|
498
|
-
console.log();
|
|
499
|
-
console.log(chalk.cyan('╭──────────────────────────────────────────────────────────────────────╮'));
|
|
500
|
-
console.log(chalk.cyan('│') + chalk.bold.cyan(' 🤖 Model Selection ') + chalk.cyan('│'));
|
|
501
|
-
console.log(chalk.cyan('╰──────────────────────────────────────────────────────────────────────╯'));
|
|
502
|
-
console.log();
|
|
503
|
-
console.log(` ${chalk.gray('Current Model:')} ${chalk.bold.green(this.client.model)}`);
|
|
504
|
-
console.log();
|
|
517
|
+
process.stdout.write('\r\x1B[K'); // Clear the "Fetching" line
|
|
505
518
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
for (const model of availableModels) {
|
|
517
|
-
if (model.startsWith('grok-4-1')) {
|
|
518
|
-
if (model.includes('non-reasoning')) {
|
|
519
|
-
grok41NonReasoning.push(model);
|
|
520
|
-
} else if (model.includes('reasoning')) {
|
|
521
|
-
grok41Reasoning.push(model);
|
|
522
|
-
}
|
|
523
|
-
} else if (model.startsWith('grok-4')) {
|
|
524
|
-
if (model.includes('non-reasoning')) {
|
|
525
|
-
grok4NonReasoning.push(model);
|
|
526
|
-
} else if (model.includes('reasoning')) {
|
|
527
|
-
grok4Reasoning.push(model);
|
|
528
|
-
} else {
|
|
529
|
-
grok4Other.push(model);
|
|
530
|
-
}
|
|
531
|
-
} else if (model.startsWith('grok-3')) {
|
|
532
|
-
grok3.push(model);
|
|
533
|
-
} else if (model.startsWith('grok-2')) {
|
|
534
|
-
grok2.push(model);
|
|
535
|
-
} else if (model.includes('code') || model.includes('vision') || model.includes('image')) {
|
|
536
|
-
specialized.push(model);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
519
|
+
// If model name provided directly, switch to it
|
|
520
|
+
if (modelName) {
|
|
521
|
+
let matchedModel = modelName;
|
|
522
|
+
if (!availableModels.includes(modelName)) {
|
|
523
|
+
// Normalize: "grok41" → "grok-4-1", "4.1" → "4-1"
|
|
524
|
+
const normalized = modelName.toLowerCase()
|
|
525
|
+
.replace(/grok\s*(\d)(\d)?/g, (_, d1, d2) => d2 ? `grok-${d1}-${d2}` : `grok-${d1}`)
|
|
526
|
+
.replace(/(\d+)\.(\d+)/g, '$1-$2')
|
|
527
|
+
.replace(/\s+/g, '-');
|
|
539
528
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.log(chalk.bold.magenta(' ⭐ Grok 4.1 (Latest)'));
|
|
543
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────────────────────────'));
|
|
544
|
-
for (const model of grok41Reasoning) {
|
|
545
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
546
|
-
console.log(` ${chalk.green('🧠')} ${chalk.green(model)}${current} ${chalk.yellow('[REASONING]')}`);
|
|
547
|
-
}
|
|
548
|
-
for (const model of grok41NonReasoning) {
|
|
549
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
550
|
-
console.log(` ${chalk.cyan('⚡')} ${model}${current} ${chalk.gray('[FAST]')}`);
|
|
551
|
-
}
|
|
552
|
-
console.log();
|
|
553
|
-
}
|
|
529
|
+
const partialMatch = availableModels.find(m => m.toLowerCase().includes(normalized)) ||
|
|
530
|
+
availableModels.find(m => m.toLowerCase().includes(modelName.toLowerCase()));
|
|
554
531
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
561
|
-
console.log(` ${chalk.cyan('•')} ${model}${current} ${chalk.gray('(recommended)')}`);
|
|
562
|
-
}
|
|
563
|
-
for (const model of grok4Reasoning) {
|
|
564
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
565
|
-
console.log(` ${chalk.green('🧠')} ${chalk.green(model)}${current} ${chalk.yellow('[REASONING]')}`);
|
|
566
|
-
}
|
|
567
|
-
for (const model of grok4NonReasoning) {
|
|
568
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
569
|
-
console.log(` ${chalk.cyan('⚡')} ${model}${current} ${chalk.gray('[FAST]')}`);
|
|
532
|
+
if (partialMatch) {
|
|
533
|
+
matchedModel = partialMatch;
|
|
534
|
+
} else {
|
|
535
|
+
console.log(chalk.red(` Unknown model: ${modelName}`));
|
|
536
|
+
return;
|
|
570
537
|
}
|
|
571
|
-
console.log();
|
|
572
538
|
}
|
|
573
539
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
for (const model of grok3) {
|
|
579
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
580
|
-
console.log(` ${chalk.cyan('•')} ${model}${current}`);
|
|
581
|
-
}
|
|
582
|
-
console.log();
|
|
583
|
-
}
|
|
540
|
+
this.client = new GrokClient(this.apiKey, matchedModel);
|
|
541
|
+
console.log(chalk.green(` ✓ Switched to ${matchedModel}`));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
584
544
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
console.log(chalk.bold.gray(' 📷 Grok 2 (Vision/Image)'));
|
|
588
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────────────────────────'));
|
|
589
|
-
for (const model of grok2) {
|
|
590
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
591
|
-
console.log(` ${chalk.cyan('•')} ${model}${current}`);
|
|
592
|
-
}
|
|
593
|
-
console.log();
|
|
594
|
-
}
|
|
545
|
+
// Build options for interactive selector - prioritize Grok 4.1
|
|
546
|
+
const options: SelectorOption[] = [];
|
|
595
547
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const current = model === this.client.model ? chalk.green(' ← current') : '';
|
|
602
|
-
console.log(` ${chalk.cyan('•')} ${model}${current}`);
|
|
603
|
-
}
|
|
604
|
-
console.log();
|
|
605
|
-
}
|
|
548
|
+
// Categorize models
|
|
549
|
+
const grok41 = availableModels.filter(m => m.startsWith('grok-4-1'));
|
|
550
|
+
const grok4 = availableModels.filter(m => m.startsWith('grok-4') && !m.startsWith('grok-4-1'));
|
|
551
|
+
const grok3 = availableModels.filter(m => m.startsWith('grok-3'));
|
|
552
|
+
const others = availableModels.filter(m => !m.startsWith('grok-4') && !m.startsWith('grok-3'));
|
|
606
553
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
554
|
+
// Add Grok 4.1 first (latest)
|
|
555
|
+
for (const model of grok41) {
|
|
556
|
+
const desc = model.includes('non-reasoning') ? 'fast' : model.includes('reasoning') ? 'reasoning' : '';
|
|
557
|
+
options.push({ label: model, value: model, description: desc });
|
|
611
558
|
}
|
|
612
559
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// "4.1" → "4-1", "grok 4 1" → "grok-4-1"
|
|
619
|
-
let normalized = modelName.toLowerCase()
|
|
620
|
-
.replace(/grok\s*(\d)(\d)?(\d)?/g, (_, d1, d2, d3) => {
|
|
621
|
-
if (d3) return `grok-${d1}-${d2}-${d3}`;
|
|
622
|
-
if (d2) return `grok-${d1}-${d2}`;
|
|
623
|
-
return `grok-${d1}`;
|
|
624
|
-
})
|
|
625
|
-
.replace(/(\d+)\.(\d+)/g, '$1-$2') // "4.1" → "4-1"
|
|
626
|
-
.replace(/\s+/g, '-'); // spaces to hyphens
|
|
627
|
-
|
|
628
|
-
// Try to find a match
|
|
629
|
-
let partialMatch = availableModels.find(m => m.toLowerCase().includes(normalized));
|
|
630
|
-
|
|
631
|
-
// If no match, try the original input
|
|
632
|
-
if (!partialMatch) {
|
|
633
|
-
partialMatch = availableModels.find(m =>
|
|
634
|
-
m.toLowerCase().includes(modelName.toLowerCase())
|
|
635
|
-
);
|
|
636
|
-
}
|
|
560
|
+
// Add Grok 4
|
|
561
|
+
for (const model of grok4) {
|
|
562
|
+
const desc = model.includes('non-reasoning') ? 'fast' : model.includes('reasoning') ? 'reasoning' : '';
|
|
563
|
+
options.push({ label: model, value: model, description: desc });
|
|
564
|
+
}
|
|
637
565
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
} else {
|
|
642
|
-
console.log(chalk.red(`Unknown model: ${modelName}\n`));
|
|
643
|
-
console.log(chalk.gray('Use /model to see available models.\n'));
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
566
|
+
// Add Grok 3
|
|
567
|
+
for (const model of grok3) {
|
|
568
|
+
options.push({ label: model, value: model });
|
|
646
569
|
}
|
|
647
570
|
|
|
648
|
-
|
|
649
|
-
|
|
571
|
+
// Add others
|
|
572
|
+
for (const model of others) {
|
|
573
|
+
options.push({ label: model, value: model });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
console.log();
|
|
577
|
+
const selected = await interactiveSelect('Select model:', options, this.client.model);
|
|
578
|
+
|
|
579
|
+
if (selected && selected !== this.client.model) {
|
|
580
|
+
this.client = new GrokClient(this.apiKey, selected);
|
|
581
|
+
console.log(chalk.green(` ✓ Switched to ${selected}`));
|
|
582
|
+
} else if (!selected) {
|
|
583
|
+
console.log(chalk.dim(' Cancelled'));
|
|
584
|
+
}
|
|
650
585
|
}
|
|
651
586
|
|
|
652
587
|
private handlePermissions(): void {
|
|
@@ -1101,9 +1036,8 @@ Start by checking git status and recent changes, then provide specific, actionab
|
|
|
1101
1036
|
}
|
|
1102
1037
|
|
|
1103
1038
|
private async getStreamingResponse(): Promise<void> {
|
|
1104
|
-
//
|
|
1105
|
-
process.stdout.write(
|
|
1106
|
-
process.stdout.write(chalk.cyan('│ ') + chalk.gray('⠋ thinking...'));
|
|
1039
|
+
// Simple thinking indicator like Claude Code
|
|
1040
|
+
process.stdout.write('\n' + chalk.dim(' Thinking...'));
|
|
1107
1041
|
|
|
1108
1042
|
let fullContent = '';
|
|
1109
1043
|
let toolCalls: ToolCall[] = [];
|
|
@@ -1116,8 +1050,8 @@ Start by checking git status and recent changes, then provide specific, actionab
|
|
|
1116
1050
|
|
|
1117
1051
|
if (delta?.content) {
|
|
1118
1052
|
if (firstChunk) {
|
|
1119
|
-
// Clear thinking indicator
|
|
1120
|
-
process.stdout.write('\r' +
|
|
1053
|
+
// Clear thinking indicator
|
|
1054
|
+
process.stdout.write('\r' + ' '.repeat(20) + '\r\n');
|
|
1121
1055
|
firstChunk = false;
|
|
1122
1056
|
}
|
|
1123
1057
|
process.stdout.write(delta.content);
|
|
@@ -1153,13 +1087,11 @@ Start by checking git status and recent changes, then provide specific, actionab
|
|
|
1153
1087
|
toolCalls.push(currentToolCall as ToolCall);
|
|
1154
1088
|
}
|
|
1155
1089
|
|
|
1156
|
-
//
|
|
1090
|
+
// End response
|
|
1157
1091
|
if (fullContent) {
|
|
1158
|
-
console.log();
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
process.stdout.write('\r' + chalk.cyan('│ ') + chalk.gray('Using tools...') + ' '.repeat(40) + '\n');
|
|
1162
|
-
console.log(chalk.cyan('╰──────────────────────────────────────────────────────────────────────╯'));
|
|
1092
|
+
console.log('\n');
|
|
1093
|
+
} else if (toolCalls.length > 0 && firstChunk) {
|
|
1094
|
+
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
|
1163
1095
|
}
|
|
1164
1096
|
|
|
1165
1097
|
// Build the message for history
|
|
@@ -1223,59 +1155,35 @@ Start by checking git status and recent changes, then provide specific, actionab
|
|
|
1223
1155
|
return;
|
|
1224
1156
|
}
|
|
1225
1157
|
|
|
1226
|
-
//
|
|
1227
|
-
|
|
1228
|
-
Read: '📖', Write: '✏️', Edit: '🔧', Bash: '⚡',
|
|
1229
|
-
Glob: '🔍', Grep: '🔎', WebFetch: '🌐', WebSearch: '🔍'
|
|
1230
|
-
};
|
|
1231
|
-
const toolColors: Record<string, typeof chalk> = {
|
|
1232
|
-
Read: chalk.green, Write: chalk.yellow, Edit: chalk.yellow, Bash: chalk.red,
|
|
1233
|
-
Glob: chalk.green, Grep: chalk.green, WebFetch: chalk.green, WebSearch: chalk.green
|
|
1234
|
-
};
|
|
1235
|
-
|
|
1236
|
-
const icon = toolIcons[name] || '🔧';
|
|
1237
|
-
const color = toolColors[name] || chalk.gray;
|
|
1238
|
-
|
|
1239
|
-
console.log();
|
|
1240
|
-
console.log(color('┌─ ') + chalk.bold(`${icon} ${name}`) + color(' ─────────────────────────────────────────────────'));
|
|
1241
|
-
|
|
1242
|
-
// Show details based on tool type
|
|
1158
|
+
// Simple tool display like Claude Code
|
|
1159
|
+
let toolInfo = '';
|
|
1243
1160
|
if (name === 'Bash') {
|
|
1244
|
-
|
|
1245
|
-
} else if (name === 'Read'
|
|
1246
|
-
|
|
1247
|
-
} else if (name === '
|
|
1248
|
-
|
|
1161
|
+
toolInfo = chalk.dim('$ ') + (params.command as string).slice(0, 60);
|
|
1162
|
+
} else if (name === 'Read') {
|
|
1163
|
+
toolInfo = params.file_path as string;
|
|
1164
|
+
} else if (name === 'Write') {
|
|
1165
|
+
toolInfo = params.file_path as string;
|
|
1166
|
+
} else if (name === 'Edit') {
|
|
1167
|
+
toolInfo = params.file_path as string;
|
|
1168
|
+
} else if (name === 'Glob') {
|
|
1169
|
+
toolInfo = params.pattern as string;
|
|
1170
|
+
} else if (name === 'Grep') {
|
|
1171
|
+
toolInfo = params.pattern as string;
|
|
1249
1172
|
} else if (name === 'WebFetch') {
|
|
1250
|
-
|
|
1173
|
+
toolInfo = (params.url as string).slice(0, 50);
|
|
1251
1174
|
} else if (name === 'WebSearch') {
|
|
1252
|
-
|
|
1175
|
+
toolInfo = params.query as string;
|
|
1253
1176
|
}
|
|
1254
1177
|
|
|
1178
|
+
console.log(chalk.dim(' ● ') + chalk.cyan(name) + chalk.dim(' ' + toolInfo));
|
|
1179
|
+
|
|
1255
1180
|
// Execute
|
|
1256
1181
|
const result = await executeTool(name, params);
|
|
1257
1182
|
|
|
1258
|
-
if (result.success) {
|
|
1259
|
-
console.log(
|
|
1260
|
-
console.log(color('│ ') + chalk.green('✓ Success'));
|
|
1261
|
-
if (result.output && result.output.length < 500) {
|
|
1262
|
-
const lines = result.output.split('\n').slice(0, 10);
|
|
1263
|
-
for (const line of lines) {
|
|
1264
|
-
console.log(color('│ ') + chalk.gray(line.slice(0, 80)));
|
|
1265
|
-
}
|
|
1266
|
-
if (result.output.split('\n').length > 10) {
|
|
1267
|
-
console.log(color('│ ') + chalk.gray('... (truncated)'));
|
|
1268
|
-
}
|
|
1269
|
-
} else if (result.output) {
|
|
1270
|
-
console.log(color('│ ') + chalk.gray(result.output.slice(0, 200) + '... (truncated)'));
|
|
1271
|
-
}
|
|
1272
|
-
} else {
|
|
1273
|
-
console.log(color('│'));
|
|
1274
|
-
console.log(color('│ ') + chalk.red('✗ Failed: ') + chalk.red(result.error || 'Unknown error'));
|
|
1183
|
+
if (!result.success) {
|
|
1184
|
+
console.log(chalk.red(' ✗ ') + chalk.red(result.error || 'Failed'));
|
|
1275
1185
|
}
|
|
1276
1186
|
|
|
1277
|
-
console.log(color('└──────────────────────────────────────────────────────────────────────'));
|
|
1278
|
-
|
|
1279
1187
|
this.messages.push({
|
|
1280
1188
|
role: 'tool',
|
|
1281
1189
|
tool_call_id: toolCall.id,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
|
|
4
|
+
export interface SelectorOption {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function interactiveSelect(
|
|
11
|
+
title: string,
|
|
12
|
+
options: SelectorOption[],
|
|
13
|
+
currentValue?: string
|
|
14
|
+
): Promise<string | null> {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let selectedIndex = 0;
|
|
17
|
+
|
|
18
|
+
// Find current value index
|
|
19
|
+
if (currentValue) {
|
|
20
|
+
const idx = options.findIndex(o => o.value === currentValue);
|
|
21
|
+
if (idx >= 0) selectedIndex = idx;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const render = () => {
|
|
25
|
+
// Clear previous render
|
|
26
|
+
process.stdout.write('\x1B[?25l'); // Hide cursor
|
|
27
|
+
|
|
28
|
+
// Move up and clear lines if not first render
|
|
29
|
+
const totalLines = options.length + 2;
|
|
30
|
+
process.stdout.write(`\x1B[${totalLines}A`);
|
|
31
|
+
|
|
32
|
+
// Title
|
|
33
|
+
console.log(chalk.bold(title));
|
|
34
|
+
console.log(chalk.dim('↑↓/Tab to navigate, Enter to select, Esc to cancel'));
|
|
35
|
+
|
|
36
|
+
// Options
|
|
37
|
+
for (let i = 0; i < options.length; i++) {
|
|
38
|
+
const opt = options[i];
|
|
39
|
+
const isSelected = i === selectedIndex;
|
|
40
|
+
const isCurrent = opt.value === currentValue;
|
|
41
|
+
|
|
42
|
+
const pointer = isSelected ? chalk.cyan('❯') : ' ';
|
|
43
|
+
const label = isSelected ? chalk.cyan.bold(opt.label) : opt.label;
|
|
44
|
+
const current = isCurrent ? chalk.green(' (current)') : '';
|
|
45
|
+
const desc = opt.description ? chalk.dim(` - ${opt.description}`) : '';
|
|
46
|
+
|
|
47
|
+
console.log(`${pointer} ${label}${current}${desc}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Initial render with padding
|
|
52
|
+
console.log(chalk.bold(title));
|
|
53
|
+
console.log(chalk.dim('↑↓/Tab to navigate, Enter to select, Esc to cancel'));
|
|
54
|
+
for (const opt of options) {
|
|
55
|
+
const isCurrent = opt.value === currentValue;
|
|
56
|
+
const current = isCurrent ? chalk.green(' (current)') : '';
|
|
57
|
+
const desc = opt.description ? chalk.dim(` - ${opt.description}`) : '';
|
|
58
|
+
console.log(` ${opt.label}${current}${desc}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Now render with selection
|
|
62
|
+
render();
|
|
63
|
+
|
|
64
|
+
// Set up raw mode for key input
|
|
65
|
+
if (process.stdin.isTTY) {
|
|
66
|
+
process.stdin.setRawMode(true);
|
|
67
|
+
}
|
|
68
|
+
process.stdin.resume();
|
|
69
|
+
|
|
70
|
+
const onKeypress = (key: Buffer) => {
|
|
71
|
+
const char = key.toString();
|
|
72
|
+
|
|
73
|
+
// Arrow up or k
|
|
74
|
+
if (char === '\x1B[A' || char === 'k') {
|
|
75
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
|
|
76
|
+
render();
|
|
77
|
+
}
|
|
78
|
+
// Arrow down or j
|
|
79
|
+
else if (char === '\x1B[B' || char === 'j') {
|
|
80
|
+
selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
|
|
81
|
+
render();
|
|
82
|
+
}
|
|
83
|
+
// Tab - cycle forward
|
|
84
|
+
else if (char === '\t') {
|
|
85
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
86
|
+
render();
|
|
87
|
+
}
|
|
88
|
+
// Shift+Tab - cycle backward (usually \x1B[Z)
|
|
89
|
+
else if (char === '\x1B[Z') {
|
|
90
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
|
|
91
|
+
render();
|
|
92
|
+
}
|
|
93
|
+
// Enter
|
|
94
|
+
else if (char === '\r' || char === '\n') {
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve(options[selectedIndex].value);
|
|
97
|
+
}
|
|
98
|
+
// Escape or q
|
|
99
|
+
else if (char === '\x1B' || char === 'q') {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
103
|
+
// Ctrl+C
|
|
104
|
+
else if (char === '\x03') {
|
|
105
|
+
cleanup();
|
|
106
|
+
resolve(null);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
process.stdin.removeListener('data', onKeypress);
|
|
112
|
+
if (process.stdin.isTTY) {
|
|
113
|
+
process.stdin.setRawMode(false);
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
116
|
+
console.log(); // New line after selection
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
process.stdin.on('data', onKeypress);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Simple yes/no confirmation
|
|
124
|
+
export async function confirm(message: string, defaultYes = true): Promise<boolean> {
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
127
|
+
process.stdout.write(`${message} ${chalk.dim(hint)} `);
|
|
128
|
+
|
|
129
|
+
if (process.stdin.isTTY) {
|
|
130
|
+
process.stdin.setRawMode(true);
|
|
131
|
+
}
|
|
132
|
+
process.stdin.resume();
|
|
133
|
+
|
|
134
|
+
const onKeypress = (key: Buffer) => {
|
|
135
|
+
const char = key.toString().toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (char === 'y') {
|
|
138
|
+
cleanup();
|
|
139
|
+
console.log(chalk.green('Yes'));
|
|
140
|
+
resolve(true);
|
|
141
|
+
} else if (char === 'n') {
|
|
142
|
+
cleanup();
|
|
143
|
+
console.log(chalk.red('No'));
|
|
144
|
+
resolve(false);
|
|
145
|
+
} else if (char === '\r' || char === '\n') {
|
|
146
|
+
cleanup();
|
|
147
|
+
console.log(defaultYes ? chalk.green('Yes') : chalk.red('No'));
|
|
148
|
+
resolve(defaultYes);
|
|
149
|
+
} else if (char === '\x03' || char === '\x1B') {
|
|
150
|
+
cleanup();
|
|
151
|
+
console.log(chalk.red('Cancelled'));
|
|
152
|
+
resolve(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
process.stdin.removeListener('data', onKeypress);
|
|
158
|
+
if (process.stdin.isTTY) {
|
|
159
|
+
process.stdin.setRawMode(false);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.stdin.on('data', onKeypress);
|
|
164
|
+
});
|
|
165
|
+
}
|