cmte
Version:
Design by Committee™ except it's just you and LLMs
274 lines (243 loc) • 13.3 kB
JavaScript
// 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);