UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

670 lines (561 loc) 17.1 kB
/** * FeedbackSystem - 段階的フィードバック制御システム * * 機能: * - 段階的フィードバック(silent/normal/verbose) * - 適応的出力制御(状況に応じた情報量調整) * - 構造化ログ出力(JSON形式) * - ログローテーション * - リアルタイム統計表示 * - カラー出力対応 */ import { EventEmitter } from 'events'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import chalk from 'chalk'; /** * フィードバックシステムのデフォルト設定 */ const FEEDBACK_DEFAULTS = { // フィードバックレベル level: 'normal', // 'silent', 'normal', 'verbose' // 出力制御 colorOutput: true, // カラー出力有効 enableEmojis: true, // 絵文字使用 showTimestamps: false, // タイムスタンプ表示 showProgress: true, // プログレス表示 // ログファイル設定 logToFile: true, // ログファイル出力 logFile: '.claude-watch.log', // ログファイル名 logFormat: 'json', // 'json' or 'text' // ログローテーション maxLogSize: 10 * 1024 * 1024, // 10MB maxLogFiles: 5, // 最大ファイル数 // 統計表示 showStatistics: true, // 統計情報表示 statisticsInterval: 30000, // 30秒間隔 // プログレス制御 progressThreshold: 5, // 5個以上で進捗表示 batchSummaryThreshold: 10, // 10個以上でサマリー表示 // 通知制御 notifications: false, // デスクトップ通知 soundAlerts: false // 音声アラート }; /** * フィードバックレベル定義 */ const FEEDBACK_LEVELS = { silent: { console: false, progress: false, statistics: false, logFile: true, errorOnly: true }, normal: { console: true, progress: true, statistics: false, logFile: true, errorOnly: false, summaryOnly: true }, verbose: { console: true, progress: true, statistics: true, logFile: true, errorOnly: false, summaryOnly: false, detailed: true } }; export class FeedbackSystem extends EventEmitter { constructor(config = {}) { super(); this.config = { ...FEEDBACK_DEFAULTS, ...config }; this.levelConfig = FEEDBACK_LEVELS[this.config.level] || FEEDBACK_LEVELS.normal; // 状態管理 this.state = { isActive: false, startTime: Date.now(), lastLogRotation: Date.now(), // 統計情報 messagesLogged: 0, errorsLogged: 0, warningsLogged: 0, infosLogged: 0, // プログレス管理 currentProgress: null, lastProgressUpdate: 0, // バッチ統計 currentBatch: { startTime: null, count: 0, errors: 0, totalTime: 0 } }; // ログファイルパス this.logFilePath = null; this.projectRoot = null; // タイマー this.statisticsTimer = null; this.progressTimer = null; // カラー設定 this.colors = { error: chalk.red, warn: chalk.yellow, info: chalk.cyan, success: chalk.green, debug: chalk.gray, progress: chalk.blue, statistics: chalk.magenta, timestamp: chalk.gray }; // 絵文字設定 this.emojis = { error: '❌', warn: '⚠️', info: 'ℹ️', success: '✅', progress: '🔄', statistics: '📊', watch: '👀', file: '📝', batch: '📦', time: '⏱️', memory: '💾' }; } /** * 初期化 */ async initialize(projectRoot) { this.projectRoot = projectRoot; this.logFilePath = path.join(projectRoot, this.config.logFile); // ログファイルの初期化 if (this.config.logToFile) { await this.initializeLogFile(); } // 統計タイマー開始 if (this.levelConfig.statistics && this.config.showStatistics) { this.startStatisticsTimer(); } this.state.isActive = true; this.emit('initialized', { projectRoot, logFile: this.logFilePath }); } /** * ログファイル初期化 */ async initializeLogFile() { try { // ログローテーションチェック await this.checkLogRotation(); // セッション開始ログ const sessionStart = { type: 'session_start', timestamp: new Date().toISOString(), pid: process.pid, config: this.config, projectRoot: this.projectRoot }; await this.writeToLogFile(sessionStart); } catch (error) { console.error('Failed to initialize log file:', error.message); } } /** * ログローテーション確認 */ async checkLogRotation() { if (!fsSync.existsSync(this.logFilePath)) { return; } const stats = await fs.stat(this.logFilePath); if (stats.size > this.config.maxLogSize) { await this.rotateLogFile(); } } /** * ログファイルローテーション */ async rotateLogFile() { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedPath = `${this.logFilePath}.${timestamp}`; // 現在のログファイルをリネーム await fs.rename(this.logFilePath, rotatedPath); // 古いログファイルを削除 await this.cleanupOldLogFiles(); this.state.lastLogRotation = Date.now(); } catch (error) { console.warn('Log rotation failed:', error.message); } } /** * 古いログファイル削除 */ async cleanupOldLogFiles() { try { const logDir = path.dirname(this.logFilePath); const logBasename = path.basename(this.logFilePath); const files = await fs.readdir(logDir); const logFiles = files .filter(file => file.startsWith(logBasename + '.')) .map(file => ({ name: file, path: path.join(logDir, file), stat: fsSync.statSync(path.join(logDir, file)) })) .sort((a, b) => b.stat.mtime - a.stat.mtime); // 新しい順 // 最大ファイル数を超える古いファイルを削除 const filesToDelete = logFiles.slice(this.config.maxLogFiles); for (const file of filesToDelete) { await fs.unlink(file.path); } } catch (error) { console.warn('Log cleanup failed:', error.message); } } /** * ファイル変更通知 */ reportFileChange(filePath, metadata = {}) { const relativePath = this.getRelativePath(filePath); const message = { type: 'file_change', file: relativePath, timestamp: new Date().toISOString(), metadata }; this.log('info', this.formatFileChangeMessage(relativePath, metadata), message); } /** * バッチ開始通知 */ reportBatchStart(count) { this.state.currentBatch = { startTime: Date.now(), count, errors: 0, totalTime: 0 }; if (count >= this.config.progressThreshold && this.levelConfig.progress) { const message = `${this.emoji('batch')} Processing batch: ${count} files...`; this.log('info', message, { type: 'batch_start', count }); } } /** * バッチ完了通知 */ reportBatchComplete(stats) { const duration = Date.now() - (this.state.currentBatch.startTime || Date.now()); this.state.currentBatch.totalTime = duration; const message = { type: 'batch_complete', timestamp: new Date().toISOString(), duration, ...stats }; if (this.levelConfig.summaryOnly || this.levelConfig.detailed) { const formattedMessage = this.formatBatchCompleteMessage(stats, duration); this.log('success', formattedMessage, message); } // 統計更新 this.updateBatchStatistics(stats); } /** * エラー通知 */ reportError(error, context = {}) { const message = { type: 'error', timestamp: new Date().toISOString(), error: error.message, stack: error.stack, context }; const formattedMessage = this.formatErrorMessage(error, context); this.log('error', formattedMessage, message); this.state.errorsLogged++; } /** * 警告通知 */ reportWarning(warning, context = {}) { const message = { type: 'warning', timestamp: new Date().toISOString(), warning, context }; const formattedMessage = this.formatWarningMessage(warning, context); this.log('warn', formattedMessage, message); this.state.warningsLogged++; } /** * 統計情報表示 */ showStatistics(stats) { if (!this.levelConfig.statistics) { return; } const formattedStats = this.formatStatistics(stats); this.log('statistics', formattedStats, { type: 'statistics', ...stats }); } /** * プログレス表示 */ showProgress(current, total, description = '') { if (!this.levelConfig.progress) { return; } const now = Date.now(); // プログレス更新頻度制限(500ms間隔) if (now - this.state.lastProgressUpdate < 500) { return; } this.state.lastProgressUpdate = now; this.state.currentProgress = { current, total, description }; const percentage = Math.round((current / total) * 100); const progressBar = this.createProgressBar(percentage); const message = `${this.emoji('progress')} ${description} ${progressBar} ${current}/${total} (${percentage}%)`; // プログレスは一時的な表示なのでログファイルには記録しない if (this.levelConfig.console) { this.outputToConsole('progress', message); } } /** * ログ出力メイン関数 */ log(level, message, data = null) { // レベル制御 if (!this.shouldOutput(level)) { return; } // コンソール出力 if (this.levelConfig.console && !this.levelConfig.errorOnly || level === 'error') { this.outputToConsole(level, message); } // ログファイル出力 if (this.config.logToFile) { const logEntry = { level, message: this.stripColors(message), timestamp: new Date().toISOString(), pid: process.pid, data }; this.writeToLogFile(logEntry); } // 統計更新 this.updateMessageStatistics(level); // イベント発行 this.emit('log', { level, message, data }); } /** * 出力制御判定 */ shouldOutput(level) { if (!this.state.isActive) { return false; } if (this.config.level === 'silent' && level !== 'error') { return false; } return true; } /** * コンソール出力 */ outputToConsole(level, message) { const coloredMessage = this.config.colorOutput ? this.applyColors(level, message) : message; const timestampedMessage = this.config.showTimestamps ? this.addTimestamp(coloredMessage) : coloredMessage; console.log(timestampedMessage); } /** * ログファイル書き込み */ async writeToLogFile(entry) { try { const logLine = this.config.logFormat === 'json' ? JSON.stringify(entry) + '\n' : this.formatTextLog(entry) + '\n'; await fs.appendFile(this.logFilePath, logLine); } catch (error) { // ログ書き込みエラーは無視(無限ループ防止) } } /** * メッセージフォーマット関数群 */ formatFileChangeMessage(filePath, metadata) { const emoji = this.emoji('file'); return `${emoji} File changed: ${filePath}`; } formatBatchCompleteMessage(stats, duration) { const emoji = this.emoji('success'); const durationStr = this.formatDuration(duration); return `${emoji} Batch complete: ${stats.count || 0} files, ${stats.chunks || 0} chunks (${durationStr})`; } formatErrorMessage(error, context) { const emoji = this.emoji('error'); const contextStr = Object.keys(context).length > 0 ? ` [${JSON.stringify(context)}]` : ''; return `${emoji} Error: ${error.message}${contextStr}`; } formatWarningMessage(warning, context) { const emoji = this.emoji('warn'); const contextStr = Object.keys(context).length > 0 ? ` [${JSON.stringify(context)}]` : ''; return `${emoji} Warning: ${warning}${contextStr}`; } formatStatistics(stats) { const emoji = this.emoji('statistics'); const uptime = this.formatDuration(Date.now() - this.state.startTime); return `${emoji} Statistics: ${stats.filesWatched || 0} files, ${stats.changesProcessed || 0} changes, uptime: ${uptime}`; } /** * ユーティリティ関数群 */ emoji(type) { return this.config.enableEmojis ? this.emojis[type] || '' : ''; } applyColors(level, message) { const colorFunc = this.colors[level]; return colorFunc ? colorFunc(message) : message; } stripColors(message) { // ANSI escape codes を除去 // eslint-disable-next-line no-control-regex return message.replace(/\x1b\[[0-9;]*m/g, ''); } addTimestamp(message) { const timestamp = this.colors.timestamp(`[${new Date().toISOString()}]`); return `${timestamp} ${message}`; } getRelativePath(filePath) { if (!this.projectRoot) { return filePath; } return path.relative(this.projectRoot, filePath); } formatDuration(ms) { if (ms < 1000) { return `${ms}ms`; } else if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } else { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); return `${minutes}m ${seconds}s`; } } createProgressBar(percentage, width = 20) { const filled = Math.round((percentage / 100) * width); const empty = width - filled; const bar = '█'.repeat(filled) + '░'.repeat(empty); return `[${bar}]`; } formatTextLog(entry) { return `${entry.timestamp} [${entry.level.toUpperCase()}] ${entry.message}`; } /** * 統計更新 */ updateMessageStatistics(level) { this.state.messagesLogged++; switch (level) { case 'error': this.state.errorsLogged++; break; case 'warn': this.state.warningsLogged++; break; case 'info': case 'success': this.state.infosLogged++; break; } } updateBatchStatistics(stats) { // バッチ統計の更新(必要に応じて実装) } /** * 統計タイマー */ startStatisticsTimer() { this.statisticsTimer = setInterval(() => { if (this.levelConfig.statistics) { const stats = this.getInternalStatistics(); this.showStatistics(stats); } }, this.config.statisticsInterval); } getInternalStatistics() { return { uptime: Date.now() - this.state.startTime, messagesLogged: this.state.messagesLogged, errorsLogged: this.state.errorsLogged, warningsLogged: this.state.warningsLogged, memoryUsage: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) }; } /** * 設定更新 */ updateConfig(newConfig) { const oldLevel = this.config.level; this.config = { ...this.config, ...newConfig }; this.levelConfig = FEEDBACK_LEVELS[this.config.level] || FEEDBACK_LEVELS.normal; // レベル変更時の処理 if (oldLevel !== this.config.level) { this.log('info', `Feedback level changed: ${oldLevel} → ${this.config.level}`); // 統計タイマーの制御 if (this.levelConfig.statistics && !this.statisticsTimer) { this.startStatisticsTimer(); } else if (!this.levelConfig.statistics && this.statisticsTimer) { clearInterval(this.statisticsTimer); this.statisticsTimer = null; } } this.emit('config-updated', this.config); } /** * 状態取得 */ getState() { return { ...this.state, config: this.config, levelConfig: this.levelConfig, logFilePath: this.logFilePath, uptime: Date.now() - this.state.startTime }; } /** * クリーンアップ */ async destroy() { this.state.isActive = false; if (this.statisticsTimer) { clearInterval(this.statisticsTimer); this.statisticsTimer = null; } if (this.progressTimer) { clearInterval(this.progressTimer); this.progressTimer = null; } // セッション終了ログ if (this.config.logToFile) { const sessionEnd = { type: 'session_end', timestamp: new Date().toISOString(), duration: Date.now() - this.state.startTime, statistics: this.getInternalStatistics() }; await this.writeToLogFile(sessionEnd); } this.emit('destroyed'); } }