#!/usr/bin/env node /** * KATASHIRO CLI - AI Research & Analysis Tool * * @requirement REQ-CLI-001 * @design DES-KATASHIRO-001 §2.6 CLI Interface */ import { Command } from 'commander'; import { createRequire } from 'module'; import { mkdir, writeFile } from 'fs/promises'; import { dirname, join } from 'path'; import { WebSearchClient, WebScraper, TextAnalyzer, EntityExtractor, SummaryGenerator, isOk, isErr, } from './index.js'; import { ResearchEngine, createProviderFactory, createLMReasoning, FetchLMProvider, } from '@nahisaho/katashiro-collector'; import type { ResearchReport, ResearchEvent, Reference } from '@nahisaho/katashiro-collector'; import { createContent, isValidFormat, isValidProvider, parseNumberOption, formatSearchResult, formatError, truncateText } from './cli-helpers.js'; // package.json からバージョンを取得 const require = createRequire(import.meta.url); const pkg = require('../package.json'); /** * ファイル保存(ディレクトリがなければ自動作成) */ async function saveReport(filePath: string, content: string): Promise { const dir = dirname(filePath); await mkdir(dir, { recursive: true }); await writeFile(filePath, content, 'utf-8'); } /** * 参照IDマップを作成(URLから参照IDへ) */ function buildReferenceMap(references: Reference[]): Map { const map = new Map(); for (const ref of references) { map.set(ref.url, ref.id); } return map; } /** * Deep Research v3.0.0 結果をMarkdownレポートに変換(引用付き) */ function generateMarkdownReportV3(report: ResearchReport): string { const lines: string[] = []; const now = new Date().toISOString().split('T')[0]; const referenceMap = buildReferenceMap(report.references); lines.push(`# ${report.query}`); lines.push(''); lines.push(`> Generated by KATASHIRO Deep Research v3.0.0 on ${now}`); lines.push(''); // 統計 lines.push('## 📈 統計'); lines.push(''); lines.push(`| 項目 | 値 |`); lines.push(`| --- | --- |`); lines.push(`| イテレーション | ${report.metadata.iterations} |`); lines.push(`| 使用トークン | ${report.metadata.tokensUsed} |`); lines.push(`| 実行時間 | ${(report.metadata.durationMs / 1000).toFixed(1)}秒 |`); lines.push(`| 信頼度 | ${(report.metadata.confidence * 100).toFixed(0)}% |`); lines.push(`| 収集知識 | ${report.metadata.knowledgeCount}件 |`); lines.push(`| 訪問URL | ${report.metadata.urlsVisited}件 |`); lines.push(''); // 概要 lines.push('## 📝 概要'); lines.push(''); lines.push(report.summary); lines.push(''); // 主要な発見(引用付き) if (report.findings.length > 0) { lines.push('## 📋 主要な発見'); lines.push(''); lines.push('> 各発見事項には引用元を `[ref-N]` 形式で明記しています。'); lines.push(''); for (let i = 0; i < report.findings.length; i++) { const finding = report.findings[i]; if (!finding) continue; // 引用を挿入 const citations = finding.citations .map((url: string) => referenceMap.get(url)) .filter(Boolean) .map((id: string | undefined) => `[${id}]`) .join(' '); lines.push(`### ${i + 1}. 発見事項`); lines.push(''); lines.push(`${finding.statement}`); if (citations) { lines.push(''); lines.push(`**引用元:** ${citations}`); } lines.push(''); lines.push(`*信頼度: ${(finding.confidence * 100).toFixed(0)}%*`); lines.push(''); } } // 選択肢 if (report.options.length > 0) { lines.push('## 🔀 選択肢'); lines.push(''); for (const option of report.options) { lines.push(`### ${option.name}`); lines.push(''); lines.push(option.description); lines.push(''); } } // 推奨事項 if (report.recommendations.length > 0) { lines.push('## 💡 推奨事項'); lines.push(''); for (const rec of report.recommendations) { lines.push(`${rec.priority}. **${rec.recommendation}**`); lines.push(` - 理由: ${rec.rationale}`); lines.push(''); } } // 参考文献(引用リスト) lines.push('## 📚 参考文献'); lines.push(''); lines.push('> 本レポートで使用した情報源の一覧です。本文中の `[ref-N]` は以下の参考文献を示しています。'); lines.push(''); for (const ref of report.references) { lines.push(`- **[${ref.id}]** [${ref.title}](${ref.url}) (アクセス: ${ref.accessDate})`); } lines.push(''); return lines.join('\n'); } const program = new Command(); program .name('katashiro') .description('KATASHIRO CLI - AI Research & Analysis Tool') .version(pkg.version, '-v, --version', 'バージョンを表示'); // 検索コマンド program .command('search ') .description('Web検索を実行') .option('-n, --max ', '結果の最大件数', '10') .option('-p, --provider ', '検索プロバイダー (duckduckgo|searxng)', 'duckduckgo') .option('-f, --format ', '出力形式 (json|text)', 'text') .action(async (query: string, options: { max: string; provider: string; format: string }) => { try { const client = new WebSearchClient(); const provider = options.provider as 'duckduckgo' | 'searxng'; const results = await client.search(query, { maxResults: parseInt(options.max, 10), provider }); if (options.format === 'json') { console.log(JSON.stringify(results, null, 2)); } else { console.log(`\n🔍 "${query}" の検索結果: ${results.length}件\n`); for (const r of results) { console.log(`📄 ${r.title}`); console.log(` ${r.url}`); if (r.snippet) { console.log(` ${r.snippet.substring(0, 100)}...`); } console.log(); } } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); process.exit(1); } }); // スクレイピングコマンド program .command('scrape ') .description('Webページの内容を取得') .option('-f, --format ', '出力形式 (json|text)', 'text') .option('-s, --summary', '要約を表示', false) .action(async (url: string, options: { format: string; summary: boolean }) => { try { const scraper = new WebScraper(); const result = await scraper.scrape(url); if (isErr(result)) { console.error('❌ スクレイプエラー:', result.error.message); process.exit(1); } const page = result.value; if (options.format === 'json') { console.log(JSON.stringify(page, null, 2)); } else { console.log(`\n📄 ${page.title}`); console.log(`🔗 ${page.url}`); console.log('\n---\n'); if (options.summary) { const summarizer = new SummaryGenerator(); const content = createContent(page.title, page.content, page.url); const summary = await summarizer.generateSummary(content, { maxLength: 500 }); if (isOk(summary)) { console.log('📝 要約:\n'); console.log(summary.value); } } else { console.log(page.content.substring(0, 2000)); if (page.content.length > 2000) { console.log(`\n... (${page.content.length - 2000}文字省略)`); } } } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); process.exit(1); } }); // 分析コマンド program .command('analyze ') .description('テキストを分析(キーワード・感情分析)') .option('-f, --format ', '出力形式 (json|text)', 'text') .action(async (text: string, options: { format: string }) => { try { const analyzer = new TextAnalyzer(); const result = await analyzer.analyze(text); if (options.format === 'json') { console.log(JSON.stringify(result, null, 2)); } else { console.log('\n📊 テキスト分析結果\n'); console.log(`🔑 キーワード: ${result.keywords.join(', ')}`); console.log(`💬 感情: ${result.sentiment.sentiment}`); console.log(`📈 感情スコア: ${result.sentiment.score.toFixed(2)}`); console.log(`📝 複雑度: ${result.complexity.level} (${result.complexity.score})`); console.log(`📄 単語数: ${result.wordCount}`); console.log(`📄 文数: ${result.sentenceCount}`); } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); process.exit(1); } }); // エンティティ抽出コマンド program .command('extract ') .description('テキストからエンティティを抽出') .option('-f, --format ', '出力形式 (json|text)', 'text') .action(async (text: string, options: { format: string }) => { try { const extractor = new EntityExtractor(); const entities = await extractor.extract(text); if (options.format === 'json') { console.log(JSON.stringify(entities, null, 2)); } else { console.log('\n🔍 エンティティ抽出結果\n'); if (entities.persons.length > 0) { console.log(`👤 人名: ${entities.persons.join(', ')}`); } if (entities.organizations.length > 0) { console.log(`🏢 組織: ${entities.organizations.join(', ')}`); } if (entities.locations.length > 0) { console.log(`📍 場所: ${entities.locations.join(', ')}`); } if (entities.dates.length > 0) { console.log(`📅 日付: ${entities.dates.join(', ')}`); } if (entities.urls.length > 0) { console.log(`🔗 URL: ${entities.urls.join(', ')}`); } if (entities.emails.length > 0) { console.log(`📧 メール: ${entities.emails.join(', ')}`); } if (entities.phones.length > 0) { console.log(`📞 電話: ${entities.phones.join(', ')}`); } if (entities.money.length > 0) { console.log(`💰 金額: ${entities.money.join(', ')}`); } console.log(`\n📊 合計: ${entities.all.length}件のエンティティ`); } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); process.exit(1); } }); // 要約コマンド program .command('summarize ') .description('テキストを要約') .option('-l, --length ', '要約の最大文字数', '300') .option('-f, --format ', '出力形式 (json|text)', 'text') .action(async (text: string, options: { length: string; format: string }) => { try { const summarizer = new SummaryGenerator(); const content = createContent('CLI Input', text); const result = await summarizer.generateSummary(content, { maxLength: parseInt(options.length, 10) }); if (isErr(result)) { const error = result.error as Error; console.error('❌ 要約エラー:', error.message); process.exit(1); } const summary = result.value; if (options.format === 'json') { console.log(JSON.stringify({ summary, originalLength: text.length }, null, 2)); } else { console.log('\n📝 要約結果\n'); console.log(summary); console.log(`\n(元のテキスト: ${text.length}文字 → 要約: ${summary.length}文字)`); } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); process.exit(1); } }); // ============================================================================= // deep-research コマンド (v3.0.0 - MUSUBIX準拠) // ============================================================================= program .command('deep-research ') .description('Deep Research v3.0.0 - MUSUBIX準拠の反復的深掘り調査') .option('-i, --iterations ', '最大イテレーション数', '5') .option('-t, --tokens ', 'トークン予算', '15000') .option('--jina-key ', 'Jina API Key (JINA_API_KEY環境変数も可)') .option('--openai-key ', 'OpenAI API Key (OPENAI_API_KEY環境変数も可)') .option('--format ', '出力形式 (text/json)', 'text') .option('-o, --output ', 'レポート保存先 (デフォルト: ./Reports/.md)') .option('--debug', 'デバッグモード', false) .action(async (topic: string, options: { iterations: string; tokens: string; jinaKey?: string; openaiKey?: string; format: string; output?: string; debug?: boolean; }) => { try { const jinaApiKey = options.jinaKey || process.env.JINA_API_KEY; const openaiApiKey = options.openaiKey || process.env.OPENAI_API_KEY; const maxIterations = parseInt(options.iterations, 10); const tokenBudget = parseInt(options.tokens, 10); const debug = options.debug ?? false; console.log(`\n🔬 Deep Research v3.0.0: "${topic}"\n`); console.log('設定:'); console.log(` - 最大イテレーション: ${maxIterations}`); console.log(` - トークン予算: ${tokenBudget}`); console.log(` - Jina API: ${jinaApiKey ? '✅ 設定済み' : '❌ 未設定 (DuckDuckGoにフォールバック)'}`); console.log(` - OpenAI API: ${openaiApiKey ? '✅ 設定済み' : '❌ 未設定 (テンプレートLMを使用)'}`); console.log('\n調査を開始しています...\n'); // プロバイダーファクトリーを設定 const providerFactory = createProviderFactory( jinaApiKey ? { jinaApiKey } : undefined, debug ); // LMReasoningを設定 const lmReasoning = openaiApiKey ? createLMReasoning({ provider: new FetchLMProvider( 'https://api.openai.com/v1/chat/completions', openaiApiKey, 'gpt-4o-mini' ), debug, }) : createLMReasoning({ debug }); // ResearchEngineを作成 const engine = new ResearchEngine({ providerFactory, lmReasoning, debug, }); const startTime = Date.now(); // イベントリスナーで進捗を表示 engine.on((event: ResearchEvent) => { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); switch (event.type) { case 'iteration_start': console.log(`📊 イテレーション ${event.iteration}/${maxIterations} (${elapsed}s)`); break; case 'search_complete': console.log(` 🔍 検索完了: ${event.data?.resultCount ?? 0}件`); break; case 'read_complete': console.log(` 📖 読取完了: ${event.data?.urlsRead ?? 0}URL, ${event.data?.factsExtracted ?? 0}件の事実`); break; case 'reason_complete': console.log(` 🧠 推論完了: ${event.data?.tokensUsed ?? 0}トークン使用`); break; case 'iteration_complete': const eval_ = event.data?.evaluation as { confidence?: number } | undefined; if (eval_) { console.log(` ✅ 信頼度: ${((eval_.confidence ?? 0) * 100).toFixed(0)}%`); } console.log(); break; case 'answer_found': const answerConf = (event.data?.confidence as number | undefined) ?? 0; console.log(`\n🎯 回答発見! 信頼度: ${(answerConf * 100).toFixed(0)}%\n`); break; case 'error': console.error(` ❌ エラー: ${event.data?.error ?? 'Unknown'}`); break; } }); // リサーチ実行 const report = await engine.research({ query: topic, maxIterations, tokenBudget, outputFormat: 'markdown', language: 'ja', }); const totalTimeMs = Date.now() - startTime; if (options.format === 'json') { console.log(JSON.stringify(report, null, 2)); } else { console.log('━'.repeat(60)); console.log('🎯 Deep Research 完了\n'); console.log('📈 統計:'); console.log(` - イテレーション: ${report.metadata.iterations}`); console.log(` - トークン使用量: ${report.metadata.tokensUsed}`); console.log(` - 実行時間: ${(totalTimeMs / 1000).toFixed(1)}秒`); console.log(` - 信頼度: ${(report.metadata.confidence * 100).toFixed(0)}%`); console.log(` - 収集知識: ${report.metadata.knowledgeCount}件`); console.log(` - 訪問URL: ${report.metadata.urlsVisited}件`); console.log('\n📝 概要:'); console.log(` ${report.summary.substring(0, 200)}${report.summary.length > 200 ? '...' : ''}`); console.log('\n📋 主要な発見(引用付き):'); const referenceMap = new Map(); for (const ref of report.references) { referenceMap.set(ref.url, ref.id); } report.findings.slice(0, 5).forEach((finding: { statement: string; citations: string[]; confidence: number }, i: number) => { const citations = finding.citations .map((url: string) => referenceMap.get(url)) .filter(Boolean) .map((id: string | undefined) => `[${id}]`) .join(' '); console.log(` ${i + 1}. ${finding.statement.substring(0, 80)}${finding.statement.length > 80 ? '...' : ''}`); if (citations) { console.log(` 引用: ${citations}`); } }); if (report.findings.length > 5) { console.log(` ... 他 ${report.findings.length - 5} 件`); } console.log('\n📚 参考文献:'); report.references.slice(0, 5).forEach((ref: Reference) => { console.log(` [${ref.id}] ${ref.title} (${ref.url})`); }); if (report.references.length > 5) { console.log(` ... 他 ${report.references.length - 5} 件`); } // レポートをファイルに保存 const sanitizedTopic = topic.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50); const outputPath = options.output || join('Reports', `${sanitizedTopic}.md`); // 引用付きMarkdownレポートを生成 const reportContent = generateMarkdownReportV3(report); await saveReport(outputPath, reportContent); console.log(`\n📁 レポート保存: ${outputPath}`); console.log('\n💡 ヒント:'); console.log(' - --jina-key または JINA_API_KEY で Jina AI 検索を使用'); console.log(' - --openai-key または OPENAI_API_KEY で GPT-4o-mini 推論を使用'); console.log(' - --format json で完全な結果を取得'); } } catch (error) { console.error('❌ エラー:', error instanceof Error ? error.message : error); if (options.debug) { console.error(error); } process.exit(1); } }); program.parse();