help_ai
Version:
_help_ai_ is an npm CLI tool designed to assist non-CLI developers and DevOps engineers in configuring and completing terminal tasks with a single prompt.
363 lines (328 loc) • 11.2 kB
JavaScript
import chalk from "chalk";
import { Command } from "commander";
import figlet from "figlet";
import inquirer from "inquirer";
import fs from "fs";
import path from "path";
import os from "os";
import axios from "axios";
import ora from "ora"; // Import ora for loading spinner
import executeChainOfCommands from "./executeCommands/index.js";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const program = new Command();
// Configuration file path
const configPath = path.join(os.homedir(), ".ai-cli-config.json");
process.on("SIGINT", () => {
console.log(chalk.yellow("\nExiting gracefully..."));
process.exit(0); // Clean exit
});
process.on("unhandledRejection", (reason, promise) => {
if (reason.name === "ExitPromptError") {
console.log(chalk.yellow("\nForce quite! bye bye."));
process.exit(0); // Clean exit
} else {
console.error(chalk.red("Unhandled rejection:"), reason);
}
});
// Check if configuration file exists, create it if not
if (!fs.existsSync(configPath)) {
const defaultConfig = {
apiKey: "",
instructions:
"you must give me a series of CLI commands in bash, nothing more than. I am strictly preventing you from saying 'ok'. use echo instead of vim or nano just in case of updating a file",
tries: 5,
model: "gemini-1.5-flash",
};
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
}
// Function to load the configuration
const loadConfig = () => {
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
};
// Command to show the header
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.json"), "utf-8")
);
program
.version(packageJson.version, "-v, --version", "output the current version")
.description(
"AI-powered CLI to execute tasks based on natural language input"
);
// Command to show the header
const showHeader = () => {
console.log(
chalk.blue(
figlet.textSync("AI CLI", {
horizontalLayout: "default",
verticalLayout: "default",
})
)
);
console.log(chalk.green(`Version: ${packageJson.version}`)); // Display version in the header
};
showHeader();
// Command to view or edit the configuration
program
.command("config")
.description("View or update the API key, instructions, and model")
.action(async () => {
const config = loadConfig();
// Show current configuration
console.log(chalk.green("Current Configuration:"));
console.log(chalk.cyan(`API Key: ${config.apiKey}`));
console.log(chalk.cyan(`Instructions: ${config.instructions}`));
console.log(chalk.cyan(`Model: ${config.model}`));
console.log(chalk.cyan(`tries: ${config?.tries}`));
// Ask if the user wants to update the configuration
const { updateConfig } = await inquirer.prompt([
{
type: "confirm",
name: "updateConfig",
message:
"Would you like to update the API Key, Instructions, or Model?",
default: false,
},
]);
if (updateConfig) {
const { newApiKey, newInstructions, newModel, newTries } =
await inquirer.prompt([
{
type: "input",
name: "newApiKey",
message: "Enter the new API Key:",
default: config.apiKey,
},
{
type: "input",
name: "newInstructions",
message: "Enter the new Instructions:",
default: config.instructions,
},
{
type: "input",
name: "newTries",
message: "Enter how many tries should be held after failure:",
default: config?.tries,
validate: (input) => {
// Ensure the input is a valid number
const num = Number(input);
if (isNaN(num) || num <= 0) {
return "Please enter a valid number greater than 0 for tries.";
}
return true; // Validation passed
},
},
{
type: "list",
name: "newModel",
message: "Choose the AI model:",
choices: ["gemini-1.5-flash", "gpt-3.5-turbo", "custom-model"], // Add more models if needed
default: config.model,
},
]);
const tries = Number(newTries);
// Update the configuration file
const updatedConfig = {
apiKey: newApiKey,
instructions: newInstructions,
model: newModel,
tries: tries,
};
fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2));
console.log(chalk.green("Configuration updated successfully!"));
}
});
// Command to start a conversation
program
.command("chat")
.description("Start a conversation with the AI")
.action(async () => {
let isChatting = true;
const conversationHistoryPath = path.join(
os.homedir(),
".ai-cli-history.json"
);
// Initialize the conversation history file if it doesn't exist
if (!fs.existsSync(conversationHistoryPath)) {
fs.writeFileSync(conversationHistoryPath, JSON.stringify([], null, 2));
}
const conversationHistory = JSON.parse(
fs.readFileSync(conversationHistoryPath, "utf-8")
);
console.log(
chalk.green(
"Type your queries below. Type 'exit' to end the conversation."
)
);
while (isChatting) {
const { userInput } = await inquirer.prompt([
{
type: "input",
name: "userInput",
message: chalk.yellow("You: "),
},
]);
if (userInput.toLowerCase() === "exit") {
isChatting = false;
console.log(chalk.blue("Goodbye!"));
break;
}
// Load the configuration (API Key, Instructions, and Model)
const config = loadConfig();
if (!config.apiKey) {
console.error(
chalk.red(
`Please provide api key first for ${config.model}, ${
config.model === "gemini-1.5-flash" &&
"get your api key from https://ai.google.dev/gemini-api/docs/api-key then run 'acli config'"
}. Other models are also available!`
)
);
return;
}
if (!config.model) {
console.error(chalk.red(`Please select a model first`));
return;
}
const spinner = ora("Fetching AI response...").start();
// Call the AI function to generate a response based on the selected model
const aiResponse = await getAIResponseFromModel(
userInput,
conversationHistory,
config
);
spinner.stop();
spinner.clear();
// Keep trying until the task is successful or retry limit is reached
if (aiResponse?.message) {
let isExecuted = { success: false, error: null }; // Initial state
let retryAttempts = 0; // Optional: To limit the number of retries
let aiResponseMessage = aiResponse; // Use `let` to allow reassignment
let historyError = "";
// Keep trying until the task is successful or retry limit is reached
while (!isExecuted.success && retryAttempts < parseInt(config.tries)) {
try {
// Execute the commands
isExecuted = await executeChainOfCommands(
aiResponseMessage?.message
);
if (isExecuted.error) {
console.error(
chalk.red(`Error executing commands: ${isExecuted.error}`)
);
historyError += isExecuted.error;
// If there's an error, retry after getting an AI response
aiResponseMessage = await getAIResponseFromModel(
`Error: ${historyError}. user input:${userInput}`,
conversationHistory,
config
);
}
if (isExecuted.success) {
console.log(chalk.cyan(`Your task is successfully done!`));
return isExecuted; // Exit the loop if successful
}
retryAttempts++;
console.log(chalk.yellow(`Retrying attempt ${retryAttempts}...`));
} catch (err) {
console.error(chalk.red(`Unexpected error: ${err.message}`));
break; // If an unexpected error occurs, break out of the loop
}
}
// If the retry limit is reached
if (!isExecuted.success) {
console.log(
chalk.red(`Failed to execute task after ${retryAttempts} attempts.`)
);
}
}
// If the retry limit is reached
if (!isExecuted.success) {
console.log(
chalk.red(`Failed to execute task after ${retryAttempts} attempts.`)
);
}
}
if (aiResponse.error) {
console.error(chalk.red(`Error: ${aiResponse.error}`));
} else {
console.log(chalk.cyan(`AI: ${aiResponse?.message}`));
}
// Append to conversation history
conversationHistory.push({ user: userInput, ai: aiResponse });
fs.writeFileSync(
conversationHistoryPath,
JSON.stringify(conversationHistory, null, 2)
);
});
// Function to get AI response from the selected model
const getAIResponseFromModel = async (
userInput,
conversationHistory,
config
) => {
console.log(userInput);
try {
const { apiKey, instructions, model } = config;
// Select the correct model URL
let apiUrl = "";
if (model === "gemini-1.5-flash") {
apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
} else if (model === "gpt-3.5-turbo") {
apiUrl = `https://api.openai.com/v1/completions`;
} else {
// Custom model handling can be added here
apiUrl = `https://custom-api.com/${model}`;
}
const response = await axios.post(
apiUrl,
{
contents: [
{
parts: [
{
text: `Instructions: ${instructions}, user Input: ${userInput}`,
},
],
},
],
},
{
headers: {
"Content-Type": "application/json",
...(model === "gpt-3.5-turbo" && {
Authorization: `Bearer ${apiKey}`,
}),
},
}
);
// for gemini
if (response?.data?.candidates[0]?.content?.parts[0]?.text) {
console.log(response?.data?.candidates[0]?.content?.parts[0]?.text);
return {
message: response?.data?.candidates[0]?.content?.parts[0]?.text,
};
}
// for gpt-3.5-turbo
if (response?.data?.choices[0]?.text) {
return {
message: response?.data?.choices[0]?.text.trim(),
};
} else if (response?.data?.candidates[0]?.content?.parts[0]?.text) {
return {
message: response?.data?.candidates[0]?.content?.parts[0]?.text,
};
} else {
return { message: response.data };
}
} catch (error) {
if (error?.response?.data?.error?.message) {
return { error: error?.response?.data?.error?.message };
}
return { error: error || "Unexpected error" };
}
};
program.parse(process.argv);