@claude-vector/cli
Version:
CLI for Claude-integrated vector search
576 lines (480 loc) • 16.9 kB
JavaScript
/**
* 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}`);
}
}
}