Initial commit
This commit is contained in:
334
skills/deep-research/scripts/company-analyzer-compact.ts
Normal file
334
skills/deep-research/scripts/company-analyzer-compact.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Company Analyzer - Comprehensive company research and analysis
|
||||
* Automated company intelligence gathering for business research
|
||||
*/
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
gatherBasicInfo,
|
||||
gatherFoundationInfo,
|
||||
gatherFinancialInfo,
|
||||
gatherMarketPositionInfo,
|
||||
gatherCultureInfo,
|
||||
gatherRecentDevelopments,
|
||||
} from './company-gatherers';
|
||||
import { JSON_INDENTATION } from './constants';
|
||||
import {
|
||||
generateHeaderSection,
|
||||
generateBasicInfoSection,
|
||||
generateLeadershipSection,
|
||||
generateFinancialSection,
|
||||
generateMarketPositionSection,
|
||||
generateRecentDevelopmentsSection,
|
||||
generateCultureSection,
|
||||
generateSourcesSection,
|
||||
generateFooter,
|
||||
} from './report-generators';
|
||||
import { WebResearcher } from './web-researcher';
|
||||
|
||||
const logger = {
|
||||
log: (message: string) => Bun.write(Bun.stdout, `${message}\n`),
|
||||
warn: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
error: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
};
|
||||
|
||||
interface CompanyAnalysisOptions {
|
||||
company: string;
|
||||
focus: 'foundation' | 'financial' | 'market-position' | 'comprehensive';
|
||||
outputFormat?: 'json' | 'markdown' | 'csv';
|
||||
outputFile?: string;
|
||||
}
|
||||
|
||||
|
||||
interface CompanyData {
|
||||
基本信息: {
|
||||
company_name: string;
|
||||
founded_date: string;
|
||||
headquarters: string;
|
||||
website: string;
|
||||
employee_count: string;
|
||||
industry: string;
|
||||
sector: string;
|
||||
};
|
||||
leadership: { ceo: string; key_executives: Array<{ name: string; position: string; experience: string }> };
|
||||
financial: { revenue: string; market_cap: string; profit_margin: string; revenue_growth: string };
|
||||
market_position: { market_share: string; competitors: string[]; customer_segments: string[]; geographic_presence: string[] };
|
||||
recent_developments: Array<{ date: string; type: string; description: string; source: string }>;
|
||||
culture_employment: { employee_satisfaction: string; benefits: string[]; work_life_balance: string; diversity_initiatives: string[] };
|
||||
sources: Array<{ url: string; title: string; access_date: string; reliability: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Company Analyzer class for comprehensive company research and analysis
|
||||
*/
|
||||
class CompanyAnalyzer {
|
||||
private webResearcher: WebResearcher;
|
||||
private db: Database;
|
||||
|
||||
/** Creates a new CompanyAnalyzer instance */
|
||||
constructor() {
|
||||
this.webResearcher = new WebResearcher();
|
||||
this.db = new Database('company_analysis.db');
|
||||
this.initializeDatabase();
|
||||
}
|
||||
|
||||
/** Initializes the SQLite database */
|
||||
private initializeDatabase(): void {
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS company_analyses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_name TEXT,
|
||||
analysis_focus TEXT,
|
||||
analysis_data TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(company_name, analysis_focus)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
/** Performs comprehensive company analysis
|
||||
* @param {CompanyAnalysisOptions} options - Analysis configuration including company name and focus area
|
||||
* @returns {Promise<CompanyData>} Promise that resolves to complete company analysis data
|
||||
*/
|
||||
async analyzeCompany(options: CompanyAnalysisOptions): Promise<CompanyData> {
|
||||
logger.log(`🏢 Starting company analysis for: ${options.company}`);
|
||||
logger.log(`📊 Focus area: ${options.focus}`);
|
||||
|
||||
try {
|
||||
const companyData = this.initializeCompanyData(options.company);
|
||||
|
||||
// Gather information based on focus
|
||||
await this.gatherFocusSpecificInfo(options.focus, companyData);
|
||||
|
||||
// Save analysis to database
|
||||
this.saveAnalysis(options.company, options.focus, companyData);
|
||||
|
||||
logger.log(`✅ Company analysis completed for ${options.company}`);
|
||||
return companyData;
|
||||
} catch (error) {
|
||||
logger.error('❌ Company analysis failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new CompanyData structure with default values
|
||||
* @param {string} companyName - The name of the company to analyze
|
||||
* @returns {CompanyData} A new CompanyData structure with default values
|
||||
*/
|
||||
private initializeCompanyData(companyName: string): CompanyData {
|
||||
return {
|
||||
基本信息: {
|
||||
company_name: companyName,
|
||||
founded_date: '',
|
||||
headquarters: '',
|
||||
website: '',
|
||||
employee_count: '',
|
||||
industry: '',
|
||||
sector: '',
|
||||
},
|
||||
leadership: { ceo: '', key_executives: [] },
|
||||
financial: { revenue: '', market_cap: '', profit_margin: '', revenue_growth: '' },
|
||||
market_position: { market_share: '', competitors: [], customer_segments: [], geographic_presence: [] },
|
||||
recent_developments: [],
|
||||
culture_employment: { employee_satisfaction: '', benefits: [], work_life_balance: '', diversity_initiatives: [] },
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers information specific to the analysis focus area
|
||||
* @param {string} focus - The analysis focus area (foundation, financial, market-position, comprehensive)
|
||||
* @param {CompanyData} companyData - The company data object to populate with gathered information
|
||||
* @returns {Promise<void>} Promise that resolves when focus-specific information gathering is complete
|
||||
*/
|
||||
private async gatherFocusSpecificInfo(focus: string, companyData: CompanyData): Promise<void> {
|
||||
switch (focus) {
|
||||
case 'foundation':
|
||||
await this.gatherFoundationData(companyData);
|
||||
break;
|
||||
case 'financial':
|
||||
await this.gatherFinancialData(companyData);
|
||||
break;
|
||||
case 'market-position':
|
||||
await this.gatherMarketPositionData(companyData);
|
||||
break;
|
||||
case 'comprehensive':
|
||||
await this.gatherFoundationData(companyData);
|
||||
await this.gatherFinancialData(companyData);
|
||||
await this.gatherMarketPositionData(companyData);
|
||||
await this.gatherCultureData(companyData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all foundation data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with foundation information
|
||||
* @returns {Promise<void>} Promise that resolves when foundation data gathering is complete
|
||||
*/
|
||||
private async gatherFoundationData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherFoundationInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers financial data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with financial information
|
||||
* @returns {Promise<void>} Promise that resolves when financial data gathering is complete
|
||||
*/
|
||||
private async gatherFinancialData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherFinancialInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers market position data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with market position information
|
||||
* @returns {Promise<void>} Promise that resolves when market position data gathering is complete
|
||||
*/
|
||||
private async gatherMarketPositionData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherMarketPositionInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers culture data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with culture and employment information
|
||||
* @returns {Promise<void>} Promise that resolves when culture data gathering is complete
|
||||
*/
|
||||
private async gatherCultureData(companyData: CompanyData): Promise<void> {
|
||||
await gatherCultureInfo(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the completed company analysis to the database
|
||||
* @param {string} companyName - The name of the company being analyzed
|
||||
* @param {string} focus - The analysis focus area (foundation, financial, market-position, comprehensive)
|
||||
* @param {CompanyData} data - The complete company analysis data to save
|
||||
* @returns {void}
|
||||
*/
|
||||
private saveAnalysis(companyName: string, focus: string, data: CompanyData): void {
|
||||
this.db.run(
|
||||
'INSERT OR REPLACE INTO company_analyses (company_name, analysis_focus, analysis_data) VALUES (?, ?, ?)',
|
||||
[companyName, focus, JSON.stringify(data, null, JSON_INDENTATION)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted report from the company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @param {string} format - The output format (markdown, json, csv)
|
||||
* @returns {string} The formatted report as a string
|
||||
*/
|
||||
generateReport(companyData: CompanyData, format = 'markdown'): string {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(companyData, null, JSON_INDENTATION);
|
||||
case 'csv':
|
||||
return this.generateCSVReport(companyData);
|
||||
case 'markdown':
|
||||
default:
|
||||
return this.generateMarkdownReport(companyData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a comprehensive Markdown report from company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @returns {string} The formatted Markdown report as a string
|
||||
*/
|
||||
private generateMarkdownReport(companyData: CompanyData): string {
|
||||
const sections = [
|
||||
generateHeaderSection(companyData),
|
||||
generateBasicInfoSection(companyData),
|
||||
generateLeadershipSection(companyData),
|
||||
generateFinancialSection(companyData),
|
||||
generateMarketPositionSection(companyData),
|
||||
generateRecentDevelopmentsSection(companyData),
|
||||
generateCultureSection(companyData),
|
||||
generateSourcesSection(companyData),
|
||||
generateFooter()
|
||||
];
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CSV report from company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @returns {string} The formatted CSV report as a string
|
||||
*/
|
||||
private generateCSVReport(companyData: CompanyData): string {
|
||||
const headers = ['Category', 'Field', 'Value'];
|
||||
const rows = [
|
||||
['Basic Information', 'Company Name', companyData.基本信息.company_name],
|
||||
['Financial', 'Revenue', companyData.financial.revenue],
|
||||
['Market Position', 'Market Share', companyData.market_position.market_share],
|
||||
];
|
||||
|
||||
return [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the analyzer and cleans up resources
|
||||
* @returns {void}
|
||||
*/
|
||||
close(): void {
|
||||
this.webResearcher.close();
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('company-analyzer')
|
||||
.description('Comprehensive company research and analysis')
|
||||
.version('1.0.0');
|
||||
|
||||
program
|
||||
.requiredOption('-c, --company <string>', 'Company name to analyze')
|
||||
.option('-f, --focus <string>', 'Analysis focus area', 'comprehensive')
|
||||
.option('-o, --output <string>', 'Output file')
|
||||
.option('--format <string>', 'Output format', 'markdown')
|
||||
.action(async options => {
|
||||
const analyzer = new CompanyAnalyzer();
|
||||
|
||||
try {
|
||||
const analysis = await analyzer.analyzeCompany({
|
||||
company: options.company,
|
||||
focus: options.focus,
|
||||
outputFormat: options.format,
|
||||
outputFile: options.output,
|
||||
});
|
||||
|
||||
const report = analyzer.generateReport(analysis, options.format);
|
||||
|
||||
if (options.output) {
|
||||
await Bun.write(options.output, report);
|
||||
logger.log(`📄 Company analysis saved to: ${options.output}`);
|
||||
} else {
|
||||
logger.log(report);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Error:', (error as Error).message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
analyzer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Execute CLI
|
||||
if (import.meta.main) {
|
||||
program.parse();
|
||||
}
|
||||
|
||||
export { CompanyAnalyzer, type CompanyAnalysisOptions, type CompanyData };
|
||||
327
skills/deep-research/scripts/company-analyzer.ts
Normal file
327
skills/deep-research/scripts/company-analyzer.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/* Company Analyzer - Comprehensive company research and analysis */
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
gatherBasicInfo,
|
||||
gatherFoundationInfo,
|
||||
gatherFinancialInfo,
|
||||
gatherMarketPositionInfo,
|
||||
gatherCultureInfo,
|
||||
gatherRecentDevelopments,
|
||||
} from './company-gatherers';
|
||||
import { JSON_INDENTATION } from './constants';
|
||||
import {
|
||||
generateHeaderSection,
|
||||
generateBasicInfoSection,
|
||||
generateLeadershipSection,
|
||||
generateFinancialSection,
|
||||
generateMarketPositionSection,
|
||||
generateRecentDevelopmentsSection,
|
||||
generateCultureSection,
|
||||
generateSourcesSection,
|
||||
generateFooter,
|
||||
} from './report-generators';
|
||||
import { WebResearcher } from './web-researcher';
|
||||
|
||||
const logger = {
|
||||
log: (message: string) => Bun.write(Bun.stdout, `${message}\n`),
|
||||
warn: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
error: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
};
|
||||
|
||||
interface CompanyAnalysisOptions {
|
||||
company: string;
|
||||
focus: 'foundation' | 'financial' | 'market-position' | 'comprehensive';
|
||||
outputFormat?: 'json' | 'markdown' | 'csv';
|
||||
outputFile?: string;
|
||||
}
|
||||
|
||||
interface CompanyData {
|
||||
基本信息: {
|
||||
company_name: string;
|
||||
founded_date: string;
|
||||
headquarters: string;
|
||||
website: string;
|
||||
employee_count: string;
|
||||
industry: string;
|
||||
sector: string;
|
||||
};
|
||||
leadership: { ceo: string; key_executives: Array<{ name: string; position: string; experience: string }> };
|
||||
financial: { revenue: string; market_cap: string; profit_margin: string; revenue_growth: string };
|
||||
market_position: { market_share: string; competitors: string[]; customer_segments: string[]; geographic_presence: string[] };
|
||||
recent_developments: Array<{ date: string; type: string; description: string; source: string }>;
|
||||
culture_employment: { employee_satisfaction: string; benefits: string[]; work_life_balance: string; diversity_initiatives: string[] };
|
||||
sources: Array<{ url: string; title: string; access_date: string; reliability: string }>;
|
||||
};
|
||||
|
||||
/** Company Analyzer class */
|
||||
class CompanyAnalyzer {
|
||||
private webResearcher: WebResearcher;
|
||||
private db: Database;
|
||||
|
||||
/** Creates a new CompanyAnalyzer instance */
|
||||
constructor() {
|
||||
this.webResearcher = new WebResearcher();
|
||||
this.db = new Database('company_analysis.db');
|
||||
this.initializeDatabase();
|
||||
}
|
||||
|
||||
/** Initialize database */
|
||||
private initializeDatabase(): void {
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS company_analyses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_name TEXT,
|
||||
analysis_focus TEXT,
|
||||
analysis_data TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(company_name, analysis_focus)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
/** Performs comprehensive company analysis
|
||||
* @param {CompanyAnalysisOptions} options - Analysis configuration including company name and focus area
|
||||
* @returns {Promise<CompanyData>} Promise that resolves to complete company analysis data
|
||||
*/
|
||||
async analyzeCompany(options: CompanyAnalysisOptions): Promise<CompanyData> {
|
||||
logger.log(`🏢 Starting company analysis for: ${options.company}`);
|
||||
logger.log(`📊 Focus area: ${options.focus}`);
|
||||
|
||||
try {
|
||||
const companyData = this.initializeCompanyData(options.company);
|
||||
|
||||
// Gather information based on focus
|
||||
await this.gatherFocusSpecificInfo(options.focus, companyData);
|
||||
|
||||
// Save analysis to database
|
||||
this.saveAnalysis(options.company, options.focus, companyData);
|
||||
|
||||
logger.log(`✅ Company analysis completed for ${options.company}`);
|
||||
return companyData;
|
||||
} catch (error) {
|
||||
logger.error('❌ Company analysis failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize company data
|
||||
* @param {string} companyName - The name of the company to analyze
|
||||
* @returns {CompanyData} A new CompanyData structure with default values
|
||||
*/
|
||||
private initializeCompanyData(companyName: string): CompanyData {
|
||||
return {
|
||||
基本信息: {
|
||||
company_name: companyName,
|
||||
founded_date: '',
|
||||
headquarters: '',
|
||||
website: '',
|
||||
employee_count: '',
|
||||
industry: '',
|
||||
sector: '',
|
||||
},
|
||||
leadership: { ceo: '', key_executives: [] },
|
||||
financial: { revenue: '', market_cap: '', profit_margin: '', revenue_growth: '' },
|
||||
market_position: { market_share: '', competitors: [], customer_segments: [], geographic_presence: [] },
|
||||
recent_developments: [],
|
||||
culture_employment: { employee_satisfaction: '', benefits: [], work_life_balance: '', diversity_initiatives: [] },
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather focus-specific information
|
||||
* @param {string} focus - The analysis focus area (foundation, financial, market-position, comprehensive)
|
||||
* @param {CompanyData} companyData - The company data object to populate with gathered information
|
||||
* @returns {Promise<void>} Promise that resolves when focus-specific information gathering is complete
|
||||
*/
|
||||
private async gatherFocusSpecificInfo(focus: string, companyData: CompanyData): Promise<void> {
|
||||
switch (focus) {
|
||||
case 'foundation':
|
||||
await this.gatherFoundationData(companyData);
|
||||
break;
|
||||
case 'financial':
|
||||
await this.gatherFinancialData(companyData);
|
||||
break;
|
||||
case 'market-position':
|
||||
await this.gatherMarketPositionData(companyData);
|
||||
break;
|
||||
case 'comprehensive':
|
||||
await this.gatherFoundationData(companyData);
|
||||
await this.gatherFinancialData(companyData);
|
||||
await this.gatherMarketPositionData(companyData);
|
||||
await this.gatherCultureData(companyData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather foundation data
|
||||
* @param {CompanyData} companyData - The company data object to populate with foundation information
|
||||
* @returns {Promise<void>} Promise that resolves when foundation data gathering is complete
|
||||
*/
|
||||
private async gatherFoundationData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherFoundationInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather financial data
|
||||
* @param {CompanyData} companyData - The company data object to populate with financial information
|
||||
* @returns {Promise<void>} Promise that resolves when financial data gathering is complete
|
||||
*/
|
||||
private async gatherFinancialData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherFinancialInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers market position data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with market position information
|
||||
* @returns {Promise<void>} Promise that resolves when market position data gathering is complete
|
||||
*/
|
||||
private async gatherMarketPositionData(companyData: CompanyData): Promise<void> {
|
||||
await gatherBasicInfo(this.webResearcher, companyData);
|
||||
await gatherMarketPositionInfo(this.webResearcher, companyData);
|
||||
await gatherRecentDevelopments(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers culture data for the company
|
||||
* @param {CompanyData} companyData - The company data object to populate with culture and employment information
|
||||
* @returns {Promise<void>} Promise that resolves when culture data gathering is complete
|
||||
*/
|
||||
private async gatherCultureData(companyData: CompanyData): Promise<void> {
|
||||
await gatherCultureInfo(this.webResearcher, companyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the completed company analysis to the database
|
||||
* @param {string} companyName - The name of the company being analyzed
|
||||
* @param {string} focus - The analysis focus area (foundation, financial, market-position, comprehensive)
|
||||
* @param {CompanyData} data - The complete company analysis data to save
|
||||
* @returns {void}
|
||||
*/
|
||||
private saveAnalysis(companyName: string, focus: string, data: CompanyData): void {
|
||||
this.db.run(
|
||||
'INSERT OR REPLACE INTO company_analyses (company_name, analysis_focus, analysis_data) VALUES (?, ?, ?)',
|
||||
[companyName, focus, JSON.stringify(data, null, JSON_INDENTATION)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted report from the company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @param {string} format - The output format (markdown, json, csv)
|
||||
* @returns {string} The formatted report as a string
|
||||
*/
|
||||
generateReport(companyData: CompanyData, format = 'markdown'): string {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(companyData, null, JSON_INDENTATION);
|
||||
case 'csv':
|
||||
return this.generateCSVReport(companyData);
|
||||
case 'markdown':
|
||||
default:
|
||||
return this.generateMarkdownReport(companyData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a comprehensive Markdown report from company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @returns {string} The formatted Markdown report as a string
|
||||
*/
|
||||
private generateMarkdownReport(companyData: CompanyData): string {
|
||||
const sections = [
|
||||
generateHeaderSection(companyData),
|
||||
generateBasicInfoSection(companyData),
|
||||
generateLeadershipSection(companyData),
|
||||
generateFinancialSection(companyData),
|
||||
generateMarketPositionSection(companyData),
|
||||
generateRecentDevelopmentsSection(companyData),
|
||||
generateCultureSection(companyData),
|
||||
generateSourcesSection(companyData),
|
||||
generateFooter()
|
||||
];
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CSV report from company analysis data
|
||||
* @param {CompanyData} companyData - The complete company analysis data
|
||||
* @returns {string} The formatted CSV report as a string
|
||||
*/
|
||||
private generateCSVReport(companyData: CompanyData): string {
|
||||
const headers = ['Category', 'Field', 'Value'];
|
||||
const rows = [
|
||||
['Basic Information', 'Company Name', companyData.基本信息.company_name],
|
||||
['Financial', 'Revenue', companyData.financial.revenue],
|
||||
['Market Position', 'Market Share', companyData.market_position.market_share],
|
||||
];
|
||||
|
||||
return [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the analyzer and cleans up resources
|
||||
*/
|
||||
close(): void {
|
||||
this.webResearcher.close();
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('company-analyzer')
|
||||
.description('Comprehensive company research and analysis')
|
||||
.version('1.0.0');
|
||||
|
||||
program
|
||||
.requiredOption('-c, --company <string>', 'Company name to analyze')
|
||||
.option('-f, --focus <string>', 'Analysis focus area', 'comprehensive')
|
||||
.option('-o, --output <string>', 'Output file')
|
||||
.option('--format <string>', 'Output format', 'markdown')
|
||||
.action(async options => {
|
||||
const analyzer = new CompanyAnalyzer();
|
||||
|
||||
try {
|
||||
const analysis = await analyzer.analyzeCompany({
|
||||
company: options.company,
|
||||
focus: options.focus,
|
||||
outputFormat: options.format,
|
||||
outputFile: options.output,
|
||||
});
|
||||
|
||||
const report = analyzer.generateReport(analysis, options.format);
|
||||
|
||||
if (options.output) {
|
||||
await Bun.write(options.output, report);
|
||||
logger.log(`📄 Company analysis saved to: ${options.output}`);
|
||||
} else {
|
||||
logger.log(report);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Error:', (error as Error).message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
analyzer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Execute CLI
|
||||
if (import.meta.main) {
|
||||
program.parse();
|
||||
}
|
||||
|
||||
export { CompanyAnalyzer, type CompanyAnalysisOptions, type CompanyData };
|
||||
220
skills/deep-research/scripts/company-gatherers.ts
Normal file
220
skills/deep-research/scripts/company-gatherers.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Data gathering utilities for company analysis
|
||||
*/
|
||||
|
||||
import type { CompanyData, SearchResult } from './company-analyzer';
|
||||
import { RESEARCH_DEPTH } from './constants';
|
||||
import {
|
||||
extractBasicInfo,
|
||||
extractLeadershipInfo,
|
||||
extractFinancialInfo,
|
||||
extractMarketPositionInfo,
|
||||
extractCultureInfo,
|
||||
} from './data-extractors';
|
||||
import type { WebResearcher } from './web-researcher';
|
||||
|
||||
const logger = {
|
||||
log: (message: string) => Bun.write(Bun.stdout, `${message}\n`),
|
||||
warn: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
error: (message: string) => Bun.write(Bun.stderr, `${message}\n`),
|
||||
};
|
||||
|
||||
/**
|
||||
* Gathers basic company information from multiple sources
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherBasicInfo(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} company profile Wikipedia`,
|
||||
`${companyData.基本信息.company_name} about us official website`,
|
||||
`${companyData.基本信息.company_name} company information Crunchbase`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'quick',
|
||||
maxResults: RESEARCH_DEPTH.QUICK,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
await extractBasicInfo(result, companyData);
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather basic info for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers foundational company information including leadership and business model
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherFoundationInfo(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} CEO leadership team`,
|
||||
`${companyData.基本信息.company_name} mission vision values`,
|
||||
`${companyData.基本信息.company_name} business model revenue streams`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'comprehensive',
|
||||
maxResults: RESEARCH_DEPTH.COMPREHENSIVE,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
await extractLeadershipInfo(result, companyData);
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather foundation info for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers financial information about the company
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherFinancialInfo(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} annual revenue financial results`,
|
||||
`${companyData.基本信息.company_name} market cap stock price`,
|
||||
`${companyData.基本信息.company_name} financial performance profit margin`,
|
||||
`${companyData.基本信息.company_name} revenue growth quarterly results`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'comprehensive',
|
||||
maxResults: RESEARCH_DEPTH.COMPREHENSIVE + RESEARCH_DEPTH.QUICK,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
await extractFinancialInfo(result, companyData);
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather financial info for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers market position information about the company
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherMarketPositionInfo(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} market share competitors`,
|
||||
`${companyData.基本信息.company_name} customer base target market`,
|
||||
`${companyData.基本信息.company_name} competitive advantages differentiation`,
|
||||
`${companyData.基本信息.company_name} industry ranking position`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'comprehensive',
|
||||
maxResults: RESEARCH_DEPTH.COMPREHENSIVE,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
await extractMarketPositionInfo(result, companyData);
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather market position info for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers company culture and employment information
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherCultureInfo(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} employee reviews Glassdoor`,
|
||||
`${companyData.基本信息.company_name} company culture work life balance`,
|
||||
`${companyData.基本信息.company_name} benefits compensation packages`,
|
||||
`${companyData.基本信息.company_name} diversity inclusion initiatives`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'comprehensive',
|
||||
maxResults: RESEARCH_DEPTH.RECENT,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
await extractCultureInfo(result, companyData);
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather culture info for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers recent developments and news about the company
|
||||
* @param webResearcher
|
||||
* @param companyData
|
||||
*/
|
||||
export async function gatherRecentDevelopments(
|
||||
webResearcher: WebResearcher,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const queries = [
|
||||
`${companyData.基本信息.company_name} recent news 2024`,
|
||||
`${companyData.基本信息.company_name} latest developments`,
|
||||
`${companyData.基本信息.company_name} partnerships acquisitions 2024`,
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
const results = await webResearcher.performResearch({
|
||||
query,
|
||||
depth: 'recent',
|
||||
maxResults: RESEARCH_DEPTH.QUICK,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
companyData.recent_developments.push({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
type: 'news',
|
||||
description: result.snippet,
|
||||
source: result.source,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
logger.warn(`⚠️ Failed to gather recent developments for query: ${query}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
skills/deep-research/scripts/constants.ts
Normal file
24
skills/deep-research/scripts/constants.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Constants and utilities for company analysis
|
||||
*/
|
||||
|
||||
export const JSON_INDENTATION = 2;
|
||||
export const NOT_AVAILABLE = 'Not available';
|
||||
export const RESEARCH_DEPTH = {
|
||||
QUICK: 5,
|
||||
COMPREHENSIVE: 8,
|
||||
RECENT: 6,
|
||||
} as const;
|
||||
|
||||
export const REGEX_PATTERNS = {
|
||||
WEBSITE: /https?:\/\/\S+/,
|
||||
FOUNDED_DATE: /founded in (\d{4})|established (\d{4})|since (\d{4})/i,
|
||||
HEADQUARTERS: /headquartered in ([^.]+?)\.|based in ([^.]+?)\./i,
|
||||
CEO: /ceo[\s:]+([^.]+?)\.|chief executive officer[\s:]+([^.]+?)\./i,
|
||||
EXECUTIVES: /(president|cto|cfo|coo|chief[^,]+?)[\s:]+([^.]+?)\./gi,
|
||||
REVENUE: /revenue[\s:]+\$([\d.]+)[bkm]|annual revenue[\s:]+\$([\d.]+)[bkm]/i,
|
||||
MARKET_CAP: /market cap[\s:]+\$([\d.]+)[bkm]|market capitalization[\s:]+\$([\d.]+)[bkm]/i,
|
||||
COMPETITORS: /competitors?[\s:]+([^.]+?)\.|rivals?[\s:]+([^.]+?)\./i,
|
||||
MARKET_SHARE: /market share[\s:]+([\d.]+)%|([\d.]+)% market share/i,
|
||||
SATISFACTION: /(\d+\.\d+)\/5|(\d+)%.*?recommend|satisfaction[\s:]+(\d+\.\d+)/i,
|
||||
} as const;
|
||||
165
skills/deep-research/scripts/data-extractors.ts
Normal file
165
skills/deep-research/scripts/data-extractors.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Data extraction utilities for company analysis
|
||||
*/
|
||||
|
||||
import type { CompanyData, SearchResult } from './company-analyzer';
|
||||
import { REGEX_PATTERNS } from './constants';
|
||||
|
||||
/**
|
||||
* Extracts basic company information from search results
|
||||
* Uses regex patterns to identify and extract company website, founded date, and headquarters
|
||||
* @param {SearchResult} searchResult - The search result containing title and snippet to analyze
|
||||
* @param {CompanyData} companyData - The company data object to populate with extracted information
|
||||
* @returns {Promise<void>} Promise that resolves when basic information extraction is complete
|
||||
*/
|
||||
export async function extractBasicInfo(searchResult: SearchResult, companyData: CompanyData): Promise<void> {
|
||||
const text = `${searchResult.title} ${searchResult.snippet}`;
|
||||
|
||||
// Extract website
|
||||
const websiteMatch = text.match(REGEX_PATTERNS.WEBSITE);
|
||||
if (websiteMatch && !companyData.基本信息.website) {
|
||||
companyData.基本信息.website = websiteMatch[0];
|
||||
}
|
||||
|
||||
// Extract founded date
|
||||
const foundedMatch = text.match(REGEX_PATTERNS.FOUNDED_DATE);
|
||||
if (foundedMatch && !companyData.基本信息.founded_date) {
|
||||
companyData.基本信息.founded_date = foundedMatch[1] || foundedMatch[2] || foundedMatch[3];
|
||||
}
|
||||
|
||||
// Extract headquarters
|
||||
const hqMatch = text.match(REGEX_PATTERNS.HEADQUARTERS);
|
||||
if (hqMatch && !companyData.基本信息.headquarters) {
|
||||
companyData.基本信息.headquarters = (hqMatch[1] || hqMatch[2] || '').replace(/\.$/, '').trim();
|
||||
}
|
||||
|
||||
companyData.sources.push({
|
||||
url: searchResult.url,
|
||||
title: searchResult.title,
|
||||
access_date: new Date().toISOString(),
|
||||
reliability: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts leadership information from search results
|
||||
* Uses regex patterns to identify CEO and key executive information
|
||||
* @param {SearchResult} searchResult - The search result containing title and snippet to analyze
|
||||
* @param {CompanyData} companyData - The company data object to populate with leadership information
|
||||
* @returns {Promise<void>} Promise that resolves when leadership information extraction is complete
|
||||
*/
|
||||
export async function extractLeadershipInfo(searchResult: SearchResult, companyData: CompanyData): Promise<void> {
|
||||
const text = `${searchResult.title} ${searchResult.snippet}`;
|
||||
|
||||
// Extract CEO
|
||||
const ceoMatch = text.match(REGEX_PATTERNS.CEO);
|
||||
if (ceoMatch && !companyData.leadership.ceo) {
|
||||
companyData.leadership.ceo = (ceoMatch[1] || ceoMatch[2] || '').replace(/\.$/, '').trim();
|
||||
}
|
||||
|
||||
// Extract key executives
|
||||
const executiveMatches = text.match(REGEX_PATTERNS.EXECUTIVES);
|
||||
if (executiveMatches) {
|
||||
for (const match of executiveMatches) {
|
||||
const positionMatch = match.match(/(president|cto|cfo|coo|chief[^,]+?)[\s:]+([^.]+?)\./i);
|
||||
if (positionMatch) {
|
||||
companyData.leadership.key_executives.push({
|
||||
name: positionMatch[2].trim(),
|
||||
position: positionMatch[1].trim(),
|
||||
experience: 'To be researched',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts financial information from search results
|
||||
* Uses regex patterns to identify revenue, market cap, and other financial metrics
|
||||
* @param {SearchResult} searchResult - The search result containing title and snippet to analyze
|
||||
* @param {CompanyData} companyData - The company data object to populate with financial information
|
||||
* @returns {Promise<void>} Promise that resolves when financial information extraction is complete
|
||||
*/
|
||||
export async function extractFinancialInfo(searchResult: SearchResult, companyData: CompanyData): Promise<void> {
|
||||
const text = `${searchResult.title} ${searchResult.snippet}`;
|
||||
|
||||
// Extract revenue
|
||||
const revenueMatch = text.match(REGEX_PATTERNS.REVENUE);
|
||||
if (revenueMatch && !companyData.financial.revenue) {
|
||||
const amount = revenueMatch[1] || revenueMatch[2];
|
||||
const multiplier = getFinancialMultiplier(text);
|
||||
companyData.financial.revenue = `$${amount} ${multiplier}`;
|
||||
}
|
||||
|
||||
// Extract market cap
|
||||
const marketCapMatch = text.match(REGEX_PATTERNS.MARKET_CAP);
|
||||
if (marketCapMatch && !companyData.financial.market_cap) {
|
||||
const amount = marketCapMatch[1] || marketCapMatch[2];
|
||||
const multiplier = getFinancialMultiplier(text);
|
||||
companyData.financial.market_cap = `$${amount} ${multiplier}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts market position information from search results
|
||||
* Uses regex patterns to identify competitors, market share, and competitive advantages
|
||||
* @param {SearchResult} searchResult - The search result containing title and snippet to analyze
|
||||
* @param {CompanyData} companyData - The company data object to populate with market position information
|
||||
* @returns {Promise<void>} Promise that resolves when market position information extraction is complete
|
||||
*/
|
||||
export async function extractMarketPositionInfo(
|
||||
searchResult: SearchResult,
|
||||
companyData: CompanyData
|
||||
): Promise<void> {
|
||||
const text = `${searchResult.title} ${searchResult.snippet}`;
|
||||
|
||||
// Extract competitors
|
||||
const competitorMatches = text.match(REGEX_PATTERNS.COMPETITORS);
|
||||
if (competitorMatches) {
|
||||
const competitors = (competitorMatches[1] || competitorMatches[2])
|
||||
.split(/[,;]/)
|
||||
.map(c => c.trim());
|
||||
companyData.market_position.competitors.push(...competitors);
|
||||
}
|
||||
|
||||
// Extract market share
|
||||
const marketShareMatch = text.match(REGEX_PATTERNS.MARKET_SHARE);
|
||||
if (marketShareMatch && !companyData.market_position.market_share) {
|
||||
companyData.market_position.market_share = `${marketShareMatch[1] || marketShareMatch[2]}%`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts culture and employment information from search results
|
||||
* Uses regex patterns to identify employee satisfaction ratings and work environment details
|
||||
* @param {SearchResult} searchResult - The search result containing title and snippet to analyze
|
||||
* @param {CompanyData} companyData - The company data object to populate with culture and employment information
|
||||
* @returns {Promise<void>} Promise that resolves when culture information extraction is complete
|
||||
*/
|
||||
export async function extractCultureInfo(searchResult: SearchResult, companyData: CompanyData): Promise<void> {
|
||||
const text = `${searchResult.title} ${searchResult.snippet}`;
|
||||
|
||||
// Extract employee satisfaction
|
||||
const satisfactionMatch = text.match(REGEX_PATTERNS.SATISFACTION);
|
||||
if (satisfactionMatch && !companyData.culture_employment.employee_satisfaction) {
|
||||
const rating = satisfactionMatch[1] || satisfactionMatch[2] || satisfactionMatch[3];
|
||||
companyData.culture_employment.employee_satisfaction = `${rating}/5`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines financial multiplier based on text content
|
||||
* @param {string} text - Text to search for multiplier indicators
|
||||
* @returns {string} Appropriate financial multiplier string
|
||||
*/
|
||||
function getFinancialMultiplier(text: string): string {
|
||||
if (text.includes('B')) {
|
||||
return 'Billion';
|
||||
}
|
||||
|
||||
if (text.includes('M')) {
|
||||
return 'Million';
|
||||
}
|
||||
|
||||
return 'Thousand';
|
||||
}
|
||||
656
skills/deep-research/scripts/report-generator.ts
Normal file
656
skills/deep-research/scripts/report-generator.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Report Generator - Automated report generation from research data
|
||||
* Generates professional research reports in various formats
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Command } from 'commander';
|
||||
|
||||
interface ReportGenerationOptions {
|
||||
template: string;
|
||||
input: string;
|
||||
output?: string;
|
||||
format?: 'markdown' | 'html' | 'pdf' | 'docx';
|
||||
includeExecutiveSummary?: boolean;
|
||||
includeAppendix?: boolean;
|
||||
}
|
||||
|
||||
interface ReportData {
|
||||
title: string;
|
||||
author?: string;
|
||||
date: string;
|
||||
sections: Array<{
|
||||
title: string;
|
||||
content: string;
|
||||
subsections?: Array<{
|
||||
title: string;
|
||||
content: string;
|
||||
}>;
|
||||
}>;
|
||||
metadata?: {
|
||||
research_objectives: string;
|
||||
methodology: string;
|
||||
sources: Array<{
|
||||
title: string;
|
||||
url: string;
|
||||
access_date: string;
|
||||
reliability: string;
|
||||
}>;
|
||||
limitations: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class ReportGenerator {
|
||||
private readonly templateDir = join(__dirname, '..', 'assets', 'report-templates');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
async generateReport(options: ReportGenerationOptions): Promise<string> {
|
||||
console.log(`📝 Generating report using template: ${options.template}`);
|
||||
console.log(`📊 Input data: ${options.input}`);
|
||||
|
||||
try {
|
||||
// Load research data
|
||||
const researchData = this.loadResearchData(options.input);
|
||||
|
||||
// Load template
|
||||
const template = this.loadTemplate(options.template);
|
||||
|
||||
// Generate report
|
||||
const report = this.processTemplate(template, researchData, options);
|
||||
|
||||
// Save report
|
||||
if (options.output) {
|
||||
await Bun.write(options.output, report);
|
||||
console.log(`📄 Report saved to: ${options.output}`);
|
||||
}
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
console.error('❌ Report generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param inputPath
|
||||
*/
|
||||
private loadResearchData(inputPath: string): ReportData {
|
||||
try {
|
||||
const data = readFileSync(inputPath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load research data from ${inputPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param templateName
|
||||
*/
|
||||
private loadTemplate(templateName: string): string {
|
||||
const templatePath = join(this.templateDir, `${templateName}.md`);
|
||||
|
||||
try {
|
||||
return readFileSync(templatePath, 'utf-8');
|
||||
} catch {
|
||||
// Fallback to default template
|
||||
return this.getDefaultTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private getDefaultTemplate(): string {
|
||||
return `
|
||||
# {{title}}
|
||||
|
||||
{{#if author}}
|
||||
**Author:** {{author}}
|
||||
{{/if}}
|
||||
**Date:** {{date}}
|
||||
|
||||
## Executive Summary
|
||||
|
||||
{{executive_summary}}
|
||||
|
||||
## Research Objectives
|
||||
|
||||
{{research_objectives}}
|
||||
|
||||
## Methodology
|
||||
|
||||
{{methodology}}
|
||||
|
||||
## Findings
|
||||
|
||||
{{#each sections}}
|
||||
### {{title}}
|
||||
|
||||
{{content}}
|
||||
|
||||
{{#each subsections}}
|
||||
#### {{title}}
|
||||
|
||||
{{content}}
|
||||
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
## Sources
|
||||
|
||||
{{#each sources}}
|
||||
- [{{title}}]({{url}}) - {{reliability}} reliability - Accessed {{access_date}}
|
||||
{{/each}}
|
||||
|
||||
## Limitations
|
||||
|
||||
{{#each limitations}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
|
||||
{{#if include_appendix}}
|
||||
## Appendix
|
||||
|
||||
Detailed data and additional information can be found in the appendix.
|
||||
{{/if}}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param data
|
||||
* @param options
|
||||
*/
|
||||
private processTemplate(
|
||||
template: string,
|
||||
data: ReportData,
|
||||
options: ReportGenerationOptions
|
||||
): string {
|
||||
let report = template;
|
||||
|
||||
report = this.replaceSimpleVariables(report, data);
|
||||
report = this.replaceMetadata(report, data.metadata);
|
||||
report = this.replaceSections(report, data.sections);
|
||||
report = this.handleConditionalBlocks(report, options);
|
||||
report = this.cleanupRemainingTemplateSyntax(report);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param data
|
||||
*/
|
||||
private replaceSimpleVariables(template: string, data: ReportData): string {
|
||||
let report = template;
|
||||
report = report.replace(/{{title}}/g, data.title);
|
||||
report = report.replace(/{{author}}/g, data.author || 'Research Analyst');
|
||||
report = report.replace(/{{date}}/g, data.date);
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param metadata
|
||||
*/
|
||||
private replaceMetadata(template: string, metadata: ReportData['metadata']): string {
|
||||
let report = template;
|
||||
|
||||
if (metadata) {
|
||||
report = report.replace(
|
||||
/{{research_objectives}}/g,
|
||||
metadata.research_objectives || 'Not specified'
|
||||
);
|
||||
report = report.replace(
|
||||
/{{methodology}}/g,
|
||||
metadata.methodology || 'Standard research methodology'
|
||||
);
|
||||
|
||||
report = this.replaceSourcesSection(report, metadata.sources);
|
||||
report = this.replaceLimitationsSection(report, metadata.limitations);
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param sources
|
||||
*/
|
||||
private replaceSourcesSection(template: string, sources: ReportData['metadata']['sources']): string {
|
||||
if (!sources || sources.length === 0) {
|
||||
return template.replace(/{{#each sources}}[\S\s]*?{{\/each}}/g, 'No sources specified');
|
||||
}
|
||||
|
||||
const sourcesSection = sources
|
||||
.map(source =>
|
||||
`- [${source.title}](${source.url}) - ${source.reliability} reliability - Accessed ${new Date(source.access_date).toLocaleDateString()}`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return template.replace(/{{#each sources}}[\S\s]*?{{\/each}}/g, sourcesSection);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param limitations
|
||||
*/
|
||||
private replaceLimitationsSection(template: string, limitations: ReportData['metadata']['limitations']): string {
|
||||
const limitationsSection = limitations?.map(limitation => `- ${limitation}`).join('\n') || 'No limitations specified';
|
||||
return template.replace(/{{#each limitations}}[\S\s]*?{{\/each}}/g, limitationsSection);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param sections
|
||||
*/
|
||||
private replaceSections(template: string, sections: ReportData['sections']): string {
|
||||
if (!sections || sections.length === 0) {
|
||||
return template.replace(/{{#each sections}}[\S\s]*?{{\/each}}/g, 'No sections available');
|
||||
}
|
||||
|
||||
const sectionsSection = sections
|
||||
.map(section => this.formatSection(section))
|
||||
.join('\n');
|
||||
|
||||
return template.replace(/{{#each sections}}[\S\s]*?{{\/each}}/g, sectionsSection);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param section
|
||||
*/
|
||||
private formatSection(section: ReportData['sections'][0]): string {
|
||||
let sectionText = `### ${section.title}\n\n${section.content}\n\n`;
|
||||
|
||||
if (section.subsections && section.subsections.length > 0) {
|
||||
const subsectionsText = section.subsections
|
||||
.map(subsection => `#### ${subsection.title}\n\n${subsection.content}\n\n`)
|
||||
.join('');
|
||||
sectionText += subsectionsText;
|
||||
}
|
||||
|
||||
return sectionText;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
* @param options
|
||||
*/
|
||||
private handleConditionalBlocks(template: string, options: ReportGenerationOptions): string {
|
||||
return template.replace(
|
||||
/{{#if include_appendix}}([\S\s]*?){{\/if}}/g,
|
||||
options.includeAppendix ? '$1' : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template
|
||||
*/
|
||||
private cleanupRemainingTemplateSyntax(template: string): string {
|
||||
return template.replace(/{{[^}]*}}/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param markdownContent
|
||||
*/
|
||||
async generateHTMLReport(markdownContent: string): Promise<string> {
|
||||
const htmlStructure = this.createHTMLStructure();
|
||||
const cssStyles = this.generateCSSStyles();
|
||||
const bodyContent = this.markdownToHTML(markdownContent);
|
||||
|
||||
return htmlStructure
|
||||
.replace('{{CSS_STYLES}}', cssStyles)
|
||||
.replace('{{BODY_CONTENT}}', bodyContent);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private createHTMLStructure(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Research Report</title>
|
||||
<style>
|
||||
{{CSS_STYLES}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{BODY_CONTENT}}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateCSSStyles(): string {
|
||||
const cssSections = [
|
||||
this.generateBodyCSS(),
|
||||
this.generateHeadingCSS(),
|
||||
this.generateLinkCSS(),
|
||||
this.generateListCSS(),
|
||||
this.generateBlockquoteCSS(),
|
||||
this.generateCodeCSS(),
|
||||
this.generateTableCSS(),
|
||||
this.generateUtilityCSS(),
|
||||
this.generatePrintCSS()
|
||||
];
|
||||
|
||||
return cssSections.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateBodyCSS(): string {
|
||||
return `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateHeadingCSS(): string {
|
||||
return `
|
||||
h1, h2, h3, h4 {
|
||||
color: #2c3e50;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1 {
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
h2 {
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
padding-bottom: 0.3rem;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateLinkCSS(): string {
|
||||
return `
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateListCSS(): string {
|
||||
return `
|
||||
ul, ol {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateBlockquoteCSS(): string {
|
||||
return `
|
||||
blockquote {
|
||||
border-left: 4px solid #3498db;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateCodeCSS(): string {
|
||||
return `
|
||||
code {
|
||||
background-color: #f1f2f6;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
pre {
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateTableCSS(): string {
|
||||
return `
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.8rem;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generateUtilityCSS(): string {
|
||||
return `
|
||||
.executive-summary {
|
||||
background-color: #e8f4fd;
|
||||
padding: 1.5rem;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #3498db;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.source-list {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private generatePrintCSS(): string {
|
||||
return `
|
||||
@media print {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
padding: 1rem;
|
||||
}
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param markdown
|
||||
*/
|
||||
private markdownToHTML(markdown: string): string {
|
||||
// Basic markdown to HTML conversion
|
||||
let html = markdown;
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
||||
|
||||
// Bold and italic
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^\* (.+)$/gim, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// Line breaks
|
||||
html = html.replace(/\n\n/g, '</p><p>');
|
||||
html = `<p>${ html }</p>`;
|
||||
|
||||
// Clean up
|
||||
html = html.replace(/<p><h/g, '<h');
|
||||
html = html.replace(/<\/h([1-6])><\/p>/g, '</h$1>');
|
||||
html = html.replace(/<p><ul>/g, '<ul>');
|
||||
html = html.replace(/<\/ul><\/p>/g, '</ul>');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param htmlContent
|
||||
* @param outputPath
|
||||
*/
|
||||
async generatePDFReport(htmlContent: string, outputPath: string): Promise<void> {
|
||||
// Note: PDF generation would require a headless browser or PDF library
|
||||
// For now, we'll save as HTML and suggest using a browser's print to PDF
|
||||
const htmlPath = outputPath.replace('.pdf', '.html');
|
||||
await Bun.write(htmlPath, htmlContent);
|
||||
console.log(`📄 HTML report saved to: ${htmlPath}`);
|
||||
console.log(`💡 To convert to PDF, open the HTML file in a browser and use "Print to PDF"`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
listAvailableTemplates(): string[] {
|
||||
try {
|
||||
const templates = [
|
||||
'executive-summary',
|
||||
'comprehensive-analysis',
|
||||
'competitive-intelligence',
|
||||
'technical-evaluation',
|
||||
'market-analysis',
|
||||
];
|
||||
|
||||
console.log('📋 Available Report Templates:');
|
||||
for (const template of templates) {
|
||||
console.log(` - ${template}`);
|
||||
}
|
||||
|
||||
return templates;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to list templates:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('report-generator')
|
||||
.description('Automated report generation from research data')
|
||||
.version('1.0.0');
|
||||
|
||||
program
|
||||
.requiredOption('-t, --template <string>', 'Report template to use')
|
||||
.requiredOption('-i, --input <string>', 'Input research data file (JSON)')
|
||||
.option('-o, --output <string>', 'Output file path')
|
||||
.option('-f, --format <string>', 'Output format', 'markdown')
|
||||
.option('--include-executive-summary', 'Include executive summary')
|
||||
.option('--include-appendix', 'Include appendix')
|
||||
.action(async options => {
|
||||
const generator = new ReportGenerator();
|
||||
|
||||
try {
|
||||
let report = await generator.generateReport({
|
||||
template: options.template,
|
||||
input: options.input,
|
||||
output: options.output,
|
||||
format: options.format,
|
||||
includeExecutiveSummary: options.includeExecutiveSummary,
|
||||
includeAppendix: options.includeAppendix,
|
||||
});
|
||||
|
||||
// Convert to different formats if requested
|
||||
if (options.format === 'html') {
|
||||
report = await generator.generateHTMLReport(report);
|
||||
} else if (options.format === 'pdf') {
|
||||
await generator.generatePDFReport(report, options.output || 'report.pdf');
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.output) {
|
||||
await Bun.write(options.output, report);
|
||||
console.log(`📄 Report generated successfully: ${options.output}`);
|
||||
} else {
|
||||
console.log(report);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('templates')
|
||||
.description('List available report templates')
|
||||
.action(() => {
|
||||
const generator = new ReportGenerator();
|
||||
generator.listAvailableTemplates();
|
||||
});
|
||||
|
||||
// Execute CLI
|
||||
if (import.meta.main) {
|
||||
program.parse();
|
||||
}
|
||||
|
||||
export { ReportGenerator, type ReportGenerationOptions, type ReportData };
|
||||
185
skills/deep-research/scripts/report-generators.ts
Normal file
185
skills/deep-research/scripts/report-generators.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Report generation utilities for company analysis
|
||||
*/
|
||||
|
||||
import type { CompanyData } from './company-analyzer';
|
||||
import { NOT_AVAILABLE } from './constants';
|
||||
|
||||
/**
|
||||
* Generates the header section of the Markdown report
|
||||
* Creates the title and executive summary for the company analysis report
|
||||
* @param {CompanyData} companyData - The company analysis data containing the company name
|
||||
* @returns {string} A formatted Markdown string for the report header section
|
||||
*/
|
||||
export function generateHeaderSection(companyData: CompanyData): string {
|
||||
return `# Company Analysis Report: ${companyData.基本信息.company_name}
|
||||
|
||||
## 📊 Executive Summary
|
||||
|
||||
This comprehensive analysis provides insights into ${companyData.基本信息.company_name}'s business operations, financial performance, market position, and recent developments.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the basic information section of the Markdown report
|
||||
* Creates a formatted section with company name, founding date, headquarters, and website
|
||||
* @param {CompanyData} companyData - The company analysis data containing basic company information
|
||||
* @returns {string} A formatted Markdown string for the basic information section
|
||||
*/
|
||||
export function generateBasicInfoSection(companyData: CompanyData): string {
|
||||
return `## 🏢 Basic Information
|
||||
|
||||
- **Company Name:** ${companyData.基本信息.company_name}
|
||||
- **Founded:** ${companyData.基本信息.founded_date || NOT_AVAILABLE}
|
||||
- **Headquarters:** ${companyData.基本信息.headquarters || NOT_AVAILABLE}
|
||||
- **Website:** ${companyData.基本信息.website || NOT_AVAILABLE}
|
||||
- **Industry:** ${companyData.基本信息.industry || NOT_AVAILABLE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the leadership section of the Markdown report
|
||||
* Creates a formatted section with CEO and key executives information
|
||||
* @param {CompanyData} companyData - The company analysis data containing leadership information
|
||||
* @returns {string} A formatted Markdown string for the leadership section
|
||||
*/
|
||||
export function generateLeadershipSection(companyData: CompanyData): string {
|
||||
const executives = companyData.leadership.key_executives
|
||||
.map(exec => `- **${exec.position}:** ${exec.name}`)
|
||||
.join('\n') || 'No key executives identified';
|
||||
|
||||
return `## 👥 Leadership
|
||||
|
||||
- **CEO:** ${companyData.leadership.ceo || NOT_AVAILABLE}
|
||||
|
||||
### Key Executives
|
||||
${executives}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the financial performance section of the Markdown report
|
||||
* Creates a formatted section with revenue, market cap, profit margin, and growth data
|
||||
* @param {CompanyData} companyData - The company analysis data containing financial information
|
||||
* @returns {string} A formatted Markdown string for the financial performance section
|
||||
*/
|
||||
export function generateFinancialSection(companyData: CompanyData): string {
|
||||
return `## 💰 Financial Performance
|
||||
|
||||
- **Revenue:** ${companyData.financial.revenue || NOT_AVAILABLE}
|
||||
- **Market Cap:** ${companyData.financial.market_cap || NOT_AVAILABLE}
|
||||
- **Profit Margin:** ${companyData.financial.profit_margin || NOT_AVAILABLE}
|
||||
- **Revenue Growth:** ${companyData.financial.revenue_growth || NOT_AVAILABLE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the market position section of the Markdown report
|
||||
* Creates a formatted section with market share, competitors, and customer segments
|
||||
* @param {CompanyData} companyData - The company analysis data containing market position information
|
||||
* @returns {string} A formatted Markdown string for the market position section
|
||||
*/
|
||||
export function generateMarketPositionSection(companyData: CompanyData): string {
|
||||
const competitors = companyData.market_position.competitors
|
||||
.map(comp => `- ${comp}`)
|
||||
.join('\n') || 'No competitors identified';
|
||||
|
||||
const customerSegments = companyData.market_position.customer_segments
|
||||
.map(seg => `- ${seg}`)
|
||||
.join('\n') || 'No customer segments identified';
|
||||
|
||||
return `## 🎯 Market Position
|
||||
|
||||
- **Market Share:** ${companyData.market_position.market_share || NOT_AVAILABLE}
|
||||
|
||||
### Competitors
|
||||
${competitors}
|
||||
|
||||
### Customer Segments
|
||||
${customerSegments}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the recent developments section of the Markdown report
|
||||
* Creates a formatted section with recent news, partnerships, and company developments
|
||||
* @param {CompanyData} companyData - The company analysis data containing recent developments information
|
||||
* @returns {string} A formatted Markdown string for the recent developments section
|
||||
*/
|
||||
export function generateRecentDevelopmentsSection(companyData: CompanyData): string {
|
||||
const developments = companyData.recent_developments
|
||||
.map(dev => `- **${dev.date}:** ${dev.description} (${dev.source})`)
|
||||
.join('\n') || 'No recent developments identified';
|
||||
|
||||
return `## 📈 Recent Developments
|
||||
|
||||
${developments}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the culture and employment section of the Markdown report
|
||||
* Creates a formatted section with employee satisfaction and benefits information
|
||||
* @param {CompanyData} companyData - The company analysis data containing culture and employment information
|
||||
* @returns {string} A formatted Markdown string for the culture and employment section
|
||||
*/
|
||||
export function generateCultureSection(companyData: CompanyData): string {
|
||||
const benefits = companyData.culture_employment.benefits
|
||||
.map(benefit => `- ${benefit}`)
|
||||
.join('\n') || 'No benefits information available';
|
||||
|
||||
return `## 🏢 Culture & Employment
|
||||
|
||||
- **Employee Satisfaction:** ${companyData.culture_employment.employee_satisfaction || NOT_AVAILABLE}
|
||||
|
||||
### Benefits
|
||||
${benefits}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the sources section of the Markdown report
|
||||
* Creates a formatted section listing all sources used in the analysis with reliability ratings
|
||||
* @param {CompanyData} companyData - The company analysis data containing sources information
|
||||
* @returns {string} A formatted Markdown string for the sources section
|
||||
*/
|
||||
export function generateSourcesSection(companyData: CompanyData): string {
|
||||
const sources = companyData.sources
|
||||
.map((source, index) =>
|
||||
`${index + 1}. [${source.title}](${source.url}) - ${source.reliability} reliability - Accessed ${new Date(source.access_date).toLocaleDateString()}`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return `## 📚 Sources
|
||||
|
||||
${sources}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the footer section of the Markdown report
|
||||
* Creates a disclaimer and attribution for the automatically generated report
|
||||
* @returns {string} A formatted Markdown string for the report footer section
|
||||
*/
|
||||
export function generateFooter(): string {
|
||||
return `---
|
||||
|
||||
*This report was generated automatically using the Deep Research Professional skill. Information accuracy should be verified with official sources.*`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CSV report from company analysis data
|
||||
* Creates a tabular format with key company information organized by category
|
||||
* @param {CompanyData} companyData - The complete company analysis data to format into CSV
|
||||
* @returns {string} A formatted CSV string containing the company analysis data
|
||||
*/
|
||||
export function generateCSVReport(companyData: CompanyData): string {
|
||||
const headers = ['Category', 'Field', 'Value'];
|
||||
const rows = [
|
||||
['Basic Information', 'Company Name', companyData.基本信息.company_name],
|
||||
['Basic Information', 'Founded Date', companyData.基本信息.founded_date],
|
||||
['Basic Information', 'Headquarters', companyData.基本信息.headquarters],
|
||||
['Basic Information', 'Website', companyData.基本信息.website],
|
||||
['Leadership', 'CEO', companyData.leadership.ceo],
|
||||
['Financial', 'Revenue', companyData.financial.revenue],
|
||||
['Financial', 'Market Cap', companyData.financial.market_cap],
|
||||
['Financial', 'Profit Margin', companyData.financial.profit_margin],
|
||||
['Financial', 'Revenue Growth', companyData.financial.revenue_growth],
|
||||
['Market Position', 'Market Share', companyData.market_position.market_share],
|
||||
['Culture', 'Employee Satisfaction', companyData.culture_employment.employee_satisfaction],
|
||||
];
|
||||
|
||||
return [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
||||
}
|
||||
280
skills/deep-research/scripts/web-researcher.ts
Normal file
280
skills/deep-research/scripts/web-researcher.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Web Researcher - Web-based research automation
|
||||
* Handles web search and content extraction for research tasks
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
interface ResearchOptions {
|
||||
query: string;
|
||||
depth?: 'quick' | 'comprehensive' | 'recent';
|
||||
maxResults?: number;
|
||||
sources?: string[];
|
||||
excludeDomains?: string[];
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
title: string;
|
||||
snippet: string;
|
||||
url?: string;
|
||||
content?: string;
|
||||
source: string;
|
||||
relevanceScore: number;
|
||||
publishDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Researcher class for automating web-based research
|
||||
*/
|
||||
class WebResearcher {
|
||||
private readonly userAgent = 'Mozilla/5.0 (compatible; ResearchBot/1.0)';
|
||||
private readonly maxRetries = 3;
|
||||
private readonly requestDelay = 1000; // 1 second between requests
|
||||
|
||||
/**
|
||||
* Performs web research based on the provided options
|
||||
*
|
||||
* @param options - Research configuration options
|
||||
* @returns Array of search results
|
||||
*/
|
||||
async performResearch(options: ResearchOptions): Promise<SearchResult[]> {
|
||||
|
||||
try {
|
||||
// Simulate web search results
|
||||
const results = await this.simulateWebSearch(options);
|
||||
|
||||
// Filter and rank results
|
||||
const filteredResults = this.filterResults(results, options);
|
||||
const rankedResults = this.rankResults(filteredResults);
|
||||
|
||||
return rankedResults;
|
||||
} catch (error) {
|
||||
console.error('❌ Research failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates web search functionality
|
||||
* In a real implementation, this would use search APIs or web scraping
|
||||
*
|
||||
* @param options - Research options
|
||||
* @returns Simulated search results
|
||||
*/
|
||||
private async simulateWebSearch(options: ResearchOptions): Promise<SearchResult[]> {
|
||||
// Simulate API delay
|
||||
await this.delay(500);
|
||||
|
||||
const mockResults: SearchResult[] = [
|
||||
{
|
||||
title: `${options.query} - Wikipedia`,
|
||||
snippet: `Comprehensive information about ${options.query} including history, operations, and key facts.`,
|
||||
url: `https://en.wikipedia.org/wiki/${encodeURIComponent(options.query)}`,
|
||||
source: 'Wikipedia',
|
||||
relevanceScore: 0.9,
|
||||
publishDate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
title: `${options.query} Official Website`,
|
||||
snippet: `Official information about ${options.query}, including company profile, leadership, and recent news.`,
|
||||
url: `https://www.${options.query.toLowerCase().replace(/\s+/g, '')}.com`,
|
||||
source: 'Official Website',
|
||||
relevanceScore: 0.95,
|
||||
publishDate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
title: `${options.query} - Crunchbase Profile`,
|
||||
snippet: `Funding information, investors, and company details for ${options.query}.`,
|
||||
url: `https://www.crunchbase.com/organization/${options.query.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
source: 'Crunchbase',
|
||||
relevanceScore: 0.85,
|
||||
publishDate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
title: `${options.query} Financial Performance`,
|
||||
snippet: `Latest financial results, revenue data, and market performance for ${options.query}.`,
|
||||
url: `https://finance.yahoo.com/quote/${options.query.toUpperCase()}`,
|
||||
source: 'Yahoo Finance',
|
||||
relevanceScore: 0.8,
|
||||
publishDate: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
title: `${options.query} Industry Analysis`,
|
||||
snippet: `Market position, competitors, and industry analysis for ${options.query}.`,
|
||||
url: `https://www.industry-analysis.com/${options.query.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
source: 'Industry Analysis',
|
||||
relevanceScore: 0.75,
|
||||
publishDate: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
// Return subset based on maxResults
|
||||
const maxResults = options.maxResults || 10;
|
||||
return mockResults.slice(0, Math.min(maxResults, mockResults.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters search results based on research options
|
||||
*
|
||||
* @param results - Raw search results
|
||||
* @param options - Research options
|
||||
* @returns Filtered results
|
||||
*/
|
||||
private filterResults(results: SearchResult[], options: ResearchOptions): SearchResult[] {
|
||||
let filtered = [...results];
|
||||
|
||||
// Filter by excluded domains
|
||||
if (options.excludeDomains && options.excludeDomains.length > 0) {
|
||||
filtered = filtered.filter(result => {
|
||||
const url = result.url || '';
|
||||
return !options.excludeDomains!.some(domain => url.includes(domain));
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by specific sources
|
||||
if (options.sources && options.sources.length > 0) {
|
||||
filtered = filtered.filter(result =>
|
||||
options.sources!.some(source =>
|
||||
result.source.toLowerCase().includes(source.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by relevance score
|
||||
const minRelevance = this.getMinRelevanceScore(options.depth);
|
||||
filtered = filtered.filter(result => result.relevanceScore >= minRelevance);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranks search results by relevance and other factors
|
||||
*
|
||||
* @param results - Filtered search results
|
||||
* @returns Ranked search results
|
||||
*/
|
||||
private rankResults(results: SearchResult[]): SearchResult[] {
|
||||
return results.sort((a, b) => {
|
||||
// Primary sort: relevance score
|
||||
if (b.relevanceScore !== a.relevanceScore) {
|
||||
return b.relevanceScore - a.relevanceScore;
|
||||
}
|
||||
|
||||
// Secondary sort: publish date (more recent first)
|
||||
if (a.publishDate && b.publishDate) {
|
||||
return new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime();
|
||||
}
|
||||
|
||||
// Tertiary sort: source authority
|
||||
const sourceAuthority = this.getSourceAuthorityScore(a.source) - this.getSourceAuthorityScore(b.source);
|
||||
if (sourceAuthority !== 0) {
|
||||
return sourceAuthority;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum relevance score based on research depth
|
||||
*
|
||||
* @param depth - Research depth level
|
||||
* @returns Minimum relevance score threshold
|
||||
*/
|
||||
private getMinRelevanceScore(depth?: string): number {
|
||||
switch (depth) {
|
||||
case 'comprehensive':
|
||||
return 0.6;
|
||||
case 'recent':
|
||||
return 0.7;
|
||||
case 'quick':
|
||||
default:
|
||||
return 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets source authority score for ranking
|
||||
*
|
||||
* @param source - Source name
|
||||
* @returns Authority score
|
||||
*/
|
||||
private getSourceAuthorityScore(source: string): number {
|
||||
const authorityScores: Record<string, number> = {
|
||||
'Official Website': 10,
|
||||
'Wikipedia': 9,
|
||||
'Reuters': 9,
|
||||
'Bloomberg': 9,
|
||||
'Yahoo Finance': 8,
|
||||
'Crunchbase': 8,
|
||||
'Industry Analysis': 7,
|
||||
'News': 6,
|
||||
};
|
||||
|
||||
return authorityScores[source] || 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches full content from a URL
|
||||
* In a real implementation, this would perform HTTP requests and content extraction
|
||||
*
|
||||
* @param url - URL to fetch content from
|
||||
* @returns Extracted content
|
||||
*/
|
||||
async fetchContent(url: string): Promise<string> {
|
||||
// Simulate content fetching
|
||||
await this.delay(this.requestDelay);
|
||||
|
||||
// Return mock content
|
||||
return `This is the extracted content from ${url}. In a real implementation, this would contain the actual web page content after processing and cleaning.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the researcher and cleans up resources
|
||||
*/
|
||||
close(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to create delays
|
||||
*
|
||||
* @param ms - Milliseconds to delay
|
||||
* @returns Promise that resolves after the delay
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique research session ID
|
||||
*
|
||||
* @returns Unique session ID
|
||||
*/
|
||||
generateSessionId(): string {
|
||||
return `research_${randomUUID()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates research options
|
||||
*
|
||||
* @param options - Research options to validate
|
||||
* @throws Error if options are invalid
|
||||
*/
|
||||
private validateOptions(options: ResearchOptions): void {
|
||||
if (!options.query || options.query.trim().length === 0) {
|
||||
throw new Error('Query is required and cannot be empty');
|
||||
}
|
||||
|
||||
if (options.maxResults && (options.maxResults < 1 || options.maxResults > 100)) {
|
||||
throw new Error('Max results must be between 1 and 100');
|
||||
}
|
||||
|
||||
if (options.depth && !['quick', 'comprehensive', 'recent'].includes(options.depth)) {
|
||||
throw new Error('Depth must be one of: quick, comprehensive, recent');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { WebResearcher, type ResearchOptions, type SearchResult };
|
||||
Reference in New Issue
Block a user