UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

232 lines (231 loc) • 8.66 kB
import readline from 'readline/promises'; import { stdin as input, stdout as output } from 'process'; import chalk from 'chalk'; import ora from 'ora'; import { select, confirm } from '@inquirer/prompts'; import { stateService } from '../services/state.js'; import { contextManager } from '../services/context.js'; import { chatService } from '../services/chat.js'; import { providerRegistry } from '../providers/base.js'; import { FileHandlerInstance } from '../services/file-handler.js'; export class StandardCLI { rl; fileHandler = new FileHandlerInstance(); running = true; constructor() { this.rl = readline.createInterface({ input, output, prompt: this.getPrompt() }); } getPrompt() { const mode = stateService.getMode(); const icons = { chat: 'šŸ’¬', agent: 'šŸ¤–', plan: 'šŸ“‹', fusion: 'šŸ”®' }; const icon = icons[mode] || 'šŸ’¬'; const attachCount = this.fileHandler.getAttachedFiles().length; const attach = attachCount > 0 ? chalk.yellow(` [${attachCount}šŸ“Ž]`) : ''; return chalk.cyan(`${icon}${attach} › `); } updatePrompt() { this.rl.setPrompt(this.getPrompt()); } showHelp() { console.log(` ${chalk.yellow('šŸ“š Commands:')} ${chalk.cyan('/help')} Show this help ${chalk.cyan('/mode')} Switch mode (chat/agent/plan/fusion) ${chalk.cyan('/provider')} Switch AI provider ${chalk.cyan('/model')} Switch AI model ${chalk.cyan('/new')} Start new conversation ${chalk.cyan('/clear')} Clear conversation ${chalk.cyan('/attach')} Attach files (/attach file1 file2...) ${chalk.cyan('/files')} List attached files ${chalk.cyan('/exit')} Exit application ${chalk.dim('šŸ’” Just type to chat, Ctrl+C to exit')} `); } async handleCommand(line) { const [cmd, ...args] = line.slice(1).split(/\s+/); switch (cmd) { case 'help': case 'h': this.showHelp(); break; case 'exit': case 'quit': this.running = false; break; case 'mode': const mode = await select({ message: 'Select mode:', choices: [ { name: 'šŸ’¬ Chat', value: 'chat' }, { name: 'šŸ¤– Agent', value: 'agent' }, { name: 'šŸ“‹ Plan', value: 'plan' }, { name: 'šŸ”® Fusion', value: 'fusion' } ] }); stateService.setMode(mode); console.log(chalk.green(`āœ“ Switched to ${mode} mode`)); this.updatePrompt(); break; case 'provider': case 'p': const providers = providerRegistry.getProviders(); const provider = await select({ message: 'Select provider:', choices: providers.map(p => ({ name: p.name, value: p.name.toLowerCase() })) }); stateService.setProvider(provider); console.log(chalk.green(`āœ“ Switched to ${provider}`)); break; case 'model': case 'm': const currentProvider = providerRegistry.getProvider(stateService.getProvider()); if (currentProvider) { const models = await currentProvider.getAvailableModels(); const model = await select({ message: 'Select model:', choices: models.map(m => ({ name: m.name, value: m.id })) }); stateService.setModel(model); console.log(chalk.green(`āœ“ Switched to ${model}`)); } break; case 'new': case 'n': contextManager.newContext(); console.log(chalk.green('āœ“ Started new conversation')); break; case 'clear': case 'c': if (await confirm({ message: 'Clear conversation?', default: false })) { contextManager.clearContext(); console.log(chalk.green('āœ“ Conversation cleared')); } break; case 'attach': case 'a': if (args.length === 0) { console.log(chalk.yellow('Usage: /attach <file1> [file2] ...')); } else { for (const file of args) { try { await this.fileHandler.attachFile(file); console.log(chalk.green(`āœ“ Attached ${file}`)); } catch (error) { console.log(chalk.red(`āœ— Failed: ${error.message}`)); } } this.updatePrompt(); } break; case 'files': case 'f': const files = this.fileHandler.getAttachedFiles(); if (files.length === 0) { console.log(chalk.gray('No files attached')); } else { console.log(chalk.yellow('\nAttached files:')); files.forEach((file, i) => { console.log(` ${i + 1}. ${file.name}`); }); } break; default: console.log(chalk.red(`Unknown command: /${cmd}`)); console.log(chalk.dim('Type /help for commands')); } } async sendMessage(message) { const spinner = ora('Thinking...').start(); try { let content; const attachedFiles = this.fileHandler.getAttachedFiles(); if (attachedFiles.length > 0) { content = [ { type: 'text', text: message }, ...attachedFiles.map(file => ({ type: 'file', filename: file.name, media_type: file.type, data: file.content })) ]; this.fileHandler.clearFiles(); this.updatePrompt(); } else { content = message; } const response = await chatService.sendMessage(content); spinner.stop(); console.log('\n' + chalk.green('Assistant:'), response.content); if (response.usage) { console.log(chalk.dim(`\n[Tokens: ${response.usage.totalTokens}]`)); } console.log(); } catch (error) { spinner.stop(); console.error(chalk.red('\nError:'), error.message + '\n'); } } async start() { console.log(chalk.cyan('\nšŸš€ Welcome to Capsule CLI!\n')); console.log(chalk.gray('Type /help for commands or just start chatting')); console.log(chalk.gray(`Using ${stateService.getProvider()}/${stateService.getModel()}\n`)); this.rl.on('line', async (line) => { const trimmed = line.trim(); if (!trimmed) { this.rl.prompt(); return; } if (trimmed.startsWith('/')) { await this.handleCommand(trimmed); } else { await this.sendMessage(trimmed); } if (this.running) { this.rl.prompt(); } else { this.rl.close(); } }); this.rl.on('close', () => { console.log(chalk.yellow('\nšŸ‘‹ Goodbye!\n')); process.exit(0); }); this.rl.on('SIGINT', async () => { console.log(); const shouldExit = await confirm({ message: 'Exit Capsule CLI?', default: false }); if (shouldExit) { this.rl.close(); } else { this.rl.prompt(); } }); this.rl.prompt(); } } export async function startStandardCLI() { const cli = new StandardCLI(); await cli.start(); } //# sourceMappingURL=standard-cli.js.map