capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
306 lines • 13.3 kB
JavaScript
import chalk from 'chalk';
import { configManager } from '../core/config.js';
import { authService } from './auth.js';
import { stateService } from './state.js';
import { contextManager } from './context.js';
import { chatService } from './chat.js';
import { toolExecutor } from '../tools/executor.js';
import { toolResultsService } from './tool-results.js';
import { ModelResolver } from './model-resolver.js';
import { openRouterModelsService } from './openrouter-models.js';
import { ProviderFactory } from '../providers/factory.js';
import { providerRegistry } from '../providers/base.js';
import { registerBuiltinToolsForTaskMode } from '../tools/builtin/index.js';
export async function executeTaskMode(options) {
const unhandledRejectionHandler = (reason, promise) => {
console.error(chalk.red('\n[Unhandled Promise Rejection]'), reason);
};
const uncaughtExceptionHandler = (error) => {
console.error(chalk.red('\n[Uncaught Exception]'), error);
console.error(chalk.red('[Stack trace]'), error.stack);
};
const signalHandler = (signal) => {
console.error(chalk.red(`\n[Process received ${signal}]`));
};
process.on('SIGTERM', () => signalHandler('SIGTERM'));
process.on('SIGINT', () => signalHandler('SIGINT'));
process.on('unhandledRejection', unhandledRejectionHandler);
process.on('uncaughtException', uncaughtExceptionHandler);
try {
await initializeServices();
const authStatus = await authService.getStatus();
if (!authStatus.isAuthenticated) {
const error = 'Capsule must be activated to use task mode.\n' +
'Purchase at: https://capsulecli.dev\n' +
'Then run: capsule and use /activate';
if (options.json) {
console.log(JSON.stringify({ success: false, error }, null, 2));
}
else {
console.error(chalk.red('Error: ') + error);
}
return { success: false, error };
}
const config = configManager.getConfig();
if (!config.providers.openrouter?.apiKey) {
const error = 'OpenRouter API key required.\n' +
'Get your key at: https://openrouter.ai/keys\n' +
'Then run: capsule and use /keys';
if (options.json) {
console.log(JSON.stringify({ success: false, error }, null, 2));
}
else {
console.error(chalk.red('Error: ') + error);
}
return { success: false, error };
}
let provider = options.provider || stateService.getProvider();
let model = options.model || stateService.getModel();
if (options.provider) {
const resolvedProvider = ModelResolver.resolveProvider(options.provider);
if (!resolvedProvider) {
const error = `Provider "${options.provider}" not found`;
if (options.json) {
console.log(JSON.stringify({ success: false, error }, null, 2));
}
else {
console.error(chalk.red('Error: ') + error);
}
return { success: false, error };
}
provider = resolvedProvider;
}
if (options.model) {
const resolvedModel = ModelResolver.resolveModel(options.model, provider);
if (!resolvedModel) {
const error = `Model "${options.model}" not found for provider ${provider}`;
if (options.json) {
console.log(JSON.stringify({ success: false, error }, null, 2));
}
else {
console.error(chalk.red('Error: ') + error);
}
return { success: false, error };
}
model = resolvedModel;
}
const validation = ModelResolver.validateModel(model);
if (!validation.valid) {
const error = `Model "${model}" cannot be used: ${validation.reason}`;
if (options.json) {
console.log(JSON.stringify({ success: false, error }, null, 2));
}
else {
console.error(chalk.red('Error: ') + error);
}
return { success: false, error };
}
if (!options.json) {
console.log(chalk.cyan('╭' + '─'.repeat(58) + '╮'));
console.log(chalk.cyan('│') + chalk.bold(' 🚀 Executing Task'.padEnd(58)) + chalk.cyan('│'));
console.log(chalk.cyan('├' + '─'.repeat(58) + '┤'));
console.log(chalk.cyan('│') + chalk.gray(' Provider: ') + provider.padEnd(48) + chalk.cyan('│'));
console.log(chalk.cyan('│') + chalk.gray(' Model: ') + model.padEnd(51) + chalk.cyan('│'));
console.log(chalk.cyan('╰' + '─'.repeat(58) + '╯'));
console.log();
}
const taskContext = contextManager.createNewContext();
const previousContext = contextManager.getCurrentContext();
contextManager.setCurrentContext(taskContext.id);
const originalProvider = stateService.getProvider();
const originalModel = stateService.getModel();
stateService.setProvider(provider);
stateService.setModel(model);
try {
const result = await executeTask(options.task, options);
stateService.setProvider(originalProvider);
stateService.setModel(originalModel);
contextManager.setCurrentContext(previousContext.id);
if (options.json) {
const output = {
success: result.success,
task: options.task,
provider: provider,
model: model,
duration: result.duration,
tokensUsed: result.tokensUsed || 0,
toolsExecuted: result.toolsExecuted || [],
output: result.output || '',
error: result.error
};
console.log(JSON.stringify(output, null, 2));
}
else {
if (result.success) {
console.log('\n' + chalk.green('═'.repeat(60)));
console.log(chalk.green('✓ Task completed successfully'));
console.log(chalk.green('═'.repeat(60)));
console.log(chalk.gray('Duration: ') + result.duration);
console.log(chalk.gray('Tokens: ') + (result.tokensUsed || 0));
if (result.toolsExecuted && result.toolsExecuted.length > 0) {
console.log(chalk.gray('Tools used: ') + result.toolsExecuted.join(', '));
}
}
else {
console.error('\n' + chalk.red('═'.repeat(60)));
console.error(chalk.red('✗ Task failed'));
console.error(chalk.red('═'.repeat(60)));
console.error(chalk.red('Error: ') + (result.error || 'Unknown error'));
}
}
return { success: result.success, output: result.output, error: result.error };
}
catch (error) {
stateService.setProvider(originalProvider);
stateService.setModel(originalModel);
contextManager.setCurrentContext(previousContext.id);
throw error;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
if (options.json) {
console.log(JSON.stringify({ success: false, error: errorMessage }, null, 2));
}
else {
console.error(chalk.red('Error: ') + errorMessage);
}
return { success: false, error: errorMessage };
}
finally {
process.removeListener('unhandledRejection', unhandledRejectionHandler);
process.removeListener('uncaughtException', uncaughtExceptionHandler);
process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGINT');
}
}
async function initializeServices() {
await configManager.loadConfig();
const config = configManager.getConfig();
if (config.providers?.openrouter?.apiKey) {
await openRouterModelsService.fetchModels(false, true);
const supportedProviders = openRouterModelsService.getAvailableProviders();
for (const providerName of supportedProviders) {
try {
const provider = await ProviderFactory.create(providerName);
providerRegistry.register(provider);
}
catch {
}
}
}
registerBuiltinToolsForTaskMode();
}
async function executeTask(task, options) {
const startTime = Date.now();
try {
let fullOutput = '';
let tokensUsed = 0;
const toolsExecuted = [];
let isFirstOutput = true;
if (!options.json) {
process.stdout.write(chalk.yellow('⠋ ') + chalk.gray('Thinking...'));
}
const stream = chatService.stream(task, {
mode: 'agent'
});
let timeoutId;
if (options.timeout) {
timeoutId = setTimeout(() => {
chatService.abort();
}, options.timeout * 1000);
}
const pendingToolCalls = new Set();
for await (const chunk of stream) {
if (chunk.delta) {
if (!options.json && isFirstOutput) {
process.stdout.write('\r' + ' '.repeat(20) + '\r');
isFirstOutput = false;
}
fullOutput += chunk.delta;
if (!options.json) {
process.stdout.write(chunk.delta);
}
}
if (chunk.usage) {
tokensUsed = chunk.usage.totalTokens;
}
if (chunk.toolCall) {
const toolName = chunk.toolCall.name;
pendingToolCalls.add(chunk.toolCall.id);
if (!toolsExecuted.includes(toolName)) {
toolsExecuted.push(toolName);
}
if (!options.json) {
console.log(chalk.yellow(`\n\n🔧 Executing tool: ${chalk.bold(toolName)}`));
process.stdout.write(chalk.gray(' '));
}
try {
const execution = await toolExecutor.execute(chunk.toolCall, {
requireApproval: false,
workingDirectory: process.cwd()
});
const toolResult = {
call_id: chunk.toolCall.id,
output: execution.result?.output || execution.result?.error || 'Unknown error',
success: execution.state === 'completed',
name: chunk.toolCall.name
};
toolResultsService.setToolResult(chunk.toolCall.id, toolResult);
pendingToolCalls.delete(chunk.toolCall.id);
if (execution.result?.error) {
if (!options.json) {
console.log(chalk.red(`✗ Failed: ${execution.result.error}`));
}
}
else if (!options.json) {
console.log(chalk.green(`✓ Completed`));
}
}
catch (toolError) {
const errorResult = {
call_id: chunk.toolCall.id,
output: { error: toolError instanceof Error ? toolError.message : String(toolError) },
success: false,
name: chunk.toolCall.name
};
toolResultsService.setToolResult(chunk.toolCall.id, errorResult);
pendingToolCalls.delete(chunk.toolCall.id);
if (!options.json) {
console.log(chalk.red(`✗ Error: ${toolError}`));
}
}
if (!options.json) {
console.log();
}
}
}
if (timeoutId) {
clearTimeout(timeoutId);
}
const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
return {
success: true,
output: fullOutput.trim(),
duration,
tokensUsed,
toolsExecuted
};
}
catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
if (options.timeout && error instanceof Error && error.message.includes('aborted')) {
return {
success: false,
error: `Task timed out after ${options.timeout} seconds`,
duration
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
duration
};
}
}
//# sourceMappingURL=task-executor.js.map