#!/usr/bin/env node /** * Venture Capitalist - Investment Analysis CLI */ import { Command } from 'commander'; import * as fs from 'fs'; import * as path from 'path'; import * as dotenv from 'dotenv'; import { InvestmentAnalyzer } from '../core/analyzer'; import { PitchDeck, AnalysisConfig, ReportOptions } from '../core/types'; import chalk from 'chalk'; import ora from 'ora'; // Load environment variables dotenv.config(); const program = new Command(); program .name('vc') .description('Venture Capitalist - AI-powered investment analysis assistant') .version('1.0.0'); /** * analyze command - Analyze a pitch deck */ program .command('analyze ') .description('Analyze a business plan or pitch deck') .option('-o, --output ', 'Output file path') .option('-f, --format ', 'Output format (markdown, json)', 'markdown') .action(async (file: string, options) => { const spinner = ora('Analyzing pitch deck...').start(); try { // Load pitch deck const pitchDeck = await loadPitchDeck(file); // Load configuration const config = loadConfig(); // Create analyzer const analyzer = new InvestmentAnalyzer(config); // Perform analysis spinner.text = 'Performing investment analysis...'; const analysis = await analyzer.analyze(pitchDeck); spinner.succeed('Analysis complete!'); // Display results console.log('\n' + chalk.bold.cyan('═'.repeat(60))); console.log(chalk.bold.white(' INVESTMENT ANALYSIS REPORT')); console.log(chalk.bold.cyan('═'.repeat(60)) + '\n'); console.log(chalk.bold('Company:'), analysis.company.name); console.log(chalk.bold('Stage:'), analysis.company.stage || 'N/A'); console.log(chalk.bold('Industry:'), analysis.company.industry || 'N/A'); console.log(chalk.bold('Analysis Date:'), analysis.analysisDate.toLocaleDateString()); console.log(chalk.bold('Overall Score:'), getScoreColor(analysis.overallScore)); console.log( chalk.bold('Recommendation:'), getRecommendationColor(analysis.recommendation.decision) ); console.log('\n' + chalk.bold.yellow('Executive Summary:')); console.log(analysis.executiveSummary); console.log('\n' + chalk.bold.green('✓ Highlights:')); analysis.highlights.forEach((h) => console.log(` • ${h}`)); console.log('\n' + chalk.bold.red('⚠ Concerns:')); analysis.concerns.forEach((c) => console.log(` • ${c}`)); console.log('\n' + chalk.bold('Detailed Scores:')); console.log(` Market: ${getScoreColor(analysis.scores.market)}`); console.log(` Team: ${getScoreColor(analysis.scores.team)}`); console.log(` Product: ${getScoreColor(analysis.scores.product)}`); console.log(` Business Model: ${getScoreColor(analysis.scores.businessModel)}`); console.log(` Traction: ${getScoreColor(analysis.scores.traction)}`); console.log(` Financials: ${getScoreColor(analysis.scores.financials)}`); console.log(` Competition: ${getScoreColor(analysis.scores.competition)}`); console.log(` Risks: ${getScoreColor(analysis.scores.risks)}`); console.log('\n' + chalk.bold('Investment Thesis:')); console.log(analysis.recommendation.investmentThesis); console.log('\n' + chalk.bold('Next Steps:')); analysis.recommendation.nextSteps.forEach((step, i) => { console.log(` ${i + 1}. ${step}`); }); // Save output if requested if (options.output) { const outputContent = options.format === 'json' ? JSON.stringify(analysis, null, 2) : formatAnalysisAsMarkdown(analysis); fs.writeFileSync(options.output, outputContent); console.log(chalk.green(`\n✓ Analysis saved to: ${options.output}`)); } console.log('\n' + chalk.bold.cyan('═'.repeat(60)) + '\n'); } catch (error: any) { spinner.fail('Analysis failed'); console.error(chalk.red(`\n❌ Error: ${error.message}\n`)); process.exit(1); } }); /** * report command - Generate comprehensive investment report */ program .command('report ') .description('Generate detailed investment analysis report') .option('-f, --format ', 'Output format (markdown, pdf, docx)', 'markdown') .option('-t, --template ', 'Report template') .option('-s, --sections ', 'Comma-separated sections to include') .option('-o, --output ', 'Output file path') .action(async (file: string, options: ReportOptions) => { const spinner = ora('Generating investment report...').start(); try { const pitchDeck = await loadPitchDeck(file); const config = loadConfig(); const analyzer = new InvestmentAnalyzer(config); spinner.text = 'Analyzing investment opportunity...'; const analysis = await analyzer.analyze(pitchDeck); spinner.text = 'Generating report...'; const report = formatAnalysisAsMarkdown(analysis); const outputPath = options.output || path.join( config.outputDirectory, `${analysis.company.name}-analysis-${Date.now()}.md` ); // Ensure output directory exists const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(outputPath, report); spinner.succeed('Report generated successfully!'); console.log(chalk.green(`\n✓ Report saved to: ${outputPath}\n`)); } catch (error: any) { spinner.fail('Report generation failed'); console.error(chalk.red(`\n❌ Error: ${error.message}\n`)); process.exit(1); } }); /** * extract command - Extract specific information */ program .command('extract ') .description('Extract specific information from pitch deck') .action(async (file: string, field: string) => { const spinner = ora(`Extracting ${field}...`).start(); try { const pitchDeck = await loadPitchDeck(file); const config = loadConfig(); const analyzer = new InvestmentAnalyzer(config); const analysis = await analyzer.analyze(pitchDeck); spinner.succeed(`Extracted ${field}`); console.log('\n' + chalk.bold.cyan('═'.repeat(60))); switch (field) { case 'market-size': console.log(chalk.bold('Market Size Analysis:\n')); console.log(`TAM: ${analysis.market.tam}`); console.log(`SAM: ${analysis.market.sam}`); console.log(`SOM: ${analysis.market.som}`); console.log(`Growth Rate: ${analysis.market.growthRate}`); break; case 'team': console.log(chalk.bold('Team Information:\n')); analysis.team.founders.forEach((founder, i) => { console.log(`${i + 1}. ${founder.name} - ${founder.title}`); console.log(` Background: ${founder.background}`); console.log(` Experience: ${founder.experience}`); }); break; case 'financials': console.log(chalk.bold('Financial Information:\n')); console.log(`Current Revenue: ${analysis.financials.currentRevenue}`); console.log(`Burn Rate: ${analysis.financials.burnRate}`); console.log(`Runway: ${analysis.financials.runway}`); break; case 'traction': console.log(chalk.bold('Traction Metrics:\n')); console.log(`Users: ${analysis.traction.users}`); console.log(`Revenue: ${analysis.traction.revenue}`); console.log(`Growth Rate: ${analysis.traction.growthRate}`); break; case 'ask': console.log(chalk.bold('Investment Terms:\n')); console.log(`Funding Ask: ${analysis.terms.fundingAsk}`); console.log(`Valuation (Pre): ${analysis.terms.valuation.preMoney || 'N/A'}`); console.log(`Valuation (Post): ${analysis.terms.valuation.postMoney || 'N/A'}`); break; default: console.log(chalk.yellow(`Field "${field}" not recognized`)); } console.log('\n' + chalk.bold.cyan('═'.repeat(60)) + '\n'); } catch (error: any) { spinner.fail('Extraction failed'); console.error(chalk.red(`\n❌ Error: ${error.message}\n`)); process.exit(1); } }); /** * compare command - Compare multiple deals */ program .command('compare ') .description('Compare multiple investment opportunities') .option('-o, --output ', 'Output file path') .action(async (files: string[], options) => { const spinner = ora('Comparing deals...').start(); try { const config = loadConfig(); const analyzer = new InvestmentAnalyzer(config); const analyses = []; for (const file of files) { spinner.text = `Analyzing ${path.basename(file)}...`; const pitchDeck = await loadPitchDeck(file); const analysis = await analyzer.analyze(pitchDeck); analyses.push({ file, analysis }); } spinner.succeed('Comparison complete!'); // Display comparison console.log('\n' + chalk.bold.cyan('═'.repeat(80))); console.log(chalk.bold.white(' DEAL COMPARISON')); console.log(chalk.bold.cyan('═'.repeat(80)) + '\n'); // Table header console.log( chalk.bold( '| Deal'.padEnd(25) + '| Score | Recommendation | Market | Team | Traction |' ) ); console.log('|' + '-'.repeat(79) + '|'); // Table rows analyses.forEach(({ analysis }) => { const name = analysis.company.name.padEnd(23); const score = analysis.overallScore.toFixed(1).padEnd(6); const rec = analysis.recommendation.decision.padEnd(14); const market = analysis.scores.market.toFixed(1).padEnd(6); const team = analysis.scores.team.toFixed(1).padEnd(4); const traction = analysis.scores.traction.toFixed(1); console.log(`| ${name} | ${score} | ${rec} | ${market} | ${team} | ${traction} |`); }); console.log('\n' + chalk.bold.cyan('═'.repeat(80)) + '\n'); // Save if requested if (options.output) { const comparisonReport = formatComparison(analyses); fs.writeFileSync(options.output, comparisonReport); console.log(chalk.green(`✓ Comparison saved to: ${options.output}\n`)); } } catch (error: any) { spinner.fail('Comparison failed'); console.error(chalk.red(`\n❌ Error: ${error.message}\n`)); process.exit(1); } }); /** * Helper: Load pitch deck file */ async function loadPitchDeck(filepath: string): Promise { if (!fs.existsSync(filepath)) { throw new Error(`File not found: ${filepath}`); } const ext = path.extname(filepath).toLowerCase(); const format = ext === '.pdf' ? 'pdf' : ext === '.pptx' ? 'pptx' : ext === '.docx' ? 'docx' : 'txt'; // For now, read as text - in production would use pdf-parse, etc. const content = fs.readFileSync(filepath, 'utf-8'); return { filename: path.basename(filepath), filepath, format, content, metadata: { pageCount: content.split('\n').length, }, }; } /** * Helper: Load configuration */ function loadConfig(): AnalysisConfig { const configPath = path.join(process.cwd(), '.vcconfig.json'); // Default configuration const defaultConfig: AnalysisConfig = { framework: 'standard', requiredSections: ['market', 'team', 'product', 'financials', 'traction'], scoring: { enabled: true, weights: { market: 0.25, team: 0.30, product: 0.15, traction: 0.15, financials: 0.10, businessModel: 0.05, competition: 0.05, risks: -0.05, }, thresholds: { strongBuy: 8.0, buy: 6.5, hold: 5.0, }, }, reportTemplate: 'default', outputDirectory: './reports', saveHistory: true, }; if (fs.existsSync(configPath)) { const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); return { ...defaultConfig, ...userConfig }; } return defaultConfig; } /** * Helper: Format analysis as markdown */ function formatAnalysisAsMarkdown(analysis: any): string { return `# Investment Analysis: ${analysis.company.name} **Analysis Date**: ${analysis.analysisDate.toLocaleDateString()} **Analyst**: ${analysis.analyst} **Overall Score**: ${analysis.overallScore.toFixed(1)}/10 **Recommendation**: ${analysis.recommendation.decision.toUpperCase()} --- ## Executive Summary ${analysis.executiveSummary} **Investment Highlights**: ${analysis.highlights.map((h: string) => `- ${h}`).join('\n')} **Key Concerns**: ${analysis.concerns.map((c: string) => `- ${c}`).join('\n')} --- ## Market Analysis **TAM**: ${analysis.market.tam} **SAM**: ${analysis.market.sam} **Growth Rate**: ${analysis.market.growthRate} **Score**: ${analysis.market.score}/10 ${analysis.market.notes} --- ## Team Analysis **Team Size**: ${analysis.team.teamSize} **Score**: ${analysis.team.score}/10 ### Founders ${analysis.team.founders.map((f: any) => `- ${f.name} (${f.title}): ${f.background}`).join('\n')} --- ## Product Analysis **Score**: ${analysis.product.score}/10 **Problem**: ${analysis.product.problemStatement} **Solution**: ${analysis.product.solution} --- ## Traction & Metrics **Users**: ${analysis.traction.users} **Revenue**: ${analysis.traction.revenue} **Growth**: ${analysis.traction.growthRate} **Score**: ${analysis.traction.score}/10 --- ## Financial Analysis **Current Revenue**: ${analysis.financials.currentRevenue} **Burn Rate**: ${analysis.financials.burnRate} **Runway**: ${analysis.financials.runway} **Score**: ${analysis.financials.score}/10 --- ## Investment Terms **Funding Ask**: ${analysis.terms.fundingAsk} **Valuation (Pre-Money)**: ${analysis.terms.valuation.preMoney || 'N/A'} **Valuation (Post-Money)**: ${analysis.terms.valuation.postMoney || 'N/A'} --- ## Investment Recommendation **Decision**: ${analysis.recommendation.decision.toUpperCase()} **Investment Thesis**: ${analysis.recommendation.investmentThesis} **Key Strengths**: ${analysis.recommendation.keyStrengths.map((s: string) => `- ${s}`).join('\n')} **Key Concerns**: ${analysis.recommendation.keyConcerns.map((c: string) => `- ${c}`).join('\n')} **Next Steps**: ${analysis.recommendation.nextSteps.map((s: string, i: number) => `${i + 1}. ${s}`).join('\n')} --- *Generated by Venture Capitalist AI Investment Analysis* `; } /** * Helper: Format comparison */ function formatComparison(analyses: any[]): string { let output = '# Deal Comparison\n\n'; output += `**Comparison Date**: ${new Date().toLocaleDateString()}\n\n`; output += '| Company | Overall Score | Recommendation | Market | Team | Traction |\n'; output += '|---------|---------------|----------------|--------|------|----------|\n'; analyses.forEach(({ analysis }) => { output += `| ${analysis.company.name} | ${analysis.overallScore.toFixed(1)} | ${ analysis.recommendation.decision } | ${analysis.scores.market.toFixed(1)} | ${analysis.scores.team.toFixed(1)} | ${analysis.scores.traction.toFixed( 1 )} |\n`; }); return output; } /** * Helper: Color code scores */ function getScoreColor(score: number): string { if (score >= 8) return chalk.green(`${score.toFixed(1)}/10`); if (score >= 6) return chalk.yellow(`${score.toFixed(1)}/10`); return chalk.red(`${score.toFixed(1)}/10`); } /** * Helper: Color code recommendations */ function getRecommendationColor(rec: string): string { if (rec === 'strong-buy') return chalk.green.bold(rec.toUpperCase()); if (rec === 'buy') return chalk.green(rec.toUpperCase()); if (rec === 'hold') return chalk.yellow(rec.toUpperCase()); return chalk.red(rec.toUpperCase()); } program.parse();