@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
638 lines (573 loc) • 36.6 kB
text/typescript
import winston from "winston";
import * as fs from 'fs';
import * as path from 'path';
// Define a type for the model configuration loaded from file
interface ModelConfigFile {
SUGGESTION_MODEL?: string;
SUGGESTION_PROVIDER?: string;
EMBEDDING_PROVIDER?: string;
DEEPSEEK_API_KEY?: string;
DEEPSEEK_API_URL?: string;
OPENAI_API_KEY?: string;
GEMINI_API_KEY?: string;
CLAUDE_API_KEY?: string;
HTTP_PORT?: number; // Add this line
// Add other provider-specific keys here as needed
SUMMARIZATION_MODEL?: string; // New
REFINEMENT_MODEL?: string; // New
}
class ConfigService {
private static instance: ConfigService;
public readonly logger: winston.Logger;
// Configuration values with defaults
public readonly OLLAMA_HOST: string;
public readonly QDRANT_HOST: string;
public readonly COLLECTION_NAME: string;
private _llmProvider: string;
private _suggestionModel: string;
private _embeddingModel: string;
private _embeddingDimension: number; // New
private _deepSeekApiKey: string;
private _deepSeekApiUrl: string;
private _deepSeekModel: string;
private _deepSeekRpmLimit: number; // Requests Per Minute
private _agentQueryTimeout: number;
private _openAIApiKey: string;
private _geminiApiKey: string;
private _claudeApiKey: string;
private _qdrantSearchLimitDefault: number; // Added for Qdrant search limit
private _maxDiffLengthForContextTool: number;
private _agentDefaultMaxSteps: number;
private _agentAbsoluteMaxSteps: number;
private _maxRefinementIterations: number;
private _fileIndexingChunkSizeChars: number;
private _fileIndexingChunkOverlapChars: number;
private _summarizationModel: string;
private _refinementModel: string;
private _requestAdditionalContextMaxSearchResults: number;
private _maxFilesForSuggestionContextNoSummary: number;
private _maxSnippetLengthForContextNoSummary: number;
private _diffChunkSizeChars: number;
private _diffChunkOverlapChars: number;
private _commitHistoryMaxCountForIndexing: number; // 0 for all
private _qdrantBatchUpsertSize: number;
private _agentMaxContextItems: number;
private _diffLinesOfContext: number;
private _maxFileContentLengthForCapability: number;
private _maxDirListingEntriesForCapability: number;
private _httpPort: number; // Added
private _useMixedProviders: boolean;
private _suggestionProvider: string;
private _embeddingProvider: string;
public readonly MAX_INPUT_LENGTH: number;
public readonly MAX_SNIPPET_LENGTH: number;
public readonly REQUEST_TIMEOUT: number;
public readonly MAX_RETRIES: number;
public readonly RETRY_DELAY: number;
public readonly AGENT_QUERY_TIMEOUT_DEFAULT = 180000; // Default 3 minutes for agent queries
public readonly DEFAULT_QDRANT_SEARCH_LIMIT = 10; // Default Qdrant search limit
public readonly DEFAULT_MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY = 15; // Default max files before summarizing
public readonly DEFAULT_MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY = 1500; // Default 1500 chars
public readonly DEFAULT_AGENT_DEFAULT_MAX_STEPS = 10;
public readonly DEFAULT_AGENT_ABSOLUTE_MAX_STEPS = 15;
public readonly DEFAULT_MAX_REFINEMENT_ITERATIONS = 3;
public readonly DEFAULT_FILE_INDEXING_CHUNK_SIZE_CHARS = 1000;
public readonly DEFAULT_FILE_INDEXING_CHUNK_OVERLAP_CHARS = 200;
// For SUMMARIZATION_MODEL and REFINEMENT_MODEL, default will be SUGGESTION_MODEL if empty string.
public readonly DEFAULT_REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS = 20;
public readonly DEFAULT_DIFF_CHUNK_SIZE_CHARS = 1000;
public readonly DEFAULT_DIFF_CHUNK_OVERLAP_CHARS = 100;
public readonly DEFAULT_COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING = 0; // 0 means all commits
public readonly DEFAULT_QDRANT_BATCH_UPSERT_SIZE = 100;
public readonly DEFAULT_AGENT_MAX_CONTEXT_ITEMS = 10;
public readonly DEFAULT_DIFF_LINES_OF_CONTEXT = 3;
public readonly DEFAULT_MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY = 10000;
public readonly DEFAULT_EMBEDDING_DIMENSION = 768; // For nomic-embed-text
public readonly DEFAULT_MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY = 50;
public readonly DEFAULT_HTTP_PORT = 3001; // Added
public readonly DEEPSEEK_RPM_LIMIT_DEFAULT = 60; // Default RPM for DeepSeek
public readonly CONFIG_DIR: string;
public readonly MODEL_CONFIG_FILE: string;
public readonly DEEPSEEK_CONFIG_FILE: string;
public readonly LOG_DIR: string;
private constructor() {
// Initialize logger first
this.logger = winston.createLogger({
level: process.env.NODE_ENV === "test" ? "error" : "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// Log file transport will be added after LOG_DIR is determined
new winston.transports.Stream({
stream: process.stderr,
format: winston.format.simple(),
level: 'error',
silent: process.env.NODE_ENV === "test"
}),
],
});
this.CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.codecompass');
this.MODEL_CONFIG_FILE = path.join(this.CONFIG_DIR, 'model-config.json');
this.DEEPSEEK_CONFIG_FILE = path.join(this.CONFIG_DIR, 'deepseek-config.json');
this.LOG_DIR = path.join(this.CONFIG_DIR, 'logs');
// Ensure log directory exists
try {
if (!fs.existsSync(this.LOG_DIR)) {
fs.mkdirSync(this.LOG_DIR, { recursive: true });
}
} catch (error) {
// Fallback to local logs directory if user-specific one fails
// Use logger if available, but console.error is safer here as logger might not be fully configured
// Or, if logger is guaranteed to be partially working (e.g. stderr stream), use it.
// For now, let's assume console.error is fine for this bootstrap phase.
// If logger is used, it must be after its basic initialization.
// this.logger.error(...) would be ideal if the stderr transport is already active.
// Given the logger is initialized above, we can try using it.
this.logger.error(`Failed to create user-specific log directory: ${(error as Error).message}. Falling back to local logs dir.`);
this.LOG_DIR = path.join(process.cwd(), 'logs');
if (!fs.existsSync(this.LOG_DIR)) {
fs.mkdirSync(this.LOG_DIR, { recursive: true });
}
}
// Add the file transport now that LOG_DIR is determined
this.logger.add(new winston.transports.File({ filename: path.join(this.LOG_DIR, "codecompass.log") }));
// QDRANT_HOST and COLLECTION_NAME were previously initialized directly from process.env or defaults
// Let's align QDRANT_HOST with the validation pattern used for OLLAMA_HOST
const defaultQdrantHost = "http://127.0.0.1:6333";
const qdrantHostEnv = process.env.QDRANT_HOST;
if (qdrantHostEnv && qdrantHostEnv.trim() !== "") {
try {
const parsedUrl = new URL(qdrantHostEnv);
if (parsedUrl.protocol && (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')) {
this.QDRANT_HOST = qdrantHostEnv;
} else {
this.logger.warn(`QDRANT_HOST environment variable "${qdrantHostEnv}" has an invalid or missing protocol. Falling back to default: ${defaultQdrantHost}`);
this.QDRANT_HOST = defaultQdrantHost;
}
} catch (e) {
this.logger.warn(`QDRANT_HOST environment variable "${qdrantHostEnv}" is not a valid URL. Error: ${(e as Error).message}. Falling back to default: ${defaultQdrantHost}`);
this.QDRANT_HOST = defaultQdrantHost;
}
} else {
this.QDRANT_HOST = defaultQdrantHost; // Use default if env var is not set or is empty/whitespace
}
this.COLLECTION_NAME = process.env.COLLECTION_NAME || "codecompass_collection"; // Default, not typically changed by user config
// Validate and set OLLAMA_HOST
const defaultOllamaHost = "http://127.0.0.1:11434";
const ollamaHostEnv = process.env.OLLAMA_HOST;
if (ollamaHostEnv && ollamaHostEnv.trim() !== "") {
try {
const parsedUrl = new URL(ollamaHostEnv);
if (parsedUrl.protocol && (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')) {
this.OLLAMA_HOST = ollamaHostEnv;
} else {
this.logger.warn(`OLLAMA_HOST environment variable "${ollamaHostEnv}" has an invalid or missing protocol. Falling back to default: ${defaultOllamaHost}`);
this.OLLAMA_HOST = defaultOllamaHost;
}
} catch (e) {
this.logger.warn(`OLLAMA_HOST environment variable "${ollamaHostEnv}" is not a valid URL. Error: ${(e as Error).message}. Falling back to default: ${defaultOllamaHost}`);
this.OLLAMA_HOST = defaultOllamaHost;
}
} else {
this.OLLAMA_HOST = defaultOllamaHost; // Use default if env var is not set or is empty/whitespace
}
// QDRANT_HOST and COLLECTION_NAME are now initialized above, before logger.
// The previous direct assignment of this.QDRANT_HOST and this.COLLECTION_NAME is removed from here.
this.MAX_INPUT_LENGTH = 4096;
this.MAX_SNIPPET_LENGTH = 500;
this.REQUEST_TIMEOUT = 120000;
this.MAX_RETRIES = 3;
this.RETRY_DELAY = 2000;
// CONFIG_DIR, MODEL_CONFIG_FILE, DEEPSEEK_CONFIG_FILE, LOG_DIR are initialized earlier
// Initialize with environment variables or hardcoded defaults first
this._llmProvider = process.env.LLM_PROVIDER || "ollama";
this._suggestionModel = process.env.SUGGESTION_MODEL || "llama3.1:8b";
this._embeddingModel = process.env.EMBEDDING_MODEL || "nomic-embed-text:v1.5"; // Ollama default
this._embeddingDimension = parseInt(process.env.EMBEDDING_DIMENSION || '', 10) || this.DEFAULT_EMBEDDING_DIMENSION;
this._deepSeekApiKey = process.env.DEEPSEEK_API_KEY || "";
this._deepSeekApiUrl = process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/chat/completions";
this._deepSeekModel = process.env.DEEPSEEK_MODEL || "deepseek-coder";
this._deepSeekRpmLimit = parseInt(process.env.DEEPSEEK_RPM_LIMIT || '', 10) || this.DEEPSEEK_RPM_LIMIT_DEFAULT;
this._agentQueryTimeout = parseInt(process.env.AGENT_QUERY_TIMEOUT || '', 10) || this.AGENT_QUERY_TIMEOUT_DEFAULT;
this._qdrantSearchLimitDefault = parseInt(process.env.QDRANT_SEARCH_LIMIT_DEFAULT || '', 10) || this.DEFAULT_QDRANT_SEARCH_LIMIT; // Initialize Qdrant search limit
this._maxDiffLengthForContextTool = parseInt(process.env.MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL || '', 10) || 3000; // Default 3000 chars
this._maxFilesForSuggestionContextNoSummary = parseInt(process.env.MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY || '', 10) || this.DEFAULT_MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY;
this._maxSnippetLengthForContextNoSummary = parseInt(process.env.MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY || '', 10) || this.DEFAULT_MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY;
this._openAIApiKey = process.env.OPENAI_API_KEY || "";
this._geminiApiKey = process.env.GEMINI_API_KEY || "";
this._claudeApiKey = process.env.CLAUDE_API_KEY || "";
this._agentDefaultMaxSteps = parseInt(process.env.AGENT_DEFAULT_MAX_STEPS || '', 10) || this.DEFAULT_AGENT_DEFAULT_MAX_STEPS;
this._agentAbsoluteMaxSteps = parseInt(process.env.AGENT_ABSOLUTE_MAX_STEPS || '', 10) || this.DEFAULT_AGENT_ABSOLUTE_MAX_STEPS;
this._maxRefinementIterations = parseInt(process.env.MAX_REFINEMENT_ITERATIONS || '', 10) || this.DEFAULT_MAX_REFINEMENT_ITERATIONS;
this._fileIndexingChunkSizeChars = parseInt(process.env.FILE_INDEXING_CHUNK_SIZE_CHARS || '', 10) || this.DEFAULT_FILE_INDEXING_CHUNK_SIZE_CHARS;
this._fileIndexingChunkOverlapChars = parseInt(process.env.FILE_INDEXING_CHUNK_OVERLAP_CHARS || '', 10) || this.DEFAULT_FILE_INDEXING_CHUNK_OVERLAP_CHARS;
this._requestAdditionalContextMaxSearchResults = parseInt(process.env.REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS || '', 10) || this.DEFAULT_REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS;
this._diffChunkSizeChars = parseInt(process.env.DIFF_CHUNK_SIZE_CHARS || '', 10) || this.DEFAULT_DIFF_CHUNK_SIZE_CHARS;
this._diffChunkOverlapChars = parseInt(process.env.DIFF_CHUNK_OVERLAP_CHARS || '', 10) || this.DEFAULT_DIFF_CHUNK_OVERLAP_CHARS;
this._commitHistoryMaxCountForIndexing = parseInt(process.env.COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING || '', 10) || this.DEFAULT_COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING;
this._qdrantBatchUpsertSize = parseInt(process.env.QDRANT_BATCH_UPSERT_SIZE || '', 10) || this.DEFAULT_QDRANT_BATCH_UPSERT_SIZE;
this._agentMaxContextItems = parseInt(process.env.AGENT_MAX_CONTEXT_ITEMS || '', 10) || this.DEFAULT_AGENT_MAX_CONTEXT_ITEMS;
this._diffLinesOfContext = parseInt(process.env.DIFF_LINES_OF_CONTEXT || '', 10) || this.DEFAULT_DIFF_LINES_OF_CONTEXT;
this._maxFileContentLengthForCapability = parseInt(process.env.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY || '', 10) || this.DEFAULT_MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY;
this._maxDirListingEntriesForCapability = parseInt(process.env.MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY || '', 10) || this.DEFAULT_MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY;
this._httpPort = parseInt(process.env.HTTP_PORT || '', 10) || this.DEFAULT_HTTP_PORT; // Added
// For _summarizationModel and _refinementModel, we'll set them properly in loadConfigurationsFromFile
// and reloadConfigsFromFile after _suggestionModel is definitively set.
// For now, initialize them to empty strings or a placeholder that indicates they need to be derived.
this._summarizationModel = process.env.SUMMARIZATION_MODEL || "";
this._refinementModel = process.env.REFINEMENT_MODEL || "";
this._useMixedProviders = process.env.USE_MIXED_PROVIDERS === "true" || false;
this._suggestionProvider = process.env.SUGGESTION_PROVIDER || this._llmProvider;
// Default embedding provider to ollama, can be overridden by file/env
this._embeddingProvider = process.env.EMBEDDING_PROVIDER || "ollama";
this.loadConfigurationsFromFile(); // Load persisted configs, which can override env/defaults
this.initializeGlobalState(); // Set global vars based on the final effective configuration
}
public static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
private loadDeepSeekConfigFromFile(): Partial<ModelConfigFile> {
try {
if (fs.existsSync(this.DEEPSEEK_CONFIG_FILE)) {
const fileContent = fs.readFileSync(this.DEEPSEEK_CONFIG_FILE, 'utf8');
const config = JSON.parse(fileContent) as Partial<ModelConfigFile>;
this.logger.info(`Loaded DeepSeek config from ${this.DEEPSEEK_CONFIG_FILE}`);
return config;
}
} catch (error) {
this.logger.warn(`Failed to load DeepSeek config from ${this.DEEPSEEK_CONFIG_FILE}: ${(error as Error).message}`);
}
return {};
}
private loadModelConfigFromFile(): Partial<ModelConfigFile> {
try {
if (fs.existsSync(this.MODEL_CONFIG_FILE)) {
const fileContent = fs.readFileSync(this.MODEL_CONFIG_FILE, 'utf8');
const config = JSON.parse(fileContent) as Partial<ModelConfigFile>;
this.logger.info(`Loaded model config from ${this.MODEL_CONFIG_FILE}`);
return config;
}
} catch (error) {
this.logger.warn(`Failed to load model config from ${this.MODEL_CONFIG_FILE}: ${(error as Error).message}`);
}
return {};
}
private loadConfigurationsFromFile(): void {
const modelConfig = this.loadModelConfigFromFile();
const deepSeekConfig = this.loadDeepSeekConfigFromFile();
// Apply loaded configurations, file values take precedence over initial env/defaults
// DEEPSEEK_API_KEY: file > env > default
if (deepSeekConfig.DEEPSEEK_API_KEY) {
this._deepSeekApiKey = deepSeekConfig.DEEPSEEK_API_KEY;
} // If not in file, _deepSeekApiKey retains its env/default value
// DEEPSEEK_API_URL: file > env > default
if (deepSeekConfig.DEEPSEEK_API_URL) {
this._deepSeekApiUrl = deepSeekConfig.DEEPSEEK_API_URL;
}
// SUGGESTION_MODEL: file > env > default
if (modelConfig.SUGGESTION_MODEL) {
this._suggestionModel = modelConfig.SUGGESTION_MODEL;
}
// SUGGESTION_PROVIDER: file > env > default (where default for SUGGESTION_PROVIDER is LLM_PROVIDER)
if (modelConfig.SUGGESTION_PROVIDER) {
this._suggestionProvider = modelConfig.SUGGESTION_PROVIDER;
this._llmProvider = modelConfig.SUGGESTION_PROVIDER; // SUGGESTION_PROVIDER from file also dictates LLM_PROVIDER
}
// If not in modelConfig, _suggestionProvider and _llmProvider retain their env/default values
// Load new model-specific configs
if (modelConfig.SUMMARIZATION_MODEL) {
this._summarizationModel = modelConfig.SUMMARIZATION_MODEL;
}
if (modelConfig.REFINEMENT_MODEL) {
this._refinementModel = modelConfig.REFINEMENT_MODEL;
}
// Derive summarization and refinement models if they are empty (i.e., not set by env or file)
// This ensures _suggestionModel is already finalized from env/file before being used as a fallback.
if (!this._summarizationModel) {
this._summarizationModel = this._suggestionModel;
}
if (!this._refinementModel) {
this._refinementModel = this._suggestionModel;
}
// EMBEDDING_PROVIDER: file > env > default
if (modelConfig.EMBEDDING_PROVIDER) {
this._embeddingProvider = modelConfig.EMBEDDING_PROVIDER;
}
// API keys from model-config.json (these override env vars if present in file)
if (modelConfig.OPENAI_API_KEY) {
this._openAIApiKey = modelConfig.OPENAI_API_KEY;
}
if (modelConfig.GEMINI_API_KEY) {
this._geminiApiKey = modelConfig.GEMINI_API_KEY;
}
if (modelConfig.CLAUDE_API_KEY) {
this._claudeApiKey = modelConfig.CLAUDE_API_KEY;
}
// Apply HTTP_PORT from modelConfig if present and valid
// This value will be used if process.env.HTTP_PORT was not set, or will override it for _httpPort.
if (modelConfig.HTTP_PORT !== undefined &&
typeof modelConfig.HTTP_PORT === 'number' &&
!isNaN(modelConfig.HTTP_PORT) &&
modelConfig.HTTP_PORT > 0 && modelConfig.HTTP_PORT < 65536) {
this._httpPort = modelConfig.HTTP_PORT;
this.logger.info(`HTTP_PORT set to ${this._httpPort} from ${this.MODEL_CONFIG_FILE}`);
}
// Ensure process.env reflects the final state. This is crucial for any part of the code
// or external libraries that might still read from process.env directly.
process.env.DEEPSEEK_API_KEY = this._deepSeekApiKey; // Still handle from its specific file or env
process.env.DEEPSEEK_API_URL = this._deepSeekApiUrl; // Still handle from its specific file or env
process.env.DEEPSEEK_MODEL = this._deepSeekModel;
process.env.DEEPSEEK_RPM_LIMIT = String(this._deepSeekRpmLimit);
process.env.AGENT_QUERY_TIMEOUT = String(this._agentQueryTimeout);
process.env.SUGGESTION_MODEL = this._suggestionModel;
process.env.SUGGESTION_PROVIDER = this._suggestionProvider;
process.env.EMBEDDING_PROVIDER = this._embeddingProvider;
process.env.EMBEDDING_MODEL = this._embeddingModel; // EMBEDDING_MODEL is usually from env or default
process.env.EMBEDDING_DIMENSION = String(this._embeddingDimension);
process.env.LLM_PROVIDER = this._llmProvider;
process.env.OLLAMA_HOST = this.OLLAMA_HOST; // Ensure OLLAMA_HOST from env/default is in process.env
process.env.QDRANT_HOST = this.QDRANT_HOST; // Ensure QDRANT_HOST from env/default is in process.env
process.env.QDRANT_SEARCH_LIMIT_DEFAULT = String(this._qdrantSearchLimitDefault); // Ensure QDRANT_SEARCH_LIMIT_DEFAULT is in process.env
process.env.MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL = String(this._maxDiffLengthForContextTool);
process.env.MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY = String(this._maxFilesForSuggestionContextNoSummary);
process.env.MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY = String(this._maxSnippetLengthForContextNoSummary);
process.env.OPENAI_API_KEY = this._openAIApiKey;
process.env.GEMINI_API_KEY = this._geminiApiKey;
process.env.CLAUDE_API_KEY = this._claudeApiKey;
// Update process.env with all new configurations
process.env.AGENT_DEFAULT_MAX_STEPS = String(this._agentDefaultMaxSteps);
process.env.AGENT_ABSOLUTE_MAX_STEPS = String(this._agentAbsoluteMaxSteps);
process.env.MAX_REFINEMENT_ITERATIONS = String(this._maxRefinementIterations);
process.env.FILE_INDEXING_CHUNK_SIZE_CHARS = String(this._fileIndexingChunkSizeChars);
process.env.FILE_INDEXING_CHUNK_OVERLAP_CHARS = String(this._fileIndexingChunkOverlapChars);
process.env.REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS = String(this._requestAdditionalContextMaxSearchResults);
process.env.SUMMARIZATION_MODEL = this._summarizationModel;
process.env.REFINEMENT_MODEL = this._refinementModel;
process.env.DIFF_CHUNK_SIZE_CHARS = String(this._diffChunkSizeChars);
process.env.DIFF_CHUNK_OVERLAP_CHARS = String(this._diffChunkOverlapChars);
process.env.COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING = String(this._commitHistoryMaxCountForIndexing);
process.env.QDRANT_BATCH_UPSERT_SIZE = String(this._qdrantBatchUpsertSize);
process.env.AGENT_MAX_CONTEXT_ITEMS = String(this._agentMaxContextItems);
process.env.DIFF_LINES_OF_CONTEXT = String(this._diffLinesOfContext);
process.env.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY = String(this._maxFileContentLengthForCapability);
process.env.MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY = String(this._maxDirListingEntriesForCapability);
process.env.HTTP_PORT = String(this._httpPort); // Added
}
public reloadConfigsFromFile(_forceSet = true): void {
// Re-initialize from env/defaults
this._llmProvider = process.env.LLM_PROVIDER || "ollama";
this._suggestionModel = process.env.SUGGESTION_MODEL || "llama3.1:8b";
this._embeddingDimension = parseInt(process.env.EMBEDDING_DIMENSION || '', 10) || this.DEFAULT_EMBEDDING_DIMENSION;
this._embeddingModel = process.env.EMBEDDING_MODEL || "nomic-embed-text:v1.5";
this._deepSeekApiKey = process.env.DEEPSEEK_API_KEY || "";
this._deepSeekApiUrl = process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/chat/completions";
this._deepSeekModel = process.env.DEEPSEEK_MODEL || "deepseek-coder";
this._deepSeekRpmLimit = parseInt(process.env.DEEPSEEK_RPM_LIMIT || '', 10) || this.DEEPSEEK_RPM_LIMIT_DEFAULT;
this._agentQueryTimeout = parseInt(process.env.AGENT_QUERY_TIMEOUT || '', 10) || this.AGENT_QUERY_TIMEOUT_DEFAULT;
this._qdrantSearchLimitDefault = parseInt(process.env.QDRANT_SEARCH_LIMIT_DEFAULT || '', 10) || this.DEFAULT_QDRANT_SEARCH_LIMIT; // Re-initialize Qdrant search limit
this._maxDiffLengthForContextTool = parseInt(process.env.MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL || '', 10) || 3000;
this._maxFilesForSuggestionContextNoSummary = parseInt(process.env.MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY || '', 10) || this.DEFAULT_MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY;
this._maxSnippetLengthForContextNoSummary = parseInt(process.env.MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY || '', 10) || this.DEFAULT_MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY;
this._openAIApiKey = process.env.OPENAI_API_KEY || "";
this._geminiApiKey = process.env.GEMINI_API_KEY || "";
this._claudeApiKey = process.env.CLAUDE_API_KEY || "";
this._agentDefaultMaxSteps = parseInt(process.env.AGENT_DEFAULT_MAX_STEPS || '', 10) || this.DEFAULT_AGENT_DEFAULT_MAX_STEPS;
this._agentAbsoluteMaxSteps = parseInt(process.env.AGENT_ABSOLUTE_MAX_STEPS || '', 10) || this.DEFAULT_AGENT_ABSOLUTE_MAX_STEPS;
this._maxRefinementIterations = parseInt(process.env.MAX_REFINEMENT_ITERATIONS || '', 10) || this.DEFAULT_MAX_REFINEMENT_ITERATIONS;
this._fileIndexingChunkSizeChars = parseInt(process.env.FILE_INDEXING_CHUNK_SIZE_CHARS || '', 10) || this.DEFAULT_FILE_INDEXING_CHUNK_SIZE_CHARS;
this._fileIndexingChunkOverlapChars = parseInt(process.env.FILE_INDEXING_CHUNK_OVERLAP_CHARS || '', 10) || this.DEFAULT_FILE_INDEXING_CHUNK_OVERLAP_CHARS;
this._requestAdditionalContextMaxSearchResults = parseInt(process.env.REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS || '', 10) || this.DEFAULT_REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS;
this._diffChunkSizeChars = parseInt(process.env.DIFF_CHUNK_SIZE_CHARS || '', 10) || this.DEFAULT_DIFF_CHUNK_SIZE_CHARS;
this._diffChunkOverlapChars = parseInt(process.env.DIFF_CHUNK_OVERLAP_CHARS || '', 10) || this.DEFAULT_DIFF_CHUNK_OVERLAP_CHARS;
this._commitHistoryMaxCountForIndexing = parseInt(process.env.COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING || '', 10) || this.DEFAULT_COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING;
this._qdrantBatchUpsertSize = parseInt(process.env.QDRANT_BATCH_UPSERT_SIZE || '', 10) || this.DEFAULT_QDRANT_BATCH_UPSERT_SIZE;
this._agentMaxContextItems = parseInt(process.env.AGENT_MAX_CONTEXT_ITEMS || '', 10) || this.DEFAULT_AGENT_MAX_CONTEXT_ITEMS;
this._diffLinesOfContext = parseInt(process.env.DIFF_LINES_OF_CONTEXT || '', 10) || this.DEFAULT_DIFF_LINES_OF_CONTEXT;
this._maxFileContentLengthForCapability = parseInt(process.env.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY || '', 10) || this.DEFAULT_MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY;
this._maxDirListingEntriesForCapability = parseInt(process.env.MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY || '', 10) || this.DEFAULT_MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY;
this._httpPort = parseInt(process.env.HTTP_PORT || '', 10) || this.DEFAULT_HTTP_PORT; // Added
// Initialize from env, file loading will override if present, then derive.
this._summarizationModel = process.env.SUMMARIZATION_MODEL || "";
this._refinementModel = process.env.REFINEMENT_MODEL || "";
this._suggestionProvider = process.env.SUGGESTION_PROVIDER || this._llmProvider;
this._embeddingProvider = process.env.EMBEDDING_PROVIDER || "ollama";
this.loadConfigurationsFromFile(); // This will load from files and derive _summarizationModel/_refinementModel
this.initializeGlobalState();
}
private initializeGlobalState(): void {
global.CURRENT_LLM_PROVIDER = this._llmProvider;
global.CURRENT_SUGGESTION_PROVIDER = this._suggestionProvider;
global.CURRENT_EMBEDDING_PROVIDER = this._embeddingProvider;
global.CURRENT_SUGGESTION_MODEL = this._suggestionModel;
}
// Getters use global first (as they might be changed dynamically), then internal state.
// Internal state (_variable) reflects config file/env/default precedence.
// process.env is updated by loadConfigurationsFromFile to reflect the effective config.
get LLM_PROVIDER(): string { return global.CURRENT_LLM_PROVIDER || this._llmProvider; }
get SUGGESTION_MODEL(): string { return global.CURRENT_SUGGESTION_MODEL || this._suggestionModel; }
get EMBEDDING_MODEL(): string { return process.env.EMBEDDING_MODEL || this._embeddingModel; }
get EMBEDDING_DIMENSION(): number { return parseInt(process.env.EMBEDDING_DIMENSION || '', 10) || this._embeddingDimension; }
get DEEPSEEK_API_KEY(): string { return process.env.DEEPSEEK_API_KEY || this._deepSeekApiKey; }
get DEEPSEEK_API_URL(): string { return process.env.DEEPSEEK_API_URL || this._deepSeekApiUrl; }
get DEEPSEEK_MODEL(): string { return process.env.DEEPSEEK_MODEL || this._deepSeekModel; }
get DEEPSEEK_RPM_LIMIT(): number { return parseInt(process.env.DEEPSEEK_RPM_LIMIT || '', 10) || this._deepSeekRpmLimit; }
get AGENT_QUERY_TIMEOUT(): number { return parseInt(process.env.AGENT_QUERY_TIMEOUT || '', 10) || this._agentQueryTimeout; }
get QDRANT_SEARCH_LIMIT_DEFAULT(): number { return parseInt(process.env.QDRANT_SEARCH_LIMIT_DEFAULT || '', 10) || this._qdrantSearchLimitDefault; } // Getter for Qdrant search limit
get MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL(): number { return this._maxDiffLengthForContextTool; }
get MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY(): number { return parseInt(process.env.MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY || '', 10) || this._maxFilesForSuggestionContextNoSummary; }
get MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY(): number { return this._maxSnippetLengthForContextNoSummary; }
get OPENAI_API_KEY(): string { return process.env.OPENAI_API_KEY || this._openAIApiKey; }
get GEMINI_API_KEY(): string { return process.env.GEMINI_API_KEY || this._geminiApiKey; }
get CLAUDE_API_KEY(): string { return process.env.CLAUDE_API_KEY || this._claudeApiKey; }
get USE_MIXED_PROVIDERS(): boolean { return this._useMixedProviders; } // Typically from env or default
get SUGGESTION_PROVIDER(): string { return global.CURRENT_SUGGESTION_PROVIDER || this._suggestionProvider; }
get EMBEDDING_PROVIDER(): string { return global.CURRENT_EMBEDDING_PROVIDER || this._embeddingProvider; }
get AGENT_DEFAULT_MAX_STEPS(): number { return parseInt(process.env.AGENT_DEFAULT_MAX_STEPS || '', 10) || this._agentDefaultMaxSteps; }
get AGENT_ABSOLUTE_MAX_STEPS(): number { return parseInt(process.env.AGENT_ABSOLUTE_MAX_STEPS || '', 10) || this._agentAbsoluteMaxSteps; }
get MAX_REFINEMENT_ITERATIONS(): number { return parseInt(process.env.MAX_REFINEMENT_ITERATIONS || '', 10) || this._maxRefinementIterations; }
get FILE_INDEXING_CHUNK_SIZE_CHARS(): number { return parseInt(process.env.FILE_INDEXING_CHUNK_SIZE_CHARS || '', 10) || this._fileIndexingChunkSizeChars; }
get FILE_INDEXING_CHUNK_OVERLAP_CHARS(): number { return parseInt(process.env.FILE_INDEXING_CHUNK_OVERLAP_CHARS || '', 10) || this._fileIndexingChunkOverlapChars; }
get REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS(): number { return parseInt(process.env.REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS || '', 10) || this._requestAdditionalContextMaxSearchResults; }
get SUMMARIZATION_MODEL(): string { return process.env.SUMMARIZATION_MODEL || this._summarizationModel; }
get REFINEMENT_MODEL(): string { return process.env.REFINEMENT_MODEL || this._refinementModel; }
get DIFF_CHUNK_SIZE_CHARS(): number { return parseInt(process.env.DIFF_CHUNK_SIZE_CHARS || '', 10) || this._diffChunkSizeChars; }
get DIFF_CHUNK_OVERLAP_CHARS(): number { return parseInt(process.env.DIFF_CHUNK_OVERLAP_CHARS || '', 10) || this._diffChunkOverlapChars; }
get COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING(): number { return parseInt(process.env.COMMIT_HISTORY_MAX_COUNT_FOR_INDEXING || '', 10) || this._commitHistoryMaxCountForIndexing; }
get QDRANT_BATCH_UPSERT_SIZE(): number { return parseInt(process.env.QDRANT_BATCH_UPSERT_SIZE || '', 10) || this._qdrantBatchUpsertSize; }
get AGENT_MAX_CONTEXT_ITEMS(): number { return parseInt(process.env.AGENT_MAX_CONTEXT_ITEMS || '', 10) || this._agentMaxContextItems; }
get DIFF_LINES_OF_CONTEXT(): number { return parseInt(process.env.DIFF_LINES_OF_CONTEXT || '', 10) || this._diffLinesOfContext; }
get MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY(): number { return this._maxFileContentLengthForCapability; }
get MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY(): number { return this._maxDirListingEntriesForCapability; }
get HTTP_PORT(): number { return parseInt(process.env.HTTP_PORT || '', 10) || this._httpPort; } // Added getter
// Method to get all relevant config for a provider (example for OpenAI)
public getConfig(): { [key: string]: string | number | boolean | undefined } {
return {
DEEPSEEK_API_KEY: this.DEEPSEEK_API_KEY,
DEEPSEEK_API_URL: this.DEEPSEEK_API_URL,
DEEPSEEK_MODEL: this.DEEPSEEK_MODEL,
HTTP_PORT: this.HTTP_PORT, // Add this line
OPENAI_API_KEY: this.OPENAI_API_KEY,
GEMINI_API_KEY: this.GEMINI_API_KEY,
CLAUDE_API_KEY: this.CLAUDE_API_KEY,
// Add other keys as needed
};
}
public setSuggestionModel(model: string): void {
const oldSuggestionModel = this._suggestionModel; // Store the old value
this._suggestionModel = model;
process.env.SUGGESTION_MODEL = model;
global.CURRENT_SUGGESTION_MODEL = model;
// If summarization/refinement models were previously derived from suggestionModel or were empty, update them.
if (this._summarizationModel === oldSuggestionModel || !this._summarizationModel) {
this._summarizationModel = model;
process.env.SUMMARIZATION_MODEL = model;
}
if (this._refinementModel === oldSuggestionModel || !this._refinementModel) {
this._refinementModel = model;
process.env.REFINEMENT_MODEL = model;
}
// Ensure global state is fully updated before persisting.
// initializeGlobalState updates all global.* variables based on current service state.
this.initializeGlobalState();
this.persistModelConfiguration();
}
public setSuggestionProvider(provider: string): void {
this._suggestionProvider = provider;
process.env.SUGGESTION_PROVIDER = provider;
global.CURRENT_SUGGESTION_PROVIDER = provider;
this._llmProvider = provider;
process.env.LLM_PROVIDER = provider;
global.CURRENT_LLM_PROVIDER = provider;
this.persistModelConfiguration();
}
public setEmbeddingProvider(provider: string): void {
this._embeddingProvider = provider;
process.env.EMBEDDING_PROVIDER = provider;
global.CURRENT_EMBEDDING_PROVIDER = provider;
this.persistModelConfiguration();
}
public setDeepSeekApiKey(key: string): void {
this._deepSeekApiKey = key;
process.env.DEEPSEEK_API_KEY = key;
this.persistDeepSeekConfiguration();
}
public setDeepSeekApiUrl(url: string): void {
this._deepSeekApiUrl = url;
process.env.DEEPSEEK_API_URL = url;
this.persistDeepSeekConfiguration();
}
public setDeepSeekModel(model: string): void {
this._deepSeekModel = model;
process.env.DEEPSEEK_MODEL = model;
// Not persisted in model-config.json or deepseek-config.json by default
}
public setOpenAIApiKey(key: string): void {
this._openAIApiKey = key;
process.env.OPENAI_API_KEY = key;
this.persistModelConfiguration(); // Persist to model-config.json
}
public setGeminiApiKey(key: string): void {
this._geminiApiKey = key;
process.env.GEMINI_API_KEY = key;
this.persistModelConfiguration();
}
public setClaudeApiKey(key: string): void {
this._claudeApiKey = key;
process.env.CLAUDE_API_KEY = key;
this.persistModelConfiguration();
}
public persistModelConfiguration(): void {
try {
if (!fs.existsSync(this.CONFIG_DIR)) {
fs.mkdirSync(this.CONFIG_DIR, { recursive: true });
}
const configToSave: ModelConfigFile = {
SUGGESTION_MODEL: this.SUGGESTION_MODEL, // Use getter to ensure current value
SUGGESTION_PROVIDER: this.SUGGESTION_PROVIDER, // Use getter
EMBEDDING_PROVIDER: this.EMBEDDING_PROVIDER, // Use getter
// Include other API keys that should be persisted in model-config.json
OPENAI_API_KEY: this.OPENAI_API_KEY,
GEMINI_API_KEY: this.GEMINI_API_KEY,
CLAUDE_API_KEY: this.CLAUDE_API_KEY,
HTTP_PORT: this.HTTP_PORT, // Use getter to save the effective port
SUMMARIZATION_MODEL: this.SUMMARIZATION_MODEL, // New
REFINEMENT_MODEL: this.REFINEMENT_MODEL, // New
};
// Remove undefined keys before saving
Object.keys(configToSave).forEach(keyStr => {
const key = keyStr as keyof ModelConfigFile;
if (configToSave[key] === undefined) {
delete configToSave[key];
}
});
fs.writeFileSync(this.MODEL_CONFIG_FILE, JSON.stringify(configToSave, null, 2));
this.logger.info(`Saved model configuration to ${this.MODEL_CONFIG_FILE}`);
} catch (error) {
this.logger.warn(`Failed to save model configuration: ${(error as Error).message}`);
}
}
public persistDeepSeekConfiguration(): void {
try {
if (!fs.existsSync(this.CONFIG_DIR)) {
fs.mkdirSync(this.CONFIG_DIR, { recursive: true });
}
const configToSave = {
DEEPSEEK_API_KEY: this.DEEPSEEK_API_KEY,
DEEPSEEK_API_URL: this.DEEPSEEK_API_URL,
timestamp: new Date().toISOString()
};
fs.writeFileSync(this.DEEPSEEK_CONFIG_FILE, JSON.stringify(configToSave, null, 2));
this.logger.info(`Saved DeepSeek configuration to ${this.DEEPSEEK_CONFIG_FILE}`);
} catch (error) {
this.logger.warn(`Failed to save DeepSeek configuration: ${(error as Error).message}`);
}
}
}
export const configService = ConfigService.getInstance();
export const logger = configService.logger;
export { ConfigService };