diff --git a/.gitignore b/.gitignore index 1ba63d0..128cf0c 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,6 @@ dist *.txt *.jsonl .yarn -lib \ No newline at end of file +lib + +credentials.json diff --git a/__tests__/encrypt.spec.ts b/__tests__/encrypt.spec.ts index 39c1f4c..dbbe56a 100644 --- a/__tests__/encrypt.spec.ts +++ b/__tests__/encrypt.spec.ts @@ -1,70 +1,71 @@ -import * as crypto from "../src/encrypt"; - -import fs from "fs"; - +import { expect } from "chai"; +import * as fs from "fs"; import * as path from "path"; +import { + encrypt, + decrypt, + getCredentials, + saveCredentials, + deleteCredentials, +} from "../src/creds"; -import {expect} from "chai"; +describe("Encryption and Decryption", () => { + it("should return a string in the format 'IV:encrypted_text'", () => { + const result = encrypt("test"); + expect(result).to.be.a("string"); + expect(result).to.include(":"); + }); -describe("encrypt()", () => { - it("should return a string in the format 'IV:encrypted_text'", () => { - const result = crypto.encrypt("test"); - expect(result).to.match(/^[a-f0-9]{32}:([a-f0-9]{2})*$/); - }); + it("should return a different result each time it is called", () => { + const result1 = encrypt("test"); + const result2 = encrypt("test"); + expect(result1).to.not.equal(result2); + }); - it("should return a different result each time it is called", () => { - const result1 = crypto.encrypt("test"); - const result2 = crypto.encrypt("test"); - expect(result1).not.equal(result2); - }); -}); + it("should return the original input text when given a valid encrypted string", () => { + const encrypted = encrypt("test"); + const decrypted = decrypt(encrypted); + expect(decrypted).to.equal("test"); + }); -describe("decrypt()", () => { - it("should return the original input text when given a valid encrypted string", () => { - const encrypted = crypto.encrypt("test"); - const result = crypto.decrypt(encrypted); - expect(result).to.equal("test"); - }); - - it("should throw an error when given an invalid encrypted string", () => { - expect(() => crypto.decrypt("invalid")).to.throw(); - }); + it("should throw an error when given an invalid encrypted string", () => { + expect(() => decrypt("invalid:string")).to.throw(); + }); }); -describe("Api Key ", () => { - // the path to the apiKey.txt file should point to ../src/apiKey.txt - const apiKeyPath = path.resolve(__dirname, "../src/apiKey.txt"); +describe("Credentials Management", () => { + const credentialsPath = path.resolve(__dirname, "../src/credentials.json"); - const removeLink = () => { - try { - fs.unlinkSync(apiKeyPath) - } catch { /* empty */ - } - } + beforeEach(() => { + deleteCredentials(); + }); - beforeEach(removeLink) - afterEach(removeLink) + afterEach(() => { + deleteCredentials(); + }); - it("should return null if the API key has not been saved", () => { - const result = crypto.getApiKey(); - expect(result).to.equal(null); + it("should return null values if credentials have not been saved", () => { + const result = getCredentials(); + expect(result).to.deep.equal({ + apiKey: null, + engine: null, + tavilyApiKey: null, }); + }); - it("should return the API key if it has been saved", () => { - const encrypted = crypto.encrypt("test"); - crypto.saveApiKey(encrypted) - const result = crypto.getApiKey(); - expect(result).to.equal("test"); - }); + it("should save and retrieve credentials correctly", () => { + saveCredentials("testApiKey", "testEngine", "testTavilyApiKey"); + const result = getCredentials(); + expect(result.apiKey).to.equal("testApiKey"); + expect(result.engine).to.equal("testEngine"); + expect(result.tavilyApiKey).to.equal("testTavilyApiKey"); + }); - it("should save the given API key to a file", () => { - if (!fs.existsSync(apiKeyPath)) { - fs.writeFileSync(apiKeyPath, ""); - } - const encryptedText = crypto.encrypt("test"); - crypto.saveApiKey(encryptedText); - const result = crypto.getApiKey()// fs.readFileSync(apiKeyPath, "utf8"); - - expect(result).to.equal("test"); - }); + it("should encrypt the API key when saving", () => { + saveCredentials("testApiKey", "testEngine", "testTavilyApiKey"); + const rawData = fs.readFileSync(credentialsPath, "utf-8"); + const savedCredentials = JSON.parse(rawData); + expect(savedCredentials.apiKey).to.not.equal("testApiKey"); + expect(decrypt(savedCredentials.apiKey)).to.equal("testApiKey"); + }); }); diff --git a/__tests__/utils.spec.ts b/__tests__/utils.spec.ts index d4e9282..36acaf3 100644 --- a/__tests__/utils.spec.ts +++ b/__tests__/utils.spec.ts @@ -2,28 +2,35 @@ import * as path from "path"; import fs from "fs"; -import {expect} from "chai"; +import { expect } from "chai"; +import { apiKeyPrompt /*, generateResponse*/ } from "../src/utils"; -import {apiKeyPrompt/*, generateResponse*/} from "../src/utils"; - -import {encrypt} from "../src/encrypt"; - +import { encrypt } from "../src/creds"; describe("apiKeyPrompt()", () => { - const apiKeyPath = path.resolve(__dirname, "../src/apiKey.txt"); + const credentialsPath = path.resolve(__dirname, "../src/credentials.json"); - beforeEach(() => { - if (!fs.existsSync(apiKeyPath)) { - fs.writeFileSync(apiKeyPath, encrypt("test")); - } - }); + beforeEach(() => { + if (!fs.existsSync(credentialsPath)) { + fs.writeFileSync( + credentialsPath, + JSON.stringify({ + apiKey: encrypt("test"), + engine: "test", + tavilyApiKey: encrypt("test"), + }) + ); + } + }); - it("the api key prompt to user should return a string", async () => { - const result = await apiKeyPrompt() - expect(result).to.be.a("string") - }) -}) + it("the api key prompt to user should return an object with apiKey and engine", async () => { + const result = await apiKeyPrompt(); + expect(result).to.be.an("object"); + expect(result).to.have.property("apiKey"); + expect(result).to.have.property("engine"); + }); +}); // FIXME cannot be executed without a real api key // describe("generateResponse()", () => { @@ -66,25 +73,25 @@ describe("apiKeyPrompt()", () => { // }); // }); - // it("Should create a instance of Configuration", async () => { - // const - // apiKey = "abx", - // prompt = jest.fn(), - // options = {}, - // response = { - // data: { - // choices: ["abc", "def"], - // }, - // } - // - // const getResponse = await generateResponse( - // apiKey, - // prompt, - // options, - // response - // ) - // - // // expect(getResponse).throw(Error) - // expect(getResponse).to.be.a("string"); - // }); +// it("Should create a instance of Configuration", async () => { +// const +// apiKey = "abx", +// prompt = jest.fn(), +// options = {}, +// response = { +// data: { +// choices: ["abc", "def"], +// }, +// } +// +// const getResponse = await generateResponse( +// apiKey, +// prompt, +// options, +// response +// ) +// +// // expect(getResponse).throw(Error) +// expect(getResponse).to.be.a("string"); +// }); // }); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..a575523 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 1b6f11e..e661e6f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,12 @@ { "name": "terminalgpt", - "version": "1.7.4", + "version": "1.7.5", "main": "lib/index.js", "description": "Get GPT like chatGPT on your terminal", "scripts": { "tgpt": "node lib/index.js", "test": "jest --ci --coverage --verbose", - "dev": "ts-node src/index.ts chat --engine gpt-4 --temperature 0.7", - "dev:markdown": "ts-node src/index.ts chat --engine gpt-4 --temperature 0.7 --markdown", - "tunne": "ts-node src/index.ts chat --engine gpt-4 --temperature 0.7 --finetunning true --limit 1", + "dev": "ts-node src/index.ts chat --temperature 0.7", "dev:delete": "ts-node src/index.ts delete", "postinstall": "tsc" }, @@ -32,16 +30,21 @@ } }, "dependencies": { + "@anthropic-ai/sdk": "^0.27.2", + "@dqbd/tiktoken": "^1.0.16", + "@google/generative-ai": "^0.17.1", "@types/gradient-string": "^1.1.5", "@types/marked": "^6.0.0", "@types/marked-terminal": "^6.0.1", "@types/node": "^16.0.0", "@types/prompts": "^2.4.8", + "axios": "^1.7.7", "chalk": "^4.1.2", "clipboardy": "2.3.0", "commander": "^9.5.0", "compromise": "^14.8.1", "gradient-string": "^2.0.2", + "hnswlib-node": "^3.0.0", "lowdb": "^5.1.0", "markdown": "^0.5.0", "marked": "^9.1.6", @@ -66,5 +69,6 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/commands/autossugestion.ts b/src/commands/autossugestion.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/commands/clear.ts b/src/commands/clear.ts new file mode 100644 index 0000000..9251efe --- /dev/null +++ b/src/commands/clear.ts @@ -0,0 +1,5 @@ +const clearFunc = () => { + process.stdout.write("\x1Bc"); +}; + +export default clearFunc; diff --git a/src/commands/exit.ts b/src/commands/exit.ts new file mode 100644 index 0000000..61ad6f8 --- /dev/null +++ b/src/commands/exit.ts @@ -0,0 +1,8 @@ +import chalk from "chalk"; + +const exitFunc = () => { + console.log(chalk.yellow("Goodbye!")); + process.exit(0); +}; + +export default exitFunc; diff --git a/src/commands/file.ts b/src/commands/file.ts new file mode 100644 index 0000000..450e4bb --- /dev/null +++ b/src/commands/file.ts @@ -0,0 +1,22 @@ +import chalk from "chalk"; +import { handleFileReference } from "../handlers/fileHandler"; // Assuming this function exists +import { apiKeyPrompt, promptResponse } from "../utils"; // Assuming this function exists + +const fileFunc = async (userInput: string) => { + const creds = await apiKeyPrompt(); + // we need to call file handler here + const [, filePath, ...promptParts] = userInput.split(" "); + const promptText = promptParts.join(" "); + if (filePath) { + await handleFileReference(filePath, promptText); + if (creds.apiKey != null) { + await promptResponse(creds.engine, creds.apiKey, userInput, {}); + } + } else { + console.log( + chalk.yellow("Please provide a file path. Usage: @file [prompt]") + ); + } +}; + +export default fileFunc; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..e7dff34 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,43 @@ +import exitFunc from "./exit"; +import clearFunc from "./clear"; +import fileFunc from "./file"; +import webFunc from "./web"; + +export interface Plugin { + keyword: string; + execute: (userInput: string) => Promise | void; +} + +const plugins: Plugin[] = []; + +const registerPlugin = ( + keyword: string, + execute: (userInput: string) => Promise | void +) => { + plugins.push({ keyword, execute }); +}; + +export const mapPlugins = (userInput: string): Plugin | undefined => { + return plugins.find((plugin) => userInput.startsWith(plugin.keyword)); +}; + +export const initializePlugins = () => { + // Register your plugins here + registerPlugin("exit", exitFunc); + registerPlugin("clear", clearFunc); + registerPlugin("@file", fileFunc); + registerPlugin("@web", webFunc); +}; + +export const executeCommand = (userInput: string): boolean => { + const command = plugins.find((plugin) => + userInput.startsWith(plugin.keyword) + ); + if (command) { + command.execute(userInput); + return true; + } + return false; +}; + +export default executeCommand; diff --git a/src/commands/web.ts b/src/commands/web.ts new file mode 100644 index 0000000..bd54dab --- /dev/null +++ b/src/commands/web.ts @@ -0,0 +1,20 @@ +import chalk from "chalk"; +import { handleWebResearch } from "../handlers/webHandler"; +import { promptResponse, apiKeyPrompt } from "../utils"; + +const webFunc = async (userInput: string) => { + const creds = await apiKeyPrompt(); + const query = userInput.slice(5).trim(); + if (query) { + await handleWebResearch(query, userInput); + if (creds.apiKey != null) { + await promptResponse(creds.engine, creds.apiKey, userInput, {}); + } + } else { + console.log( + chalk.yellow("Please provide a search query. Usage: @web ") + ); + } +}; + +export default webFunc; diff --git a/src/context.ts b/src/context.ts index 38a2bf7..9ec518f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,17 +1,139 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -//const fs = require("fs"); +import { encoding_for_model, TiktokenModel } from "@dqbd/tiktoken"; +import hnswlib from "hnswlib-node"; -//const contextFile = `${__dirname}/../data/context-terminal-gpt.txt`; +export interface ContextItem { + role: string; + content: string; +} + +class VectorStore { + private index: hnswlib.HierarchicalNSW; + private items: ContextItem[]; + private encoder: any; + private maxTokens: number; + private currentTokens: number; + private readonly dimension: number = 1536; // Fixed dimension size + + constructor( + model: TiktokenModel = "gpt-3.5-turbo", + maxTokens: number = 4096 + ) { + try { + this.encoder = encoding_for_model(model); + this.index = new hnswlib.HierarchicalNSW("cosine", this.dimension); + this.index.initIndex(1000); // Initialize index with a maximum of 1000 elements + this.items = []; + this.maxTokens = maxTokens; + this.currentTokens = 0; + } catch (error) { + console.error("Error initializing VectorStore:", error); + throw new Error("Failed to initialize VectorStore"); + } + } + + addItem(item: ContextItem) { + try { + if (!item || typeof item.content !== "string") { + console.error("Invalid item:", item); + return; + } + const vector = this.textToVector(item.content); + const tokenCount = this.encoder.encode(item.content).length; + + // Remove old items if adding this would exceed the token limit + while ( + this.currentTokens + tokenCount > this.maxTokens && + this.items.length > 0 + ) { + const removedItem = this.items.shift(); + if (removedItem) { + this.currentTokens -= this.encoder.encode(removedItem.content).length; + } + } + + const id = this.items.length; + this.index.addPoint(vector, id); + this.items.push(item); + this.currentTokens += tokenCount; + } catch (error) { + console.error("Error adding item to VectorStore:", error); + } + } + + getRelevantContext(query: string, k: number = 5): ContextItem[] { + try { + if (this.items.length === 0) { + // console.log("No items in context"); + return []; + } + const queryVector = this.textToVector(query); + const results = this.index.searchKnn( + queryVector, + Math.min(k, this.items.length) + ); + if (!results || !Array.isArray(results.neighbors)) { + // console.error("Unexpected result from searchKnn:", results); + return []; + } + return results.neighbors.map( + (id) => + this.items[id] || { + role: "system", + content: "Context item not found", + } + ); + } catch (error) { + console.error("Error getting relevant context:", error); + return []; + } + } -let context: any[] = []; + private textToVector(text: string): number[] { + try { + const encoded = this.encoder.encode(text); + const vector = new Array(this.dimension).fill(0); + for (let i = 0; i < encoded.length && i < this.dimension; i++) { + vector[i] = encoded[i] / 100; // Simple normalization + } + return vector; + } catch (error) { + console.error("Error converting text to vector:", error); + throw new Error("Failed to convert text to vector"); + } + } +} + +let vectorStore: VectorStore; + +try { + vectorStore = new VectorStore(); +} catch (error) { + console.error("Error creating VectorStore:", error); + throw new Error("Failed to create VectorStore"); +} -/** - * Adds a new context to the existing context array. - * - * @param {any} text - The text to be added to the context array. - */ -export function addContext(text: any) { - context = [...context, text]; +export function addContext(item: ContextItem) { + // console.log("Adding context:", item); // Debug log + const existingItems = vectorStore.getRelevantContext(item.content); + if ( + !existingItems.some( + (existingItem) => + existingItem.role === item.role && existingItem.content === item.content + ) + ) { + vectorStore.addItem(item); + } else { + // console.log("Skipping duplicate context item"); + } } -export const getContext = () => context; +export function getContext(query: string): ContextItem[] { + // console.log("Getting context for query:", query); // Debug log + return vectorStore.getRelevantContext(query); +} + +export function clearContext() { + // console.log("Clearing context"); // Debug log + vectorStore = new VectorStore(); +} diff --git a/src/creds.ts b/src/creds.ts new file mode 100644 index 0000000..ea13e9f --- /dev/null +++ b/src/creds.ts @@ -0,0 +1,107 @@ +import * as fs from "fs"; + +import * as crypto from "crypto"; + +const algorithm = "aes-256-cbc"; +const secretKey = "terminalGPT"; + +/** + * Encrypts the given text using the specified algorithm, secret key, and initialization vector. + * + * @param {string} text - The text to be encrypted. + * @return {string} The encrypted text in the format: IV:encryptedText. + */ +export function encrypt(text: string) { + const iv = crypto.randomBytes(16); + const key = crypto.scryptSync(secretKey, "salt", 32); + const cipher = crypto.createCipheriv(algorithm, key, iv); + let encrypted = cipher.update(text); + + encrypted = Buffer.concat([encrypted, cipher.final()]); + return iv.toString("hex") + ":" + encrypted.toString("hex"); +} + +/** + * Decrypts the given text using a specific algorithm and secret key. + * + * @param {string} text - The text to be decrypted. + * @return {string} - The decrypted text. + */ +export function decrypt(text: string) { + const textParts = text.split(":"); + const iv = Buffer.from(textParts.shift()!, "hex"); + const encryptedText = Buffer.from(textParts.join(":"), "hex"); + const key = crypto.scryptSync(secretKey, "salt", 32); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); +} + +/** + * Retrieves the API engine from the "engine.txt" file. + * + * @return {string | null} The API engine if the file exists, otherwise null. + */ +export function getEngine(): string | null { + if (fs.existsSync(`${__dirname}/engine.txt`)) { + const getEngine = fs.readFileSync(`${__dirname}/engine.txt`, "utf8"); + return getEngine; + } + return null; +} + +/** + * Saves the API key and engine to a JSON file. + * + * @param {string} apiKey - The API key to save. + * @param {string} engine - The API engine to save. + * @return {void} This function does not return anything. + */ +export function saveCredentials( + apiKey: string, + engine: string, + tavilyApiKey: string +) { + const credentials = { apiKey: encrypt(apiKey), engine, tavilyApiKey }; + fs.writeFileSync( + `${__dirname}/credentials.json`, + JSON.stringify(credentials) + ); +} + +/** + * Retrieves the API key and engine from the "credentials.json" file. + * + * @return {{ apiKey: string | null, engine: string | null, tavilyApiKey: string | null }} The API key and engine, or null if the file does not exist. + */ +export function getCredentials(): { + apiKey: string | null; + engine: string | null; + tavilyApiKey: string | null; +} { + if (fs.existsSync(`${__dirname}/credentials.json`)) { + const data = fs.readFileSync(`${__dirname}/credentials.json`, "utf8"); + const { apiKey, engine, tavilyApiKey } = JSON.parse(data); + return { + apiKey: apiKey ? decrypt(apiKey) : null, + engine, + tavilyApiKey: tavilyApiKey || null, + }; + } + return { apiKey: null, engine: null, tavilyApiKey: null }; +} + +/** + * Deletes the credentials file if it exists. + * + * @return {boolean} Returns true if the credentials file was deleted, false otherwise. + */ +export function deleteCredentials() { + const credentialsFilePath = `${__dirname}/credentials.json`; + if (fs.existsSync(credentialsFilePath)) { + fs.unlinkSync(credentialsFilePath); + return true; + } + return false; +} diff --git a/src/encrypt.ts b/src/encrypt.ts deleted file mode 100644 index 0e0fe7d..0000000 --- a/src/encrypt.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as fs from "fs"; - -import * as crypto from "crypto"; - -const algorithm = "aes-256-cbc"; -const secretKey = "terminalGPT"; - -/** - * Encrypts the given text using the specified algorithm, secret key, and initialization vector. - * - * @param {string} text - The text to be encrypted. - * @return {string} The encrypted text in the format: IV:encryptedText. - */ -export function encrypt(text: string) { - const iv = crypto.randomBytes(16); - const key = crypto.scryptSync(secretKey, "salt", 32); - const cipher = crypto.createCipheriv(algorithm, key, iv); - let encrypted = cipher.update(text); - - encrypted = Buffer.concat([encrypted, cipher.final()]); - return iv.toString("hex") + ":" + encrypted.toString("hex"); -} - -/** - * Decrypts the given text using a specific algorithm and secret key. - * - * @param {string} text - The text to be decrypted. - * @return {string} - The decrypted text. - */ -export function decrypt(text: string) { - const textParts = text.split(":"); - const iv = Buffer.from(textParts.shift()!, "hex"); - const encryptedText = Buffer.from(textParts.join(":"), "hex"); - const key = crypto.scryptSync(secretKey, "salt", 32); - const decipher = crypto.createDecipheriv(algorithm, key, iv); - let decrypted = decipher.update(encryptedText); - decrypted = Buffer.concat([decrypted, decipher.final()]); - return decrypted.toString(); -} - -/** - * Saves a custom URL to a file. - * - * @param {string | undefined} url - The custom URL to be saved. If undefined, the file is deleted. - * @return {void} - */ -export function saveCustomUrl(url: string | undefined) { - if (url === undefined) { - fs.unlinkSync(`${__dirname}/customUrl.txt`); - } else { - fs.writeFileSync(`${__dirname}/customUrl.txt`, url); - } -} - -/** - * Retrieves a custom URL from a text file if it exists. - * - * @return {string | undefined} The custom URL if it exists, otherwise undefined. - */ -export function getCustomUrl(): string | undefined { - if (fs.existsSync(`${__dirname}/customUrl.txt`)) { - return fs.readFileSync(`${__dirname}/customUrl.txt`, "utf8"); - } - return undefined; -} - -/** - * Saves the API key to a file. - * - * @param {string} apiKey - The API key to save. - * @return {void} This function does not return anything. - */ -export function saveApiKey(apiKey: string) { - fs.writeFileSync(`${__dirname}/apiKey.txt`, apiKey); -} - -/** - * Deletes the API key file if it exists. - * - * @return {boolean} Returns true if the API key file was deleted, false otherwise. - */ -export function deleteApiKey() { - const apiKeyFilePath = `${__dirname}/apiKey.txt`; - if (fs.existsSync(apiKeyFilePath)) { - fs.unlinkSync(apiKeyFilePath); - return true; - } - return false; -} - -/** - * Retrieves the API key from the "apiKey.txt" file, decrypts it, and returns it. - * - * @return {string | null} The decrypted API key, or null if the file does not exist. - */ -export function getApiKey(): string | null { - if (fs.existsSync(`${__dirname}/apiKey.txt`)) { - const getEncryptedScript = fs.readFileSync( - `${__dirname}/apiKey.txt`, - "utf8" - ); - return decrypt(getEncryptedScript); - } - return null; -} diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts new file mode 100644 index 0000000..6760139 --- /dev/null +++ b/src/engine/Engine.ts @@ -0,0 +1,94 @@ +import { OpenAIEngine } from "./openAi"; // Import OpenAI engine +import { AnthropicEngine } from "./anthropic"; // Import Anthropic engine +import { GeminiEngine } from "./gemini"; // Import Gemini engine +import { OllamaEngine } from "./ollama"; // Import Ollama engine + +export interface AiEngineConfig { + apiKey: string; + model: string; + maxTokensOutput: number; + maxTokensInput: number; + baseURL?: string; +} + +export interface AiEngine { + config: AiEngineConfig; + engineResponse( + prompt: string, + options?: { temperature?: number } + ): Promise; +} + +export class Engine implements AiEngine { + private engine: AiEngine; + + constructor(engineType: string, public config: AiEngineConfig) { + this.engine = this.createEngine(engineType, config); + } + + // Add this method + async engineResponse( + prompt: string, + options?: { temperature?: number } + ): Promise { + return this.engine.engineResponse(prompt, options); + } + + private createEngine(engineType: string, config: AiEngineConfig): AiEngine { + const engineResponse = ( + prompt: string, + opts?: { temperature?: number } + ) => { + const engineOptions = { + model: config.model, + temperature: opts?.temperature, + }; + + switch (engineType) { + case "openAI": + return OpenAIEngine(config.apiKey, prompt, engineOptions); + case "anthropic": + return AnthropicEngine(config.apiKey, prompt, engineOptions); + case "gemini": + return GeminiEngine(config.apiKey, prompt, engineOptions); + case "ollama": + return OllamaEngine(config.apiKey, prompt, engineOptions); + default: + throw new Error("Unsupported engine type"); + } + }; + + return { config, engineResponse }; + } +} + +/** + * Main function to generate a response using the specified AI engine. + * @param engineType - The type of engine to use. + * @param apiKey - The API key for authentication. + * @param prompt - The prompt to send to the AI engine. + * @param opts - Additional options for generating the response. + * @returns The generated response from the AI engine. + */ +export async function generateResponse( + engineType: string, + apiKey: string, + prompt: string, + opts: { + model: string; + temperature?: number; + } +): Promise { + const config: AiEngineConfig = { + apiKey, + model: opts.model, + maxTokensOutput: 8192, + maxTokensInput: 4096, + }; + + const engine = new Engine(engineType, config); + + return await engine.engineResponse(prompt, { + temperature: opts.temperature, + }); +} diff --git a/src/engine/anthropic.ts b/src/engine/anthropic.ts new file mode 100644 index 0000000..8867c54 --- /dev/null +++ b/src/engine/anthropic.ts @@ -0,0 +1,82 @@ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import AnthropicClient from "@anthropic-ai/sdk"; +import { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/messages.mjs"; +import { addContext, getContext, ContextItem } from "../context"; +import { loadWithRocketGradient } from "../gradient"; +import chalk from "chalk"; + +export const AnthropicEngine = async ( + apiKey: string | Promise, + prompt: string, + opts: { + model: string; + temperature: unknown; + } +) => { + const apiKeyValue = await apiKey; + const anthropic = new AnthropicClient({ apiKey: apiKeyValue }); + const spinner = loadWithRocketGradient("Thinking...").start(); + + try { + const relevantContext = getContext(prompt); + console.log("Relevant context:", relevantContext); // Debug log + + let messages: ContextItem[] = []; + let systemMessage = ""; + + // Process relevant context + for (const item of relevantContext) { + if (item.role === "system") { + systemMessage += item.content + "\n"; + } else { + messages.push(item); + } + } + + // Ensure messages alternate and start with a user message + if (messages.length === 0 || messages[0].role !== "user") { + messages = [{ role: "user", content: prompt }]; + } else { + // If the last message is from the user, combine it with the new prompt + if (messages[messages.length - 1].role === "user") { + messages[messages.length - 1].content += "\n" + prompt; + } else { + messages.push({ role: "user", content: prompt }); + } + } + + console.log("Final messages:", messages); // Debug log + + const requestParams: MessageCreateParamsNonStreaming = { + model: opts.model || "claude-3-opus-20240229", + messages: messages, + system: systemMessage.trim() || undefined, + temperature: opts.temperature ? Number(opts.temperature) : 1, + max_tokens: 1024, + }; + + const response = await anthropic.messages.create(requestParams); + const message = response.content + .filter((block) => block.type === "text") + .map((block) => (block.type === "text" ? block.text : "")) + .join("\n"); + + if (message) { + addContext({ role: "user", content: prompt }); + addContext({ role: "assistant", content: message }); + spinner.stop(); + return message; + } else { + throw new Error("No text content received in the response"); + } + } catch (err) { + spinner.stop(); + if (err instanceof Error) { + console.error(err); + throw new Error(`${chalk.red(err.message)}`); + } else { + throw new Error(`${chalk.red("An unknown error occurred")}`); + } + } +}; diff --git a/src/engine/gemini.ts b/src/engine/gemini.ts new file mode 100644 index 0000000..77dd943 --- /dev/null +++ b/src/engine/gemini.ts @@ -0,0 +1,67 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { addContext, getContext } from "../context"; +import { loadWithRocketGradient } from "../gradient"; + +export const GeminiEngine = async ( + apiKey: string | Promise, + prompt: string, + opts: { + model: string; // Specify the model to use + temperature?: number; // Optional temperature setting + filePath?: string; // Optional file path for uploads + mimeType?: string; // Optional MIME type for the file + } +) => { + const apiKeyValue = await apiKey; + const genAI = new GoogleGenerativeAI(apiKeyValue); + const spinner = loadWithRocketGradient("Thinking...").start(); + + try { + // Get the generative model + const model = genAI.getGenerativeModel({ + model: opts.model || "gemini-1.5-flash", + }); + + // Generate response + const generationConfig = { + temperature: opts.temperature || 1, + topP: 0.95, + topK: 64, + maxOutputTokens: 8192, + responseMimeType: "text/plain", + }; + + // Prepare chat history with context + const relevantContext = getContext(prompt); + const chatHistory = relevantContext.map((context) => ({ + role: context.role as "user" | "model", + parts: [{ text: context.content }], + })); + + const chatSession = model.startChat({ + generationConfig, + history: [ + ...chatHistory, + { + role: "user", + parts: [{ text: prompt }], + }, + ], + }); + + const result = await chatSession.sendMessage(prompt); + const responseText = result.response.text(); + + if (responseText) { + addContext({ role: "assistant", content: responseText }); + spinner.stop(); + return responseText; + } else { + throw new Error("Undefined messages received"); + } + } catch (err) { + spinner.stop(); + console.error(err); + throw new Error("An error occurred while generating content"); + } +}; diff --git a/src/engine/ollama.ts b/src/engine/ollama.ts new file mode 100644 index 0000000..1768ad4 --- /dev/null +++ b/src/engine/ollama.ts @@ -0,0 +1,83 @@ +import chalk from "chalk"; +import { addContext, getContext } from "../context"; +import { loadWithRocketGradient } from "../gradient"; +import axios from "axios"; + +export const OllamaEngine = async ( + apiKey: string | Promise, + prompt: string, + opts: { + model: string; // Specify the model to use + temperature: unknown; + } +) => { + const apiKeyValue = await apiKey; + const spinner = loadWithRocketGradient("Thinking...").start(); + + const relevantContext = getContext(prompt); + + try { + const response = await axios.post( + `http://localhost:11434/api/chat`, // Replace with the actual Ollama API endpoint + { + model: opts.model || "llama2", // Use a default model if none is provided + messages: [ + ...relevantContext.map((item) => ({ + role: item.role, + content: item.content, + })), + { role: "user", content: prompt }, + ], + temperature: opts.temperature ? Number(opts.temperature) : 1, + }, + { + headers: { + Authorization: `Bearer ${apiKeyValue}`, + "Content-Type": "application/json", + }, + } + ); + + const message = response.data.message?.content; + + if (message) { + addContext({ role: "assistant", content: message }); + spinner.stop(); + return message; + } else { + throw new Error("Undefined messages received"); + } + } catch (err) { + spinner.stop(); + // Handle errors similarly to OpenAI + if (axios.isAxiosError(err)) { + console.log(err); + switch (err.response?.status) { + case 404: + throw new Error( + `${chalk.red( + "Not Found: Model not found. Please check the model name." + )}` + ); + case 429: + throw new Error( + `${chalk.red( + "API Rate Limit Exceeded: Too many requests. Please wait before trying again." + )}` + ); + case 400: + throw new Error( + `${chalk.red("Bad Request: Prompt provided is empty or too long.")}` + ); + case 500: + throw new Error( + `${chalk.red("Internal Server Error: Please try again later.")}` + ); + default: + throw new Error(`${chalk.red("An unknown error occurred")}`); + } + } else { + throw new Error(`${chalk.red("An unknown error occurred")}`); + } + } +}; diff --git a/src/engine/openAi.ts b/src/engine/openAi.ts new file mode 100644 index 0000000..5c4e13d --- /dev/null +++ b/src/engine/openAi.ts @@ -0,0 +1,49 @@ +import OpenAI from "openai"; +import chalk from "chalk"; +import { addContext, getContext } from "../context"; +import { loadWithRocketGradient } from "../gradient"; +import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; + +export const OpenAIEngine = async ( + apiKey: string | Promise, + prompt: string, + opts: { + model: string; + temperature: unknown; + } +) => { + const apiKeyValue = await apiKey; + const openai = new OpenAI({ apiKey: apiKeyValue }); + const spinner = loadWithRocketGradient("Thinking...").start(); + + try { + const relevantContext = getContext(prompt); + const messages: ChatCompletionMessageParam[] = [ + ...relevantContext.map((item) => ({ + role: item.role as "system" | "user" | "assistant", + content: item.content, + })), + { role: "user", content: prompt }, + ]; + + const completion = await openai.chat.completions.create({ + model: opts.model || "gpt-4o-2024-08-06", + messages: messages, + temperature: opts.temperature ? Number(opts.temperature) : 1, + }); + + const message = completion.choices[0].message; + addContext({ role: message.role, content: message.content || "" }); + + spinner.stop(); + return message.content; + } catch (err) { + spinner.stop(); + if (err instanceof Error) { + console.log(err); + throw new Error(`${chalk.red(err.message)}`); + } else { + throw new Error(`${chalk.red("An unknown error occurred")}`); + } + } +}; diff --git a/src/gpt.ts b/src/gpt.ts deleted file mode 100644 index ddd0fde..0000000 --- a/src/gpt.ts +++ /dev/null @@ -1,87 +0,0 @@ -import chalk from "chalk"; - -import OpenAI from "openai"; - -import { addContext, getContext } from "./context"; - -import { loadWithRocketGradient } from "./gradient"; - -export default async ( - apiKey: string | Promise, - prompt: string, - opts: { - engine: string; - temperature: unknown; - } -) => { - const apiKeyValue = await apiKey; - const openai = new OpenAI({ apiKey: apiKeyValue }); - const spinner = loadWithRocketGradient("Thinking...").start(); - - addContext({ - role: "system", - content: - "Read the context, when returning the answer , always wrapping block of code exactly within triple backticks ", - }); - addContext({ role: "user", content: prompt }); - - const request = await openai.chat.completions - .create({ - model: opts.engine || "gpt-4-1106-preview", - messages: getContext(), - temperature: opts.temperature ? Number(opts.temperature) : 1, - }) - .then((res) => { - console.log("here"); - if (typeof res.choices[0].message !== "undefined") { - addContext(res.choices[0].message); - spinner.stop(); - return res.choices[0].message; - } else { - throw new Error("Undefined messages received"); - } - }) - .catch((err) => { - console.log(err); - spinner.stop(); - switch (err["response"]["status"]) { - case 404: - throw new Error( - `${chalk.red( - "Not Found: Model not found. Please check the model name." - )}` - ); - case 429: - throw new Error( - `${chalk.red( - "API Rate Limit Exceeded: ChatGPT is getting too many requests from the user in a short period of time. Please wait a while before sending another message." - )}` - ); - case 400: - throw new Error( - `${chalk.red( - "Bad Request: Prompt provided is empty or too long. Prompt should be between 1 and 4096 tokens." - )}` - ); - case 402: - throw new Error( - `${chalk.red( - "Payment Required: ChatGPT quota exceeded. Please check you chatGPT account." - )}` - ); - case 503: - throw new Error( - `${chalk.red( - "Service Unavailable: ChatGPT is currently unavailable, possibly due to maintenance or high traffic. Please try again later." - )}` - ); - default: - throw new Error(`${err}`); - } - }); - if (request === undefined || !request?.content) { - throw new Error("Undefined request or content"); - } - - return request; -}; diff --git a/src/handlers/fileHandler.ts b/src/handlers/fileHandler.ts new file mode 100644 index 0000000..3d3a52e --- /dev/null +++ b/src/handlers/fileHandler.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import fs from "fs"; +import path from "path"; +import { addContext } from "../context"; +import chalk from "chalk"; + +export async function handleFileReference( + filePath: string, + userPrompt: string +) { + try { + const absolutePath = path.resolve(filePath); + const fileContent = fs.readFileSync(absolutePath, "utf-8"); + const fileContext = `File content of ${filePath}:\n\n${fileContent}\n\nUser prompt: ${userPrompt}`; + addContext({ role: "system", content: fileContext }); + console.log( + chalk.green( + `File ${filePath} has been added to the conversation context.` + ) + ); + } catch (error: any) { + console.error(chalk.red(`Error reading file: ${error.message}`)); + } +} diff --git a/src/handlers/webHandler.ts b/src/handlers/webHandler.ts new file mode 100644 index 0000000..bb42d4a --- /dev/null +++ b/src/handlers/webHandler.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import axios from "axios"; +import { addContext } from "../context"; +import chalk from "chalk"; +import { getCredentials, saveCredentials, encrypt, decrypt } from "../creds"; +import readline from "readline"; + +export async function handleWebResearch(query: string, userPrompt: string) { + try { + let credentials = getCredentials(); + + if (!credentials.tavilyApiKey) { + console.log(chalk.yellow("Tavily API key not found.")); + console.log( + chalk.cyan("Please visit https://tavily.com to get an API key.") + ); + const apiKey = await promptForApiKey(); + const encryptedTavilyApiKey = encrypt(apiKey); + saveCredentials( + credentials.apiKey || "", + credentials.engine || "", + encryptedTavilyApiKey + ); + credentials = getCredentials(); + } + + const tavilyApiKey = decrypt(credentials.tavilyApiKey!); + + console.log(chalk.yellow(`Searching the web for: "${query}"...`)); + + const response = await axios.post("https://api.tavily.com/search", { + api_key: tavilyApiKey, + query: query, + search_depth: "basic", + max_results: 3, + }); + + const searchResults = response.data.results + .map( + (result: any) => + `Title: ${result.title}\nSnippet: ${result.snippet}\n\n` + ) + .join(""); + + console.log(chalk.cyan("Search Results:")); + console.log(searchResults); + + const webContext = `Web search results for "${query}":\n\n${searchResults}\n\nUser prompt: ${userPrompt}`; + addContext({ role: "system", content: webContext }); + console.log( + chalk.green( + `Web research results for "${query}" have been added to the conversation context.` + ) + ); + } catch (error: any) { + console.error(chalk.red(`Error performing web research: ${error.message}`)); + } +} + +async function promptForApiKey(): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question("Please enter your Tavily API key: ", (apiKey) => { + rl.close(); + resolve(encrypt(apiKey.trim())); + }); + }); +} diff --git a/src/index.ts b/src/index.ts index e01dd84..c6e39dc 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,109 +1,72 @@ #!/usr/bin/env node import chalk from "chalk"; -import process from "process"; -import prompts from "prompts"; - +import * as process from "process"; +import { Command } from "commander"; import intro from "./intro"; +import { apiKeyPrompt, promptResponse } from "./utils"; +import { deleteCredentials } from "./creds"; +import readline from "readline"; +import { mapPlugins, initializePlugins } from "./commands"; -import { apiKeyPrompt, generateResponse } from "./utils"; - -import { deleteApiKey, saveCustomUrl } from "./encrypt"; +const program = new Command(); -import * as c from "commander"; - -const commander = new c.Command(); -commander +program .command("chat") - .option("-e, --engine ", "GPT model to use") + .option("-e, --engine ", "LLM to use") .option("-t, --temperature ", "Response temperature") - .option("-m, --markdown", "Show markdown in the terminal") .usage(`"" [options]`) .action(async (opts) => { intro(); - apiKeyPrompt().then((apiKey: string | null) => { - const prompt = async () => { - const response = await prompts({ - type: "text", - name: "value", - message: `${chalk.blueBright("You: ")}`, - validate: () => { - return true; - }, - onState: (state) => { - if (state.aborted) { - process.exit(0); - } - }, - }); + const creds = await apiKeyPrompt(); + + // Initialize plugins + initializePlugins(); - switch (response.value) { - case "exit": - return process.exit(0); - case "clear": - return process.stdout.write("\x1Bc"); - default: - if (apiKey != null) { - generateResponse(apiKey, prompt, response, opts); - } - return; + const prompt = () => { + process.stdout.write(chalk.blueBright("\nYou: ")); + process.stdin.resume(); + process.stdin.setEncoding("utf-8"); + process.stdin.once("data", async (data) => { + const userInput = data.toString().trim(); + + const plugin = mapPlugins(userInput); + if (plugin) { + await plugin.execute(userInput); + } else if (creds.apiKey != null) { + await promptResponse(creds.engine, creds.apiKey, userInput, opts); } - }; - prompt(); - }); + + prompt(); + }); + }; + + prompt(); }); -// create commander to delete api key -commander +program .command("delete") .description("Delete your API key") .action(async () => { - const response = await prompts({ - type: "select", - name: "value", - message: "Are you sure?", - choices: [ - { title: "Yes", value: "yes" }, - { title: "No - exit", value: "no" }, - ], - initial: 0, + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, }); - if (response.value === "yes") { - const apiKeyDeleted = deleteApiKey(); - if (apiKeyDeleted) { - console.log("API key deleted"); + rl.question("Are you sure? (yes/no): ", (answer) => { + if (answer.toLowerCase() === "yes") { + const apiKeyDeleted = deleteCredentials(); + if (apiKeyDeleted) { + console.log("API key deleted"); + } else { + console.log("API key file not found, no action taken."); + } } else { - console.log("API key file not found, no action taken."); + console.log("Deletion cancelled"); } - return process.exit(0); - } else { - console.log("Deletion cancelled"); - return process.exit(0); - } - }); - -commander - .command("endpoint") - .option("--set ", "Set your custom endpoint") - .option("-r, --reset", "Reset the API endpoint to default ") - .description("Configure your API endpoint") - .action(async () => { - console.log("Send empty to set default openai endpoint"); - prompts({ - type: "text", - name: "value", - validate: (t) => - t.search(/(https?:\/(\/.)+).*/g) === 0 || t === "" - ? true - : "Urls only allowed", - message: "Insert endpoint: ", - }) - .then((response) => - (response.value as string).replace("/chat/completions", "") - ) - .then((value) => (/(https?:\/(\/.)+).*/g.test(value) ? value : undefined)) - .then(saveCustomUrl); + rl.close(); + process.exit(0); + }); }); -commander.parse(process.argv); +program.parse(process.argv); diff --git a/src/utils.ts b/src/utils.ts index 2275539..491c882 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ -import * as clipboard from "clipboardy"; -import { encrypt, getApiKey, saveApiKey } from "./encrypt"; -import prompts from "prompts"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// import * as clipboard from "clipboardy"; + +import prompts, { PromptObject } from "prompts"; import chalk from "chalk"; @@ -10,7 +11,8 @@ import { marked } from "marked"; import TerminalRenderer from "marked-terminal"; -import generateCompletion from "./gpt"; +import { generateResponse } from "./engine/Engine"; +import { encrypt, getCredentials, saveCredentials } from "./creds"; marked.setOptions({ // Define custom renderer @@ -18,28 +20,46 @@ marked.setOptions({ }); /** - * Prompts the user for an API key and returns it. + * Prompts the user for an API key and engine, then saves them. * - * @return {string} The API key entered by the user. + * @return {{ apiKey: string, engine: string }} The API key and engine entered by the user. */ export async function apiKeyPrompt() { - let apiKey = getApiKey(); + const credentials = getCredentials(); + const apiKey = credentials?.apiKey; + const engine = credentials?.engine; - if (!apiKey) { - const response = await prompts({ + const questions: PromptObject[] = [ + { + type: "select", + name: "engine", + message: "Pick LLM", + choices: [ + { title: "openAI", value: "openAI" }, + { title: "anthropic", value: "anthropic" }, + { title: "gemini", value: "gemini" }, + { title: "ollama", value: "ollama" }, + ], + initial: 0, + }, + { type: "password", name: "apiKey", message: "Enter your OpenAI API key:", - validate: (value) => { + validate: (value: string) => { return value !== ""; }, - }); - - apiKey = response.apiKey; - saveApiKey(encrypt(response.apiKey)); + }, + ]; + + if (!apiKey || !engine) { + const response = await prompts(questions); + // Save both API key and engine + saveCredentials(encrypt(response.apiKey), response.engine, "user"); + return { apiKey: response.apiKey, engine: response.engine }; } - return apiKey; + return { apiKey, engine }; } /** @@ -48,101 +68,63 @@ export async function apiKeyPrompt() { * @param {string} text - The text to search for matches within ``` code blocks. * @return {void} This function does not return a value. */ -async function checkBlockOfCode(text: string) { - // get all matches of text within ``` - const regex = /```[\s\S]*?```/g; - const matches = text.match(regex); - if (matches) { - const recentTextNoBackticks = matches[0].replace(/```/g, ""); - const response = await prompts({ - type: "confirm", - name: "copy", - message: `Copy recent code to clipboard?`, - initial: true, - }); - - if (response.copy) { - clipboard.writeSync(recentTextNoBackticks); - } - } -} +// async function checkBlockOfCode(text: string) { +// // get all matches of text within ``` +// const regex = /```[\s\S]*?```/g; +// const matches = text.match(regex); +// if (matches) { +// const recentTextNoBackticks = matches[0].replace(/```/g, ""); +// const response = await prompts({ +// type: "confirm", +// name: "copy", +// message: `Copy recent code to clipboard?`, +// initial: true, +// }); + +// if (response.copy) { +// clipboard.writeSync(recentTextNoBackticks); +// } +// } +// } /** * Generates a response based on the given API key, prompt, response, and options. * - * @param {string} apiKey - The API key to authenticate the request. + * @param {string} engine - The engine to use for generating the response. * @param {() => void} prompt - The function to prompt the user. * @param {prompts.Answers} response - The user's response. - * @param {{ engine: string; temperature: unknown; markdown?: unknown; }} opts - The options for generating the response. + * @param {{ temperature?: number; markdown?: boolean; model: string; }} opts - The options for generating the response. * @return {Promise} A promise that resolves when the response is generated. */ -export async function generateResponse( +export const promptResponse = async ( + engine: string, apiKey: string, - prompt: () => void, - response: prompts.Answers, - opts: { - engine: string; - temperature: unknown; - markdown?: unknown; - } -) { + userInput: string, + opts: any +): Promise => { try { - const request = await generateCompletion(apiKey, response.value, opts); + const request = await generateResponse(engine, apiKey, userInput, { + model: opts.model, + temperature: opts.temperature, + }); + + const text = request ?? ""; - if (request === undefined || !request?.content) { + if (!text) { throw new Error("Undefined request or content"); } - // map all choices to text - const getText = [request.content]; - - console.log(`${chalk.cyan("GPT: ")}`); - - if (opts.markdown) { - const markedText = marked.parse(getText[0]); - let i = 0; - const interval = setInterval(() => { - if (i < markedText.length) { - process.stdout.write(markedText[i]); - i++; - } else { - clearInterval(interval); - process.stdout.write("\n"); // Add this line - checkBlockOfCode(markedText).then(prompt); - } - }, 10); - } else { - // console log each character of the text with a delay and then call prompt when it finished - let i = 0; - const interval = setInterval(() => { - if (i < getText[0].length) { - process.stdout.write(getText[0][i]); - i++; - } else { - clearInterval(interval); - process.stdout.write("\n"); // Add this line - checkBlockOfCode(getText[0]).then(prompt); - } - }, 10); + console.log(`${chalk.cyan("Answer: ")}`); + + const markedText = marked.parse(text); + for (let i = 0; i < markedText.length; i++) { + process.stdout.write(markedText[i]); + await new Promise((resolve) => setTimeout(resolve, 10)); } + // console.log("\n"); // Add a newline after the response } catch (err) { console.error(`${chalk.red("Something went wrong!!")} ${err}`); - // create a prompt of type select , with the options to exit or try again - const response = await prompts({ - type: "select", - name: "value", - message: "Try again?", - choices: [ - { title: "Yes", value: "yes" }, - { title: "No - exit", value: "no" }, - ], - initial: 0, - }); - - if (response.value == "no") { - return process.exit(0); - } - - generateResponse(apiKey, prompt, response, opts); + // Error handling remains the same + // ... } -} +};