diff --git a/packages/qllm-cli/package.json b/packages/qllm-cli/package.json index e50e6da..9d54953 100644 --- a/packages/qllm-cli/package.json +++ b/packages/qllm-cli/package.json @@ -43,7 +43,9 @@ "license": "Apache-2.0", "devDependencies": { "@types/copy-paste": "^1.1.33", + "@types/jest": "^29.5.12", + "@types/mime-types": "^2.1.4", "@types/node": "^22.5.0", "@types/prompts": "^2.4.9", "@types/screenshot-desktop": "^1.12.3", @@ -58,18 +60,19 @@ "typescript": "^5.5.4" }, "dependencies": { - "@types/node": "^22.5.0", "@npmcli/fs": "^3.1.1", + "@types/node": "^22.5.0", "cli-table3": "^0.6.5", "commander": "^12.1.0", "console-table-printer": "^2.12.1", "copy-paste": "^1.5.3", + "gradient-string": "^2.0.2", "jimp": "^0.22.12", "kleur": "^4.1.5", "lru-cache": "^11.0.0", "nanospinner": "^1.1.0", "prompts": "^2.4.2", - "qllm-lib": "1.8.5", + "qllm-lib": "^1.8.6", "readline": "^1.3.0", "screenshot-desktop": "^1.15.0", "table": "^6.8.2", diff --git a/packages/qllm-cli/src/chat/chat-config.ts b/packages/qllm-cli/src/chat/chat-config.ts index eee4348..9cc1051 100644 --- a/packages/qllm-cli/src/chat/chat-config.ts +++ b/packages/qllm-cli/src/chat/chat-config.ts @@ -1,4 +1,3 @@ -// packages/qllm-cli/src/chat/chat-config.ts import fs from "fs/promises"; import path from "path"; import os from "os"; @@ -63,7 +62,9 @@ export class ChatConfig { key: K, value: ChatConfigType[K] ): void { - this.config[key] = value; + const partialConfig = { [key]: value }; + const validatedConfig = ChatConfigSchema.partial().parse(partialConfig); + this.config[key] = validatedConfig[key] as ChatConfigType[K]; } public getProvider(): string | undefined { @@ -71,7 +72,7 @@ export class ChatConfig { } public setProvider(provider: string): void { - this.config.provider = provider; + this.set("provider", provider); } public getModel(): string | undefined { @@ -79,7 +80,7 @@ export class ChatConfig { } public setModel(model: string): void { - this.config.model = model; + this.set("model", model); } public getTemperature(): number | undefined { @@ -87,7 +88,7 @@ export class ChatConfig { } public setTemperature(temperature: number): void { - this.config.temperature = temperature; + this.set("temperature", temperature); } public getMaxTokens(): number | undefined { @@ -95,7 +96,7 @@ export class ChatConfig { } public setMaxTokens(maxTokens: number): void { - this.config.maxTokens = maxTokens; + this.set("maxTokens", maxTokens); } public getTopP(): number | undefined { @@ -103,7 +104,7 @@ export class ChatConfig { } public setTopP(topP: number): void { - this.config.topP = topP; + this.set("topP", topP); } public getFrequencyPenalty(): number | undefined { @@ -111,7 +112,7 @@ export class ChatConfig { } public setFrequencyPenalty(frequencyPenalty: number): void { - this.config.frequencyPenalty = frequencyPenalty; + this.set("frequencyPenalty", frequencyPenalty); } public getPresencePenalty(): number | undefined { @@ -119,7 +120,7 @@ export class ChatConfig { } public setPresencePenalty(presencePenalty: number): void { - this.config.presencePenalty = presencePenalty; + this.set("presencePenalty", presencePenalty); } public getStopSequence(): string[] | undefined { @@ -127,7 +128,7 @@ export class ChatConfig { } public setStopSequence(stopSequence: string[]): void { - this.config.stopSequence = stopSequence; + this.set("stopSequence", stopSequence); } public getCurrentConversationId(): string | undefined { @@ -135,7 +136,7 @@ export class ChatConfig { } public setCurrentConversationId(conversationId: string | undefined): void { - this.config.currentConversationId = conversationId; + this.set("currentConversationId", conversationId); } public async initialize(): Promise { @@ -147,4 +148,4 @@ export class ChatConfig { } } -export const chatConfig = ChatConfig.getInstance(); +export const chatConfig = ChatConfig.getInstance(); \ No newline at end of file diff --git a/packages/qllm-cli/src/chat/chat.ts b/packages/qllm-cli/src/chat/chat.ts index 27e99db..08e4a74 100644 --- a/packages/qllm-cli/src/chat/chat.ts +++ b/packages/qllm-cli/src/chat/chat.ts @@ -4,9 +4,9 @@ import { createConversationManager, getLLMProvider } from "qllm-lib"; import { ChatConfig } from "./chat-config"; import { MessageHandler } from "./message-handler"; import { CommandProcessor } from "./command-processor"; -import { IOManager } from "./io-manager"; +import { IOManager } from "../utils/io-manager"; import { ConfigManager } from "./config-manager"; -import { output } from "../utils/output"; +import { ioManager} from "../utils/io-manager"; import ImageManager from "./image-manager"; export class Chat { @@ -40,11 +40,11 @@ export class Chat { await this.config.initialize(); this.configManager.setProvider(this.providerName); this.configManager.setModel(this.modelName); - output.success( + ioManager.displaySuccess( `Chat initialized with ${this.providerName} provider and ${this.modelName} model.` ); } catch (error) { - output.error(`Failed to initialize chat: ${(error as Error).message}`); + ioManager.displayError(`Failed to initialize chat: ${(error as Error).message}`); process.exit(1); } } @@ -53,10 +53,10 @@ export class Chat { await this.initialize(); const conversation = await this.conversationManager.createConversation(); this.conversationId = conversation.id; - output.info( + ioManager.displayInfo( "Chat session started. Type your messages or use special commands." ); - output.info("Type /help for available commands."); + ioManager.displayInfo("Type /help for available commands."); this.promptUser(); } @@ -86,7 +86,7 @@ export class Chat { try { const [command, ...args] = input.trim().split(/\s+/); if (!command) { - output.error("No command provided"); + ioManager.displayError("No command provided"); return; } const cleanCommand = command.substring(1).toLowerCase(); @@ -100,7 +100,7 @@ export class Chat { }; await this.commandProcessor.processCommand(cleanCommand, args, context); } catch (error) { - output.error( + ioManager.displayError( "Error processing special command: " + (error instanceof Error ? error.message : String(error)) ); @@ -112,7 +112,7 @@ export class Chat { images: string[] ): Promise { if (!this.conversationId) { - output.error("No active conversation. Please start a chat first."); + ioManager.displayError("No active conversation. Please start a chat first."); return; } const currentProviderName = this.configManager.getProvider(); diff --git a/packages/qllm-cli/src/chat/command-processor.ts b/packages/qllm-cli/src/chat/command-processor.ts index f945718..b8a6532 100644 --- a/packages/qllm-cli/src/chat/command-processor.ts +++ b/packages/qllm-cli/src/chat/command-processor.ts @@ -2,7 +2,7 @@ import { ConversationManager } from "qllm-lib"; import { ChatConfig } from "./chat-config"; import { ConfigManager } from "./config-manager"; -import { IOManager } from "./io-manager"; +import { IOManager } from "../utils/io-manager"; import ImageManager from "./image-manager"; import { showHelp } from "./commands/show-help"; diff --git a/packages/qllm-cli/src/chat/commands/display-conversation.ts b/packages/qllm-cli/src/chat/commands/display-conversation.ts index 1756f77..a221c14 100644 --- a/packages/qllm-cli/src/chat/commands/display-conversation.ts +++ b/packages/qllm-cli/src/chat/commands/display-conversation.ts @@ -1,5 +1,5 @@ import { CommandContext } from "../command-processor"; -import { IOManager } from "../io-manager"; +import { IOManager } from "../../utils/io-manager"; import { ConversationMessage, ChatMessageContent, LLMOptions } from "qllm-lib"; const MESSAGES_PER_PAGE = 5; diff --git a/packages/qllm-cli/src/chat/config-manager.ts b/packages/qllm-cli/src/chat/config-manager.ts index e926514..103e5ab 100644 --- a/packages/qllm-cli/src/chat/config-manager.ts +++ b/packages/qllm-cli/src/chat/config-manager.ts @@ -1,7 +1,7 @@ // packages/qllm-cli/src/chat/config-manager.ts import { ChatConfig } from "./chat-config"; import { getLLMProvider } from "qllm-lib"; -import { output } from "../utils/output"; +import { ioManager } from "../utils/io-manager"; import { DEFAULT_PROVIDER, DEFAULT_MODEL } from "../constants"; export class ConfigManager { @@ -11,9 +11,11 @@ export class ConfigManager { try { await getLLMProvider(providerName); this.config.setProvider(providerName); - output.success(`Provider set to: ${providerName}`); + ioManager.displaySuccess(`Provider set to: ${providerName}`); } catch (error) { - output.error(`Failed to set provider: ${(error as Error).message}`); + ioManager.displayError( + `Failed to set provider: ${(error as Error).message}` + ); } } @@ -27,7 +29,7 @@ export class ConfigManager { setModel(modelName: string): void { this.config.setModel(modelName); - output.success(`Model set to: ${modelName}`); + ioManager.displaySuccess(`Model set to: ${modelName}`); } getModel(): string { @@ -56,11 +58,12 @@ export class ConfigManager { this.config.setStopSequence(value.split(",")); break; default: - output.error(`Unknown option: ${option}`); + ioManager.displayError(`Unknown option: ${option}`); this.showValidOptions(); return; } - output.success(`Option ${evalOption} set to: ${value}`); + + ioManager.displaySuccess(`Option ${evalOption} set to: ${value}`); } getAllSettings(): Record { @@ -77,7 +80,7 @@ export class ConfigManager { } private showValidOptions(): void { - output.info("Valid options are:"); + ioManager.displayInfo("Valid options are:"); [ "temperature", "max_tokens", @@ -85,7 +88,7 @@ export class ConfigManager { "frequency_penalty", "presence_penalty", "stop_sequence", - ].forEach((opt) => output.info(`- ${opt}`)); + ].forEach((opt) => ioManager.displayInfo(`- ${opt}`)); } async initialize(): Promise { diff --git a/packages/qllm-cli/src/chat/image-manager.ts b/packages/qllm-cli/src/chat/image-manager.ts index 6198d3c..a3777d0 100644 --- a/packages/qllm-cli/src/chat/image-manager.ts +++ b/packages/qllm-cli/src/chat/image-manager.ts @@ -1,7 +1,7 @@ // packages/qllm-cli/src/chat/image-manager.ts import { utils } from "./utils"; -import { output } from "../utils/output"; +import { ioManager } from "../utils/io-manager"; export class ImageManager { private images: Set; @@ -17,18 +17,18 @@ export class ImageManager { addImage(image: string): void { if (utils.isValidUrl(image) || utils.isImageFile(image)) { this.images.add(image); - output.success(`Image added: ${utils.truncateString(image, 50)}`); + ioManager.displaySuccess(`Image added: ${utils.truncateString(image, 50)}`); } else { - output.error("Invalid image URL or file path"); + ioManager.displayError("Invalid image URL or file path"); } } removeImage(image: string): boolean { const removed = this.images.delete(image); if (removed) { - output.success(`Image removed: ${utils.truncateString(image, 50)}`); + ioManager.displaySuccess(`Image removed: ${utils.truncateString(image, 50)}`); } else { - output.warn(`Image not found: ${utils.truncateString(image, 50)}`); + ioManager.displayWarning(`Image not found: ${utils.truncateString(image, 50)}`); } return removed; } @@ -48,20 +48,20 @@ export class ImageManager { clearImages(showOutput: boolean = true): void { const count = this.images.size; this.images.clear(); - output.success( + ioManager.displaySuccess( `Cleared ${count} image${count !== 1 ? "s" : ""} from the buffer` ); } displayImages(): void { if (this.images.size === 0) { - output.info("No images in the buffer"); + ioManager.displayInfo("No images in the buffer"); return; } - output.info(`Images in the buffer (${this.images.size}):`); + ioManager.displayInfo(`Images in the buffer (${this.images.size}):`); this.getImages().forEach((image, index) => { - output.info(`${index + 1}. ${utils.truncateString(image, 70)}`); + ioManager.displayInfo(`${index + 1}. ${utils.truncateString(image, 70)}`); }); } } diff --git a/packages/qllm-cli/src/chat/io-manager.ts b/packages/qllm-cli/src/chat/io-manager.ts deleted file mode 100644 index 262c865..0000000 --- a/packages/qllm-cli/src/chat/io-manager.ts +++ /dev/null @@ -1,278 +0,0 @@ -// packages/qllm-cli/src/chat/io-manager.ts -import readline from "readline"; -import kleur from "kleur"; -import { getBorderCharacters, table } from "table"; -import { createSpinner } from "nanospinner"; -import { Table } from "console-table-printer"; -import prompts from 'prompts'; - -interface Spinner { - start: () => void; - stop: () => void; - success: (options: { text: string }) => void; - error: (options: { text: string }) => void; -} - -export class IOManager { - private rl: readline.Interface; - private currentSpinner: Spinner | null = null; - - constructor() { - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - } - - async getUserInput(prompt: string): Promise { - const response = await prompts({ - type: 'text', - name: 'userInput', - message: this.colorize(prompt, "green"), - }); - return response.userInput; - } - - async getAsyncUserInput(prompt: string): Promise { - return this.getUserInput(prompt); - } - - displayUserMessage(message: string): void { - this.displayInfo(this.colorize(`You: ${message}`, "green")); - } - - displayAssistantMessage(message: string): void { - this.displayInfo(this.colorize(`Assistant: ${message}`, "blue")); - } - - displaySystemMessage(message: string): void { - this.displayInfo(this.colorize(`System: ${message}`, "yellow")); - } - - displayError(message: string): void { - console.error(this.colorize(`✖ ${message}`, "red")); - } - - displaySuccess(message: string): void { - console.log(this.colorize(`✔ ${message}`, "green")); - } - - displayWarning(message: string): void { - console.warn(this.colorize(`⚠ ${message}`, "yellow")); - } - - displayInfo(message: string): void { - console.log(message); - } - - displayTable(headers: string[], data: string[][]): void { - const tableData = [headers.map((h) => this.colorize(h, "cyan")), ...data]; - console.log( - table(tableData, { - border: getBorderCharacters("norc"), - columnDefault: { - paddingLeft: 0, - paddingRight: 1, - }, - drawHorizontalLine: () => false, - }) - ); - } - - displayList(items: string[]): void { - items.forEach((item) => this.displayInfo(`• ${item}`)); - } - - clearLine(): void { - process.stdout.write("\r\x1b[K"); - } - - newLine(): void { - console.log(); - } - - close(): void { - this.rl.close(); - } - - createSpinner(message: string): Spinner { - const spinner = createSpinner(message); - this.currentSpinner = spinner; - return spinner; - } - - stopSpinner(): void { - if (this.currentSpinner) { - this.currentSpinner.stop(); - this.currentSpinner = null; - } - } - - write(text: string): void { - process.stdout.write(text); - } - - displayConversationList( - conversations: { id: string; createdAt: Date }[] - ): void { - this.displayInfo("Conversations:"); - conversations.forEach((conv, index) => { - this.displayInfo( - `${index + 1}. ID: ${ - conv.id - }, Created: ${conv.createdAt.toLocaleString()}` - ); - }); - } - - displayConversationDetails( - id: string, - messages: { role: string; content: string }[] - ): void { - this.displayInfo(`Conversation ${id}:`); - messages.forEach((msg, index) => { - const roleColor = msg.role === "user" ? "green" : "blue"; - this.displayInfo( - `${index + 1}. ${this.colorize(msg.role, roleColor)}: ${msg.content}` - ); - }); - } - - async promptForConversationSelection(): Promise { - const response = await prompts({ - type: 'text', - name: 'selection', - message: 'Enter conversation number to select (or \'c\' to cancel): ', - }); - return response.selection; - } - - async promptForConversationDeletion(): Promise { - const response = await prompts({ - type: 'text', - name: 'selection', - message: 'Enter conversation number to delete (or \'c\' to cancel): ', - }); - return response.selection; - } - - async confirmAction(message: string): Promise { - const response = await prompts({ - type: 'confirm', - name: 'confirmed', - message: message, - initial: false, - }); - return response.confirmed; - } - - colorize(text: string, color: string): string { - switch (color) { - case "green": - return kleur.green(text); - case "blue": - return kleur.blue(text); - case "yellow": - return kleur.yellow(text); - case "red": - return kleur.red(text); - case "cyan": - return kleur.cyan(text); - default: - return text; - } - } - - displayGroupedInfo(title: string, items: string[]): void { - this.displayInfo(this.colorize(`\n${title}:`, "yellow")); - items.forEach((item) => this.displayInfo(` ${item}`)); - } - - displayTitle(title: string): void { - console.log(this.colorize(title, "cyan")); - } - - displayCodeBlock(code: string, language?: string): void { - const formattedCode = language ? this.colorize(code, "cyan") : code; - console.log(this.colorize("```" + (language || ""), "gray")); - console.log(formattedCode); - console.log(this.colorize("```", "gray")); - } - - clear(): void { - console.clear(); - } - - displaySectionHeader(header: string): void { - console.log(kleur.bold().yellow(`\n${header}`)); - } - - displayModelTable(models: { id: string; description: string }[]): void { - const p = new Table({ - columns: [ - { name: "id", alignment: "left", color: "cyan" }, - { name: "description", alignment: "left", color: "white" }, - ], - sort: (row1, row2) => row1.id.localeCompare(row2.id), - }); - - models.forEach((model) => { - p.addRow({ id: model.id, description: model.description || "N/A" }); - }); - - p.printTable(); - } - - displayConfigOptions(options: Array<{ name: string; value: any }>): void { - this.displaySectionHeader("Current Configuration"); - - const longestNameLength = Math.max( - ...options.map((opt) => opt.name.length) - ); - - options.forEach(({ name, value }) => { - const paddedName = name.padEnd(longestNameLength); - const formattedValue = value !== undefined ? value.toString() : "Not set"; - const coloredValue = - value !== undefined - ? this.colorize(formattedValue, "green") - : this.colorize(formattedValue, "yellow"); - this.displayInfo(`${this.colorize(paddedName, "cyan")}: ${coloredValue}`); - }); - - this.newLine(); - this.displayInfo( - this.colorize("Use 'set