UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

576 lines (480 loc) 16.9 kB
/** * WatchManager - パラメトリック思考による適応的ファイル監視システム * * 設計原則: * - ゼロコンフィグ: 設定なしで即座に動作 * - 責務分離: 監視専用、検索機能は分離 * - 適応的制御: 状況に応じた動的調整 * - 段階的複雑性: シンプル→高機能 */ import { EventEmitter } from 'events'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { AdvancedVectorEngine, IncrementalIndexer } from '@claude-vector/core'; import { AdaptiveThrottler } from './adaptive-throttler.js'; import { ProcessController } from './process-controller.js'; import { FeedbackSystem } from './feedback-system.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * ゼロコンフィグ原則によるデフォルト設定 */ const ZERO_CONFIG_DEFAULTS = { // ファイル監視設定 watchPatterns: ['**/*.{js,jsx,ts,tsx,md}'], ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '.next/**'], // パフォーマンス設定(保守的開始) batchSize: 5, // 保守的バッチサイズ maxConcurrent: 3, // API制限考慮 debounceMs: 500, // ファイル変更のデバウンス adaptiveThrottling: true, // 自動調整有効 // フィードバック設定 feedbackLevel: 'normal', // 'silent', 'normal', 'verbose' logToFile: true, // ログファイル出力 logFile: '.claude-watch.log', // 安全性設定 maxFiles: 10000, // 監視ファイル数制限 memoryThreshold: '500MB', // メモリ使用量制限 // AI最適化設定 aiOptimization: true, semanticChunking: true, phaseAdaptation: true, claudeOptimization: true }; export class WatchManager extends EventEmitter { constructor(options = {}) { super(); // パラメトリック設定: 多次元要因を考慮した設定統合 this.config = this.mergeConfigs(ZERO_CONFIG_DEFAULTS, options); this.projectRoot = null; this.isRunning = false; this.isInitialized = false; // 状態管理 this.state = { startTime: null, filesWatched: 0, changesProcessed: 0, batchesCompleted: 0, errorsEncountered: 0, lastActivity: null }; // コンポーネント初期化 this.vectorEngine = null; this.incrementalIndexer = null; this.throttler = new AdaptiveThrottler(this.config); this.processController = new ProcessController(this.config); this.feedbackSystem = new FeedbackSystem(this.config); this.setupEventHandlers(); } /** * 多次元設定統合(パラメトリック思考) */ mergeConfigs(defaults, overrides) { const merged = { ...defaults }; // 階層的設定統合 for (const [key, value] of Object.entries(overrides)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { merged[key] = { ...merged[key], ...value }; } else { merged[key] = value; } } // 制約検証 if (merged.maxFiles > 50000) { console.warn('⚠️ Very large file count may impact performance'); } if (merged.batchSize > 20) { console.warn('⚠️ Large batch size may exceed API rate limits'); } return merged; } /** * 初期化(ゼロコンフィグ原則) */ async initialize(projectRoot) { if (this.isInitialized) { return; } try { this.projectRoot = projectRoot; this.log('info', '🔍 Initializing WatchManager...'); this.log('info', `📁 Project: ${projectRoot}`); // AdvancedVectorEngine の初期化 this.vectorEngine = new AdvancedVectorEngine({ // AI最適化設定 aiOptimization: this.config.aiOptimization, semanticChunking: this.config.semanticChunking, phaseAdaptation: this.config.phaseAdaptation, claudeOptimization: this.config.claudeOptimization, verboseLogging: this.config.feedbackLevel === 'verbose', openaiApiKey: process.env.OPENAI_API_KEY, // 監視モード専用設定 incrementalIndexing: true, realTimeUpdates: true }); await this.vectorEngine.initialize(projectRoot, { // IncrementalIndexer詳細設定 incrementalIndexing: { watchPatterns: this.config.watchPatterns, ignorePatterns: this.config.ignorePatterns, debounceMs: this.config.debounceMs, batchSize: this.config.batchSize, enableRealtime: true } }); // IncrementalIndexer の取得 this.incrementalIndexer = this.vectorEngine.incrementalIndexer; if (this.incrementalIndexer) { this.setupIncrementalIndexerEvents(); } // 他のコンポーネントの初期化 await this.processController.initialize(projectRoot); await this.feedbackSystem.initialize(projectRoot); // コンポーネント間の連携設定 this.setupComponentIntegration(); this.isInitialized = true; this.log('info', '✅ WatchManager initialized successfully'); this.emit('initialized', { projectRoot, config: this.config, features: { incrementalIndexing: !!this.incrementalIndexer, adaptiveThrottling: this.config.adaptiveThrottling } }); } catch (error) { this.log('error', `❌ Failed to initialize WatchManager: ${error.message}`); throw error; } } /** * 監視開始(段階的複雑性) */ async start(options = {}) { if (!this.isInitialized) { throw new Error('WatchManager not initialized. Call initialize() first.'); } if (this.isRunning) { this.log('warn', '⚠️ WatchManager is already running'); return; } try { this.log('info', '🚀 Starting file monitoring...'); // 設定の動的更新 const runtimeConfig = { ...this.config, ...options }; // 監視対象ファイルの分析 const fileStats = await this.analyzeProject(); this.state.filesWatched = fileStats.totalFiles; this.log('info', `👀 Watching ${fileStats.totalFiles} files`); if (this.config.feedbackLevel === 'verbose') { this.log('info', `🔧 Configuration:`); this.log('info', ` • Patterns: ${this.config.watchPatterns.join(', ')}`); this.log('info', ` • Ignored: ${this.config.ignorePatterns.join(', ')}`); this.log('info', ` • Batch size: ${this.config.batchSize}`); this.log('info', ` • Max concurrent: ${this.config.maxConcurrent}`); } // 既存インデックスの確認・読み込み await this.loadOrCreateIndex(); this.isRunning = true; this.state.startTime = new Date(); this.state.lastActivity = new Date(); this.log('info', `📝 Logging to: ${this.config.logFile}`); this.log('info', '⚡ Ready - monitoring changes...'); this.log('info', '[Ctrl+C to stop]'); this.emit('started', { config: runtimeConfig, fileStats, startTime: this.state.startTime }); } catch (error) { this.log('error', `❌ Failed to start monitoring: ${error.message}`); this.isRunning = false; throw error; } } /** * 安全停止 */ async stop() { if (!this.isRunning) { this.log('warn', '⚠️ WatchManager is not running'); return; } try { this.log('info', '🛑 Stopping file monitoring...'); // 進行中の処理の完了を待機 if (this.incrementalIndexer) { this.log('info', '⏳ Waiting for pending operations...'); // TODO: 進行中のバッチ処理の完了を待機 } this.isRunning = false; // 最終統計の表示 const duration = Date.now() - this.state.startTime.getTime(); const durationStr = this.formatDuration(duration); this.log('info', `📊 Session Summary:`); this.log('info', ` • Duration: ${durationStr}`); this.log('info', ` • Files watched: ${this.state.filesWatched}`); this.log('info', ` • Changes processed: ${this.state.changesProcessed}`); this.log('info', ` • Batches completed: ${this.state.batchesCompleted}`); this.log('info', ` • Errors: ${this.state.errorsEncountered}`); this.log('info', '👋 Monitoring stopped safely'); this.emit('stopped', { duration, stats: { ...this.state } }); } catch (error) { this.log('error', `❌ Error during shutdown: ${error.message}`); throw error; } } /** * 状態取得 */ async getStatus() { const status = { isRunning: this.isRunning, isInitialized: this.isInitialized, projectRoot: this.projectRoot, startTime: this.state.startTime, uptime: this.state.startTime ? Date.now() - this.state.startTime.getTime() : 0, config: this.config, stats: { ...this.state } }; // メモリ使用量の取得 if (global.gc) { global.gc(); } const memUsage = process.memoryUsage(); status.memory = { rss: Math.round(memUsage.rss / 1024 / 1024), heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) }; return status; } /** * プロジェクト分析 */ async analyzeProject() { // TODO: より詳細な分析を実装 // 現在は簡単な実装 return { totalFiles: 1000, // プレースホルダー supportedFiles: 800, ignoredFiles: 200 }; } /** * インデックス読み込みまたは作成 */ async loadOrCreateIndex() { try { const indexPath = path.join(this.projectRoot, '.claude-code-index'); if (fsSync.existsSync(indexPath)) { this.log('info', '📂 Loading existing index...'); await this.vectorEngine.loadIndex( path.join(indexPath, 'embeddings.json'), path.join(indexPath, 'chunks.json'), path.join(indexPath, 'metadata.json') ); this.log('info', '✅ Existing index loaded'); } else { this.log('info', '🔨 Creating initial index...'); // 初期インデックス作成 const startTime = Date.now(); await this.vectorEngine.indexProject(); const duration = Date.now() - startTime; this.log('info', `✅ Initial index created in ${(duration/1000).toFixed(1)}s`); } } catch (error) { this.log('error', `❌ Index operation failed: ${error.message}`); throw error; } } /** * IncrementalIndexer イベント設定 */ setupIncrementalIndexerEvents() { if (!this.incrementalIndexer) return; this.incrementalIndexer.on('file-changed', (event) => { this.state.changesProcessed++; this.state.lastActivity = new Date(); const relativeFile = path.relative(this.projectRoot, event.filePath); this.log('info', `📝 File changed: ${relativeFile}`); this.emit('file-changed', event); }); this.incrementalIndexer.on('batch-processed', (event) => { this.state.batchesCompleted++; if (this.config.feedbackLevel !== 'silent') { this.log('info', `✓ Batch complete: ${event.count} files processed`); } this.emit('batch-processed', event); }); this.incrementalIndexer.on('error', (error) => { this.state.errorsEncountered++; this.log('error', `❌ Indexing error: ${error.message}`); this.emit('error', error); }); } /** * 基本イベントハンドラ設定 */ setupEventHandlers() { // プロセス終了ハンドリング process.on('SIGINT', async () => { if (this.isRunning) { console.log('\n🛑 Received SIGINT, stopping gracefully...'); await this.stop(); } process.exit(0); }); process.on('SIGTERM', async () => { if (this.isRunning) { console.log('\n🛑 Received SIGTERM, stopping gracefully...'); await this.stop(); } process.exit(0); }); } /** * ログ出力(段階的フィードバック) */ log(level, message) { // FeedbackSystemが利用可能な場合はそれを使用 if (this.feedbackSystem && this.feedbackSystem.state?.isActive) { this.feedbackSystem.log(level, message); } else { // フォールバック: 基本的なログ出力 const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, pid: process.pid }; // コンソール出力制御 if (this.config.feedbackLevel !== 'silent') { if (level === 'error' || this.config.feedbackLevel === 'verbose') { console.log(message); } else if (this.config.feedbackLevel === 'normal' && level === 'info') { console.log(message); } } // ログファイル出力 if (this.config.logToFile) { this.writeToLogFile(logEntry); } this.emit('log', logEntry); } } /** * ログファイル書き込み */ async writeToLogFile(logEntry) { try { const logPath = path.join(this.projectRoot || process.cwd(), this.config.logFile); const logLine = JSON.stringify(logEntry) + '\n'; await fs.appendFile(logPath, logLine); } catch (error) { // ログ書き込みエラーは無視(無限ループ防止) } } /** * 期間フォーマット */ formatDuration(ms) { const seconds = Math.floor(ms / 1000) % 60; const minutes = Math.floor(ms / (1000 * 60)) % 60; const hours = Math.floor(ms / (1000 * 60 * 60)); if (hours > 0) { return `${hours}h ${minutes}m ${seconds}s`; } else if (minutes > 0) { return `${minutes}m ${seconds}s`; } else { return `${seconds}s`; } } /** * 設定更新(動的設定変更) */ updateConfig(newConfig) { const oldConfig = { ...this.config }; this.config = this.mergeConfigs(this.config, newConfig); this.log('info', '🔧 Configuration updated'); this.emit('config-updated', { oldConfig, newConfig: this.config }); return this.config; } /** * 統計情報取得 */ getStats() { return { ...this.state, uptime: this.state.startTime ? Date.now() - this.state.startTime.getTime() : 0, isRunning: this.isRunning, config: this.config }; } /** * コンポーネント間の連携設定 */ setupComponentIntegration() { // FeedbackSystemのイベント連携 this.feedbackSystem.on('log', (logEntry) => { this.emit('log', logEntry); }); // ProcessControllerのイベント連携 this.processController.on('shutdown-start', async (event) => { this.log('info', '🛑 Graceful shutdown initiated...'); await this.stop(); }); this.processController.on('heartbeat', (event) => { // ヘルスチェック情報を記録 if (this.config.feedbackLevel === 'verbose') { this.log('debug', `💓 Heartbeat: ${event.memoryMB}MB memory, ${Math.round(event.uptime / 1000)}s uptime`); } }); // AdaptiveThrottlerのイベント連携 this.throttler.on('call-start', (event) => { if (this.feedbackSystem) { this.feedbackSystem.reportFileChange('API call started', { callId: event.callId }); } }); this.throttler.on('call-error', (event) => { this.state.errorsEncountered++; if (this.feedbackSystem) { this.feedbackSystem.reportError(event.error, { callId: event.callId }); } }); // IncrementalIndexerにThrottlerを統合 if (this.incrementalIndexer) { // API呼び出しをthrottlerを通すように設定 // Note: この部分は実際のIncrementalIndexerの実装に依存 } } /** * クリーンアップ */ async destroy() { try { if (this.isRunning) { await this.stop(); } // 各コンポーネントのクリーンアップ if (this.processController) { await this.processController.destroy(); } if (this.feedbackSystem) { await this.feedbackSystem.destroy(); } if (this.throttler) { this.throttler.destroy(); } this.emit('destroyed'); } catch (error) { this.log('error', `❌ Error during cleanup: ${error.message}`); } } }