341 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			341 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import * as plugins from '../plugins.js';
 | 
						|
import * as fs from 'fs';
 | 
						|
import type {
 | 
						|
  IContextConfig,
 | 
						|
  ITrimConfig,
 | 
						|
  ITaskConfig,
 | 
						|
  TaskType,
 | 
						|
  ContextMode,
 | 
						|
  ICacheConfig,
 | 
						|
  IAnalyzerConfig,
 | 
						|
  IPrioritizationWeights,
 | 
						|
  ITierConfig
 | 
						|
} from './types.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * Manages configuration for context building
 | 
						|
 */
 | 
						|
export class ConfigManager {
 | 
						|
  private static instance: ConfigManager;
 | 
						|
  private config: IContextConfig;
 | 
						|
  private projectDir: string = '';
 | 
						|
  private configCache: { mtime: number; config: IContextConfig } | null = null;
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get the singleton instance of ConfigManager
 | 
						|
   */
 | 
						|
  public static getInstance(): ConfigManager {
 | 
						|
    if (!ConfigManager.instance) {
 | 
						|
      ConfigManager.instance = new ConfigManager();
 | 
						|
    }
 | 
						|
    return ConfigManager.instance;
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Private constructor for singleton pattern
 | 
						|
   */
 | 
						|
  private constructor() {
 | 
						|
    this.config = this.getDefaultConfig();
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Initialize the config manager with a project directory
 | 
						|
   * @param projectDir The project directory
 | 
						|
   */
 | 
						|
  public async initialize(projectDir: string): Promise<void> {
 | 
						|
    this.projectDir = projectDir;
 | 
						|
    await this.loadConfig();
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get the default configuration
 | 
						|
   */
 | 
						|
  private getDefaultConfig(): IContextConfig {
 | 
						|
    return {
 | 
						|
      maxTokens: 190000, // Default for o4-mini with some buffer
 | 
						|
      defaultMode: 'trimmed',
 | 
						|
      taskSpecificSettings: {
 | 
						|
        readme: {
 | 
						|
          mode: 'trimmed',
 | 
						|
          includePaths: ['ts/', 'src/'],
 | 
						|
          excludePaths: ['test/', 'node_modules/']
 | 
						|
        },
 | 
						|
        commit: {
 | 
						|
          mode: 'trimmed',
 | 
						|
          focusOnChangedFiles: true
 | 
						|
        },
 | 
						|
        description: {
 | 
						|
          mode: 'trimmed',
 | 
						|
          includePackageInfo: true
 | 
						|
        }
 | 
						|
      },
 | 
						|
      trimming: {
 | 
						|
        removeImplementations: true,
 | 
						|
        preserveInterfaces: true,
 | 
						|
        preserveTypeDefs: true,
 | 
						|
        preserveJSDoc: true,
 | 
						|
        maxFunctionLines: 5,
 | 
						|
        removeComments: true,
 | 
						|
        removeBlankLines: true
 | 
						|
      },
 | 
						|
      cache: {
 | 
						|
        enabled: true,
 | 
						|
        ttl: 3600, // 1 hour
 | 
						|
        maxSize: 100, // 100MB
 | 
						|
        directory: undefined // Will be set to .nogit/context-cache by ContextCache
 | 
						|
      },
 | 
						|
      analyzer: {
 | 
						|
        enabled: true,
 | 
						|
        useAIRefinement: false, // Disabled by default for now
 | 
						|
        aiModel: 'haiku'
 | 
						|
      },
 | 
						|
      prioritization: {
 | 
						|
        dependencyWeight: 0.3,
 | 
						|
        relevanceWeight: 0.4,
 | 
						|
        efficiencyWeight: 0.2,
 | 
						|
        recencyWeight: 0.1
 | 
						|
      },
 | 
						|
      tiers: {
 | 
						|
        essential: { minScore: 0.8, trimLevel: 'none' },
 | 
						|
        important: { minScore: 0.5, trimLevel: 'light' },
 | 
						|
        optional: { minScore: 0.2, trimLevel: 'aggressive' }
 | 
						|
      }
 | 
						|
    };
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Load configuration from npmextra.json
 | 
						|
   */
 | 
						|
  private async loadConfig(): Promise<void> {
 | 
						|
    try {
 | 
						|
      if (!this.projectDir) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
 | 
						|
 | 
						|
      // Check if file exists
 | 
						|
      const fileExists = await plugins.smartfile.fs.fileExists(npmextraJsonPath);
 | 
						|
      if (!fileExists) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Check cache
 | 
						|
      const stats = await fs.promises.stat(npmextraJsonPath);
 | 
						|
      const currentMtime = Math.floor(stats.mtimeMs);
 | 
						|
 | 
						|
      if (this.configCache && this.configCache.mtime === currentMtime) {
 | 
						|
        // Use cached config
 | 
						|
        this.config = this.configCache.config;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Read the npmextra.json file
 | 
						|
      const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
 | 
						|
      const npmextraContent = JSON.parse(npmextraJsonFile.contents.toString());
 | 
						|
 | 
						|
      // Check for tsdoc context configuration
 | 
						|
      if (npmextraContent?.tsdoc?.context) {
 | 
						|
        // Merge with default config
 | 
						|
        this.config = this.mergeConfigs(this.config, npmextraContent.tsdoc.context);
 | 
						|
      }
 | 
						|
 | 
						|
      // Cache the config
 | 
						|
      this.configCache = {
 | 
						|
        mtime: currentMtime,
 | 
						|
        config: { ...this.config }
 | 
						|
      };
 | 
						|
    } catch (error) {
 | 
						|
      console.error('Error loading context configuration:', error);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Merge configurations, with userConfig taking precedence
 | 
						|
   * @param defaultConfig The default configuration
 | 
						|
   * @param userConfig The user configuration
 | 
						|
   */
 | 
						|
  private mergeConfigs(defaultConfig: IContextConfig, userConfig: Partial<IContextConfig>): IContextConfig {
 | 
						|
    const result: IContextConfig = { ...defaultConfig };
 | 
						|
    
 | 
						|
    // Merge top-level properties
 | 
						|
    if (userConfig.maxTokens !== undefined) result.maxTokens = userConfig.maxTokens;
 | 
						|
    if (userConfig.defaultMode !== undefined) result.defaultMode = userConfig.defaultMode;
 | 
						|
    
 | 
						|
    // Merge task-specific settings
 | 
						|
    if (userConfig.taskSpecificSettings) {
 | 
						|
      result.taskSpecificSettings = result.taskSpecificSettings || {};
 | 
						|
      
 | 
						|
      // For each task type, merge settings
 | 
						|
      (['readme', 'commit', 'description'] as TaskType[]).forEach(taskType => {
 | 
						|
        if (userConfig.taskSpecificSettings?.[taskType]) {
 | 
						|
          result.taskSpecificSettings![taskType] = {
 | 
						|
            ...result.taskSpecificSettings![taskType],
 | 
						|
            ...userConfig.taskSpecificSettings[taskType]
 | 
						|
          };
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Merge trimming configuration
 | 
						|
    if (userConfig.trimming) {
 | 
						|
      result.trimming = {
 | 
						|
        ...result.trimming,
 | 
						|
        ...userConfig.trimming
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    // Merge cache configuration
 | 
						|
    if (userConfig.cache) {
 | 
						|
      result.cache = {
 | 
						|
        ...result.cache,
 | 
						|
        ...userConfig.cache
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    // Merge analyzer configuration
 | 
						|
    if (userConfig.analyzer) {
 | 
						|
      result.analyzer = {
 | 
						|
        ...result.analyzer,
 | 
						|
        ...userConfig.analyzer
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    // Merge prioritization weights
 | 
						|
    if (userConfig.prioritization) {
 | 
						|
      result.prioritization = {
 | 
						|
        ...result.prioritization,
 | 
						|
        ...userConfig.prioritization
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    // Merge tier configuration
 | 
						|
    if (userConfig.tiers) {
 | 
						|
      result.tiers = {
 | 
						|
        ...result.tiers,
 | 
						|
        ...userConfig.tiers
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get the complete configuration
 | 
						|
   */
 | 
						|
  public getConfig(): IContextConfig {
 | 
						|
    return this.config;
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get the trimming configuration
 | 
						|
   */
 | 
						|
  public getTrimConfig(): ITrimConfig {
 | 
						|
    return this.config.trimming || {};
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get configuration for a specific task
 | 
						|
   * @param taskType The type of task
 | 
						|
   */
 | 
						|
  public getTaskConfig(taskType: TaskType): ITaskConfig {
 | 
						|
    // Get task-specific config or empty object
 | 
						|
    const taskConfig = this.config.taskSpecificSettings?.[taskType] || {};
 | 
						|
    
 | 
						|
    // If mode is not specified, use default mode
 | 
						|
    if (!taskConfig.mode) {
 | 
						|
      taskConfig.mode = this.config.defaultMode;
 | 
						|
    }
 | 
						|
    
 | 
						|
    return taskConfig;
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Get the maximum tokens allowed for context
 | 
						|
   */
 | 
						|
  public getMaxTokens(): number {
 | 
						|
    return this.config.maxTokens || 190000;
 | 
						|
  }
 | 
						|
  
 | 
						|
  /**
 | 
						|
   * Update the configuration
 | 
						|
   * @param config The new configuration
 | 
						|
   */
 | 
						|
  public async updateConfig(config: Partial<IContextConfig>): Promise<void> {
 | 
						|
    // Merge with existing config
 | 
						|
    this.config = this.mergeConfigs(this.config, config);
 | 
						|
 | 
						|
    // Invalidate cache
 | 
						|
    this.configCache = null;
 | 
						|
 | 
						|
    try {
 | 
						|
      if (!this.projectDir) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Read the existing npmextra.json file
 | 
						|
      const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
 | 
						|
      let npmextraContent = {};
 | 
						|
 | 
						|
      if (await plugins.smartfile.fs.fileExists(npmextraJsonPath)) {
 | 
						|
        const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
 | 
						|
        npmextraContent = JSON.parse(npmextraJsonFile.contents.toString()) || {};
 | 
						|
      }
 | 
						|
 | 
						|
      // Update the tsdoc context configuration
 | 
						|
      const typedContent = npmextraContent as any;
 | 
						|
      if (!typedContent.tsdoc) typedContent.tsdoc = {};
 | 
						|
      typedContent.tsdoc.context = this.config;
 | 
						|
 | 
						|
      // Write back to npmextra.json
 | 
						|
      const updatedContent = JSON.stringify(npmextraContent, null, 2);
 | 
						|
      await plugins.smartfile.memory.toFs(updatedContent, npmextraJsonPath);
 | 
						|
    } catch (error) {
 | 
						|
      console.error('Error updating context configuration:', error);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get cache configuration
 | 
						|
   */
 | 
						|
  public getCacheConfig(): ICacheConfig {
 | 
						|
    return this.config.cache || { enabled: true, ttl: 3600, maxSize: 100 };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get analyzer configuration
 | 
						|
   */
 | 
						|
  public getAnalyzerConfig(): IAnalyzerConfig {
 | 
						|
    return this.config.analyzer || { enabled: true, useAIRefinement: false, aiModel: 'haiku' };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get prioritization weights
 | 
						|
   */
 | 
						|
  public getPrioritizationWeights(): IPrioritizationWeights {
 | 
						|
    return this.config.prioritization || {
 | 
						|
      dependencyWeight: 0.3,
 | 
						|
      relevanceWeight: 0.4,
 | 
						|
      efficiencyWeight: 0.2,
 | 
						|
      recencyWeight: 0.1
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get tier configuration
 | 
						|
   */
 | 
						|
  public getTierConfig(): ITierConfig {
 | 
						|
    return this.config.tiers || {
 | 
						|
      essential: { minScore: 0.8, trimLevel: 'none' },
 | 
						|
      important: { minScore: 0.5, trimLevel: 'light' },
 | 
						|
      optional: { minScore: 0.2, trimLevel: 'aggressive' }
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Clear the config cache (force reload on next access)
 | 
						|
   */
 | 
						|
  public clearCache(): void {
 | 
						|
    this.configCache = null;
 | 
						|
  }
 | 
						|
} |