capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
232 lines (231 loc) ⢠8.66 kB
JavaScript
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