UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

274 lines (243 loc) 13.3 kB
#!/usr/bin/env node // Configure logging before any other imports import { logger, configureLogger } from '../utils/logger.js'; logger.silent = true; // Ensure silence from the start // Now import everything else import { Command } from 'commander'; import path from 'path'; import dotenv from 'dotenv'; import fs from 'fs'; // Use standard fs for readFileSync import { ComponentRegistry } from '../core/components/registry.js'; import { WorkflowExecutor } from '../core/execution/workflow-executor.js'; import getClaudeClient from '../core/llm/claude-adapter.js'; // Import LLM clients import getLocalLLMClient from '../core/llm/local-llm-adapter.js'; import readline from 'readline/promises'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; async function promptForReview(files) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question( `The following files are not marked as reviewed:\n${files.map(f => `- ${f}`).join('\n')}\n\nDo you want to mark them reviewed and continue? [Y/n] `, (answer) => { rl.close(); resolve(answer.toLowerCase() !== 'n'); } ); }); } // Helper function to find the workflow file path (yaml or yml) in a directory async function findWorkflowFile(dirPath) { const yamlPath = path.join(dirPath, 'workflow.yaml'); const ymlPath = path.join(dirPath, 'workflow.yml'); try { if ((await fs.promises.stat(yamlPath)).isFile()) return yamlPath; } catch (e) { /* ignore */ } try { if ((await fs.promises.stat(ymlPath)).isFile()) return ymlPath; } catch (e) { /* ignore */ } return null; // Not found } // --- Helper: Get Package Version --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); let version = 'unknown'; try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); version = packageJson.version; } catch (error) { console.error('Error reading package.json:', error); } // --- CLI Setup --- const program = new Command(); program .name('cmte') .description('Committee CLI - Run LLM workflows defined in YAML') .version(version) .argument('[workflowDirs...]', 'Paths to workflow directories') .option('-x, --local', 'Use local LLM instead of cloud API') .option('-l, --lite', 'Use a lightweight/faster model (e.g., Haiku)') .option('-p, --prompts', 'Save rendered prompts and responses') .option('--dryrun', 'Simulate workflow without LLM calls or output') .option('--apidryrun', 'Simulate API calls with minimal placeholders') .option('--mocktasks', 'Mock task execution, useful for testing structure') .option('--state <file>', 'Load initial state from a JSON file') .option('--debug', 'Enable debug logging to console and file') .action(async (workflowDirs, options) => { // --- Configure Logger FIRST --- configureLogger(options); logger.silent = false; // Enable logging now logger.debug('CLI Action Started', { args: workflowDirs, options }); if (!workflowDirs || workflowDirs.length === 0) { logger.error('Error: No workflow directories specified.'); program.help(); process.exit(1); } // --- BEGIN RESTORED WORKFLOW EXECUTION LOGIC --- try { // --- Load .env Variables --- const cwdEnvPath = path.resolve(process.cwd(), '.env'); try { await fs.promises.stat(cwdEnvPath); logger.debug(`Loading .env file from current working directory: ${cwdEnvPath}`); dotenv.config({ path: cwdEnvPath }); } catch (error) { if (error.code === 'ENOENT') { logger.debug('No .env file found in current working directory.'); } else { logger.warn(`Error checking for .env file in CWD: ${error.message}`); } } // --- Determine Shared LLM Configuration --- let modelName = process.env.DEFAULT_MODEL; if (options.local) { modelName = process.env.LOCAL_LLM_MODEL || modelName; } if (!modelName && !options.local && process.env.ANTHROPIC_API_KEY) { modelName = 'claude-3-5-sonnet-20240620'; logger.warn(`DEFAULT_MODEL environment variable not set. Using hard default: ${modelName}`); } else if (!modelName && options.local && !process.env.LOCAL_LLM_URL) { throw new Error('Using --local but LLM Model/URL not configured. Set LOCAL_LLM_MODEL and/or LOCAL_LLM_URL in environment.'); } const sharedModelConfig = { model: modelName }; const useLocalLLM = options.local ?? false; // Initialize the appropriate LLM client ONCE const llmClient = useLocalLLM ? getLocalLLMClient(sharedModelConfig) : (process.env.ANTHROPIC_API_KEY ? getClaudeClient(sharedModelConfig) : null); if (!llmClient) { throw new Error('Could not initialize LLM client. Ensure ANTHROPIC_API_KEY (for Claude) or LOCAL_LLM_URL (for --local) is set in your environment.'); } logger.info(`Using LLM Client: ${useLocalLLM ? 'Local' : 'Claude'}, Model: ${sharedModelConfig.model || 'Default'}`); // --- Process Each Workflow Argument --- const isMultiRun = workflowDirs.length > 1; const workflowExecutionPromises = []; for (const arg of workflowDirs) { const resolvedArgPath = path.resolve(process.cwd(), arg); let workflowFilePath = null; let workflowDir = null; try { const stats = await fs.promises.stat(resolvedArgPath); if (stats.isDirectory()) { workflowDir = resolvedArgPath; workflowFilePath = await findWorkflowFile(workflowDir); if (!workflowFilePath) { logger.error(`Workflow file (workflow.yaml/yml) not found in directory: ${workflowDir}. Skipping.`); continue; } } else if (stats.isFile()) { workflowFilePath = resolvedArgPath; workflowDir = path.dirname(workflowFilePath); } else { logger.error(`Argument is not a file or directory: ${resolvedArgPath}. Skipping.`); continue; } } catch (err) { logger.error(`Error accessing path: ${resolvedArgPath}. Skipping.`, { error: err.message }); continue; } logger.info(`Processing workflow: ${workflowFilePath}`); const registry = new ComponentRegistry(workflowDir); const workflowExecutor = new WorkflowExecutor({ registry, workflowPath: workflowFilePath, lite: options.lite || false, useLocalLLM: useLocalLLM, savePrompts: options.prompts || false, dryRun: options.dryrun || false, apiDryRun: options.apidryrun || false, // Pass option mockTaskExecution: options.mocktasks || false, // Pass option modelConfig: sharedModelConfig, llmClient: llmClient, isMultiRun: isMultiRun }); const executionPromise = (async () => { try { const workflowConfig = await registry.loadWorkflow(workflowFilePath); const result = await workflowExecutor.executeWorkflow(workflowConfig); logger.info(`Successfully finished workflow: ${workflowFilePath}`); return { path: workflowFilePath, status: result.status, error: result.error, dryRunIssues: result.dryRunIssues }; } catch (error) { logger.error(`Workflow execution failed critically for: ${workflowFilePath}`, { error: error.message }); return { path: workflowFilePath, status: 'failed', error: error.message, dryRunIssues: workflowExecutor.dryRunIssues || [] }; } })(); workflowExecutionPromises.push(executionPromise); } // --- Wait for all workflows and report results --- logger.info(`Waiting for ${workflowExecutionPromises.length} workflow(s) to complete...`); const results = await Promise.allSettled(workflowExecutionPromises); logger.info('\n--- Workflow Execution Summary ---'); let successCount = 0; let failureCount = 0; let allDryRunIssues = []; // Collect issues from all workflows results.forEach(result => { const isFulfilled = result.status === 'fulfilled'; const workflowResult = isFulfilled ? result.value : null; const workflowPath = isFulfilled ? workflowResult.path : '[Unknown Workflow]'; const executionFailed = isFulfilled ? (workflowResult?.status === 'failed') : true; const systemErrorReason = !isFulfilled ? (result.reason?.message || result.reason) : null; const workflowDryRunIssues = (isFulfilled && workflowResult?.dryRunIssues?.length > 0) ? workflowResult.dryRunIssues : null; if (isFulfilled && !executionFailed && !workflowDryRunIssues) { logger.info(`✅ SUCCESS: ${workflowPath}`); successCount++; } else { failureCount++; // Mark as failure if execution failed OR dry run had issues if (isFulfilled && executionFailed) { logger.error(`❌ FAILED: ${workflowPath} - ${workflowResult.error}`); } else if (workflowDryRunIssues) { logger.error(`❌ FAILED (DRY RUN ISSUES): ${workflowPath}`); // Add context (workflow path) to each issue before collecting allDryRunIssues.push(...workflowDryRunIssues.map(issue => ({ ...issue, workflowPath }))); } else if (systemErrorReason) { logger.error(`❌ SYSTEM ERROR processing workflow ${workflowPath}: ${systemErrorReason}`); } } }); logger.info(`--------------------------------`); logger.info(`Total Workflows: ${results.length}, Success: ${successCount}, Failed: ${failureCount}`); // --- Print Consolidated Dry Run Issues Summary --- if (allDryRunIssues.length > 0) { console.log('\n'); // Add spacing // Keep header red and bold via logger.error logger.error(chalk.red.bold('======== DRY RUN ISSUES SUMMARY ========')); allDryRunIssues.forEach(issue => { const issuePrefix = ` - [${issue.type.toUpperCase()}]`; const messageBody = ` in ${path.basename(issue.workflowPath || 'unknown')}: ${issue.message}`; if (issue.type === 'error') { // Print prefix in red, message in default color, using console.error console.error(chalk.red(issuePrefix) + messageBody); } else { // Print prefix in yellow, message in default color, using console.warn console.warn(chalk.yellow(issuePrefix) + messageBody); } }); // Keep footer red and bold via logger.error logger.error(chalk.red.bold('====== END DRY RUN ISSSUES SUMMARY ======')); console.log('\n'); // Add spacing } if (failureCount > 0) { process.exitCode = 1; } // --- END RESTORED WORKFLOW EXECUTION LOGIC --- } catch (error) { logger.error(`Critical setup or execution error: ${error.message}`); if (options.debug && error.stack) { logger.error(error.stack); } process.exit(1); } logger.debug('--- CMTE main finished ---'); }); program.parse(process.argv);