genkit-shell
Version:
AI CLI interpreter using Genkit and Gemini
281 lines (245 loc) • 9.98 kB
JavaScript
#!/usr/bin/env node
// node-pty solution preserving real bash session
// with AI for !, ? commands, etc.
import { gemini15Flash, gemini15Pro, googleAI, gemini20Flash } from '@genkit-ai/googleai';
import { genkit } from 'genkit';
import dotenv from 'dotenv';
import readline from 'readline';
import os from 'os';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import path from 'path';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import pty from 'node-pty';
/////////////////////////////
// FINAL node-pty SCRIPT
// Real bash session with AI
/////////////////////////////
//////////////////////////////////
// CONFIG & AUTO-START
//////////////////////////////////
let LANGUAGE = 'EN';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ path: path.join(__dirname, '..', '.env') });
const bashrcPath = path.join(os.homedir(), '.bashrc');
const marker = '# >>> genkit-shell-autoload >>>';
if (existsSync(bashrcPath)) {
const bashrcContent = readFileSync(bashrcPath, 'utf-8');
if (!bashrcContent.includes(marker)) {
console.log(chalk.cyan('\n🛠 It is detected that the autostart is not configured.'));
const rlAuto = readline.createInterface({ input: process.stdin, output: process.stdout });
rlAuto.question('Add genkit-shell to autorun on terminal login? (Y/N): ', (answer) => {
if (answer.toLowerCase() === 'y') {
const block = `\n${marker}\nif [ -z \"$GENKIT_STARTED\" ]; then\n export GENKIT_STARTED=1\n genkit-shell\nfi\n# <<< genkit-shell-autoload <<<\n`;
writeFileSync(bashrcPath, bashrcContent + block);
console.log(chalk.green('✅ Autorun added to ~/.bashrc'));
} else {
console.log(chalk.yellow('⚠️ Autorun has not been added.'));
}
rlAuto.close();
});
}
}
//////////////////////////////////
// AI Config
//////////////////////////////////
const modelMap = {
'1': gemini15Flash,
'2': gemini15Pro,
'3': gemini20Flash,
};
let selectedModel = gemini15Flash;
let ai = genkit({
plugins: [googleAI({ apiKey: process.env.GOOGLE_API_KEY })],
model: selectedModel,
});
function setModel(model) {
selectedModel = model;
ai = genkit({
plugins: [googleAI({ apiKey: process.env.GOOGLE_API_KEY })],
model: selectedModel,
});
}
function getLanguagePrefix() {
switch (LANGUAGE) {
case 'RU': return 'Отвечай на русском. ';
case 'DE': return 'Antworte auf Deutsch. ';
case 'FR': return 'Réponds en français. ';
case 'ES': return 'Responde en español. ';
case 'UA': return 'Відповідай українською. ';
case 'ZH': return '请用中文回答。';
case 'JA': return '日本語で答えてください。';
default: return 'Respond in English. ';
}
}
let chatHistory = [];
const commandFlow = ai.defineFlow('commandFlow', async (instruction) => {
const prompt = `${getLanguagePrefix()}Convert a user instruction into a CLI command. No explanations, just the command.\nInstruction: "${instruction}"`;
const start = Date.now();
const { text } = await ai.generate(prompt);
return { command: text.trim(), duration: ((Date.now() - start) / 1000).toFixed(2) };
});
const suggestionFlow = ai.defineFlow('suggestionFlow', async (partial) => {
const prompt = `${getLanguagePrefix()}Explain briefly what the command \"${partial}\" does, then suggest 3 typical ways to use it.`;
const { text } = await ai.generate(prompt);
return text;
});
const chatFlow = ai.defineFlow('chatFlow', async (input) => {
chatHistory.push(`${LANGUAGE === 'RU' ? 'Пользователь' : 'User'}: ${input}`);
const prompt = getLanguagePrefix() + chatHistory.join('\n') + `\n${LANGUAGE === 'RU' ? 'ИИ' : 'AI'}:`;
const { text } = await ai.generate(prompt);
chatHistory.push(`${LANGUAGE === 'RU' ? 'ИИ' : 'AI'}: ${text}`);
if (chatHistory.length > 20) chatHistory.shift();
return text;
});
//////////////////////////////////
// NODE-PTY + BASH
//////////////////////////////////
// Start a real bash session
// NOTE: 'xterm-color' for color support
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
const ptyProcess = pty.spawn(shell, ['-i'], {
name: 'xterm-color',
cwd: process.cwd(),
env: process.env,
cols: 80,
rows: 24,
});
// We'll do our own input reading
process.stdin.setRawMode(true);
process.stdin.resume();
//////////////////////////////////
// READLINE for AI commands
//////////////////////////////////
readline.emitKeypressEvents(process.stdin);
// We'll store partial input in a buffer
// Because we want to parse lines, but also want to send them to pty if not recognized
let inputBuffer = '';
process.stdin.on('keypress', async (str, key) => {
// handle Ctrl+C
if (key.sequence === '\u0003') {
process.exit();
}
// if user pressed Enter
if (key.name === 'return') {
const line = inputBuffer.trim();
inputBuffer = '';
// Check if line is AI or special command
if (line === 'lang?') {
process.stdout.write('\n');
process.stdout.write(chalk.magentaBright('\n🌐 Languages: RU, EN, DE, FR, ES, UA, ZH, JA\n'));
const rlLang = readline.createInterface({ input: process.stdin, output: process.stdout });
rlLang.question('Select a language: ', (lang) => {
rlLang.close();
const allowed = ['RU', 'EN', 'DE', 'FR', 'ES', 'UA', 'ZH', 'JA'];
if (allowed.includes(lang.toUpperCase())) {
LANGUAGE = lang.toUpperCase();
process.stdout.write(chalk.green(`\n✅ Language switched to: ${LANGUAGE}\n`));
} else {
process.stdout.write(chalk.red('\n❌ Invalid language code.\n'));
}
});
}
else if (line === 'autostart-remove') {
process.stdout.write('\n');
if (existsSync(bashrcPath)) {
let content = readFileSync(bashrcPath, 'utf-8');
if (content.includes(marker)) {
const cleaned = content.replace(/\n?# >>> genkit-shell-autoload >>>[\s\S]*?# <<< genkit-shell-autoload <<</, '');
writeFileSync(bashrcPath, cleaned);
process.stdout.write(chalk.green('🧹 Autostart removed from ~/.bashrc\n'));
} else {
process.stdout.write(chalk.yellow('ℹ️ Autostart was not found in ~/.bashrc.\n'));
}
}
}
else if (line === '?') {
process.stdout.write(chalk.cyan('\n🤖 Chat mode activated. Type "exit" to leave.\n\n'));
const chatLoop = async () => {
const rlChat = readline.createInterface({ input: process.stdin, output: process.stdout });
rlChat.question(chalk.cyan('💬 You: '), async (msg) => {
rlChat.close();
if (msg.toLowerCase() === 'exit') {
process.stdout.write(chalk.green('🤖 Chat ended.\n'));
return;
}
const response = await chatFlow(msg);
process.stdout.write(chalk.blueBright('🤖 AI: ') + response + '\n');
chatLoop();
});
};
chatLoop();
}
else if (line === 'model?') {
process.stdout.write(chalk.magentaBright('\n🔍 Available models:\n'));
process.stdout.write('[1] gemini-1.5-flash\n');
process.stdout.write('[2] gemini-1.5-pro\n');
process.stdout.write('[3] gemini-2.0-flash\n');
const rlModel = readline.createInterface({ input: process.stdin, output: process.stdout });
rlModel.question('Select model [1-3]: ', (num) => {
rlModel.close();
if (modelMap[num]) {
setModel(modelMap[num]);
process.stdout.write(chalk.green('✅ Model switched to: ') + modelMap[num].name + '\n');
} else {
process.stdout.write(chalk.red('❌ Invalid selection.\n'));
}
});
}
else if (line.endsWith('?') && line.length > 1) {
const cmd = line.slice(0, -1);
try {
const response = await suggestionFlow(cmd);
process.stdout.write(chalk.yellow(`\n🤖 Info:`) + ' ' + cmd + '\n');
process.stdout.write(response + '\n');
} catch (err) {
process.stdout.write(chalk.red('⚠️ Could not get information: ') + err.message + '\n');
}
}
else if (line.startsWith('!')) {
// AI command => shell
const userRequest = line.slice(1);
process.stdout.write(chalk.yellow('🧠 Thinking...\r'));
try {
const { command, duration } = await commandFlow(userRequest);
process.stdout.write('\x1b[2J\x1b[0f'); // clear screen
process.stdout.write(chalk.magenta('📎 Command: ') + chalk.white.bold(command) + '\n');
process.stdout.write(chalk.blueBright('⚡ Response time: ') + chalk.yellow(`${duration} sec\n\n`));
const rlExec = readline.createInterface({ input: process.stdin, output: process.stdout });
rlExec.question(chalk.cyan.bold('Execute? (Y/N): '), (answer) => {
rlExec.close();
if (answer.toLowerCase() === 'y') {
ptyProcess.write(command + '\n');
} else {
process.stdout.write(chalk.red.bold('❌ Cancelled.\n'));
}
});
} catch (err) {
process.stdout.write(chalk.red.bold('❌ AI Error: ') + err.message + '\n');
}
}
else {
// normal command => pass to pty
ptyProcess.write(line + '\n');
}
} else if (key.name === 'backspace') {
if (inputBuffer.length > 0) {
inputBuffer = inputBuffer.slice(0, -1);
process.stdout.write('\b \b');
}
} else {
// add typed char to inputBuffer
if (str) {
inputBuffer += str;
process.stdout.write(str);
}
}
});
// on pty data, print to stdout
ptyProcess.onData((data) => {
process.stdout.write(data);
});
process.stdout.write(chalk.yellow.bold(`\n🤖 Genkit Shell + node-pty started! Lang=${LANGUAGE}\n`));
process.stdout.write(chalk.gray('Type !<cmd> for AI, ? for chat, model?, lang?, autostart-remove, etc.\n'));