import fs from "fs"; import path from "path"; import chalk from "chalk"; import { fileURLToPath } from "url"; import { log, findProjectRoot, resolveEnvVariable } from "./utils.js"; // Calculate __dirname in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load supported models from JSON file using the calculated __dirname let MODEL_MAP; try { const supportedModelsRaw = fs.readFileSync( path.join(__dirname, "supported-models.json"), "utf-8" ); MODEL_MAP = JSON.parse(supportedModelsRaw); } catch (error) { console.error( chalk.red( "FATAL ERROR: Could not load supported-models.json. Please ensure the file exists and is valid JSON." ), error ); MODEL_MAP = {}; // Default to empty map on error to avoid crashing, though functionality will be limited process.exit(1); // Exit if models can't be loaded } const CONFIG_FILE_NAME = ".taskmasterconfig"; // Define valid providers dynamically from the loaded MODEL_MAP const VALID_PROVIDERS = Object.keys(MODEL_MAP || {}); // Default configuration structure (updated) const defaultConfig = { global: { logLevel: "info", debug: false, defaultSubtasks: 5, defaultPriority: "medium", projectName: "Taskmaster", ollamaBaseURL: "http://localhost:11434/api", azureBaseURL: "https://your-endpoint.azure.com/", }, models: { main: { provider: "anthropic", modelId: "claude-3-7-sonnet-20250219", maxTokens: 64000, temperature: 0.2, }, research: { provider: "perplexity", modelId: "sonar-pro", maxTokens: 8700, temperature: 0.1, }, fallback: { // No default fallback provider/model initially provider: "anthropic", modelId: "claude-3-5-sonnet", maxTokens: 64000, // Default parameters if fallback IS configured temperature: 0.2, }, }, account: { userId: "1234567890", // Placeholder that triggers auth/init email: "", mode: "byok", telemetryEnabled: true, }, }; // --- Internal Config Loading --- let loadedConfig = null; let loadedConfigRoot = null; // Track which root loaded the config // Custom Error for configuration issues class ConfigurationError extends Error { constructor(message) { super(message); this.name = "ConfigurationError"; } } function _loadAndValidateConfig(explicitRoot = null) { const defaults = defaultConfig; // Use the defined defaults let rootToUse = explicitRoot; let configSource = explicitRoot ? `explicit root (${explicitRoot})` : "defaults (no root provided yet)"; // ---> If no explicit root, TRY to find it <--- if (!rootToUse) { rootToUse = findProjectRoot(); if (rootToUse) { configSource = `found root (${rootToUse})`; } else { // No root found, return defaults immediately return defaults; } } // ---> End find project root logic <--- // --- Proceed with loading from the determined rootToUse --- const configPath = path.join(rootToUse, CONFIG_FILE_NAME); let config = { ...defaults }; // Start with a deep copy of defaults let configExists = false; if (fs.existsSync(configPath)) { configExists = true; try { const rawData = fs.readFileSync(configPath, "utf-8"); const parsedConfig = JSON.parse(rawData); // Deep merge parsed config onto defaults config = { models: { main: { ...defaults.models.main, ...parsedConfig?.models?.main }, research: { ...defaults.models.research, ...parsedConfig?.models?.research, }, fallback: parsedConfig?.models?.fallback?.provider && parsedConfig?.models?.fallback?.modelId ? { ...defaults.models.fallback, ...parsedConfig.models.fallback } : { ...defaults.models.fallback }, }, global: { ...defaults.global, ...parsedConfig?.global }, account: { ...defaults.account, ...parsedConfig?.account }, }; configSource = `file (${configPath})`; // Update source info // --- Validation (Warn if file content is invalid) --- // Use log.warn for consistency if (!validateProvider(config.models.main.provider)) { console.warn( chalk.yellow( `Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.` ) ); config.models.main = { ...defaults.models.main }; } if (!validateProvider(config.models.research.provider)) { console.warn( chalk.yellow( `Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.` ) ); config.models.research = { ...defaults.models.research }; } if ( config.models.fallback?.provider && !validateProvider(config.models.fallback.provider) ) { console.warn( chalk.yellow( `Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.` ) ); config.models.fallback.provider = undefined; config.models.fallback.modelId = undefined; } } catch (error) { // Use console.error for actual errors during parsing console.error( chalk.red( `Error reading or parsing ${configPath}: ${error.message}. Using default configuration.` ) ); config = { ...defaults }; // Reset to defaults on parse error configSource = `defaults (parse error at ${configPath})`; } } else { // Config file doesn't exist at the determined rootToUse. if (explicitRoot) { // Only warn if an explicit root was *expected*. console.warn( chalk.yellow( `Warning: ${CONFIG_FILE_NAME} not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.` ) ); } else { console.warn( chalk.yellow( `Warning: ${CONFIG_FILE_NAME} not found at derived root (${rootToUse}). Using defaults.` ) ); } // Keep config as defaults config = { ...defaults }; configSource = `defaults (file not found at ${configPath})`; } return config; } /** * Gets the current configuration, loading it if necessary. * Handles MCP initialization context gracefully. * @param {string|null} explicitRoot - Optional explicit path to the project root. * @param {boolean} forceReload - Force reloading the config file. * @returns {object} The loaded configuration object. */ function getConfig(explicitRoot = null, forceReload = false) { // Determine if a reload is necessary const needsLoad = !loadedConfig || forceReload || (explicitRoot && explicitRoot !== loadedConfigRoot); if (needsLoad) { const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot // Only update the global cache if loading was forced or if an explicit root // was provided (meaning we attempted to load a specific project's config). // We avoid caching the initial default load triggered without an explicitRoot. if (forceReload || explicitRoot) { loadedConfig = newConfig; loadedConfigRoot = explicitRoot; // Store the root used for this loaded config } return newConfig; // Return the newly loaded/default config } // If no load was needed, return the cached config return loadedConfig; } /** * Validates if a provider name is in the list of supported providers. * @param {string} providerName The name of the provider. * @returns {boolean} True if the provider is valid, false otherwise. */ function validateProvider(providerName) { return VALID_PROVIDERS.includes(providerName); } /** * Optional: Validates if a modelId is known for a given provider based on MODEL_MAP. * This is a non-strict validation; an unknown model might still be valid. * @param {string} providerName The name of the provider. * @param {string} modelId The model ID. * @returns {boolean} True if the modelId is in the map for the provider, false otherwise. */ function validateProviderModelCombination(providerName, modelId) { // If provider isn't even in our map, we can't validate the model if (!MODEL_MAP[providerName]) { return true; // Allow unknown providers or those without specific model lists } // If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any) return ( MODEL_MAP[providerName].length === 0 || // Use .some() to check the 'id' property of objects in the array MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId) ); } // --- Role-Specific Getters --- function getModelConfigForRole(role, explicitRoot = null) { const config = getConfig(explicitRoot); const roleConfig = config?.models?.[role]; if (!roleConfig) { log( "warn", `No model configuration found for role: ${role}. Returning default.` ); return defaultConfig.models[role] || {}; } return roleConfig; } function getMainProvider(explicitRoot = null) { return getModelConfigForRole("main", explicitRoot).provider; } function getMainModelId(explicitRoot = null) { return getModelConfigForRole("main", explicitRoot).modelId; } function getMainMaxTokens(explicitRoot = null) { // Directly return value from config (which includes defaults) return getModelConfigForRole("main", explicitRoot).maxTokens; } function getMainTemperature(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("main", explicitRoot).temperature; } function getResearchProvider(explicitRoot = null) { return getModelConfigForRole("research", explicitRoot).provider; } function getResearchModelId(explicitRoot = null) { return getModelConfigForRole("research", explicitRoot).modelId; } function getResearchMaxTokens(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("research", explicitRoot).maxTokens; } function getResearchTemperature(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("research", explicitRoot).temperature; } function getFallbackProvider(explicitRoot = null) { // Directly return value from config (will be undefined if not set) return getModelConfigForRole("fallback", explicitRoot).provider; } function getFallbackModelId(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("fallback", explicitRoot).modelId; } function getFallbackMaxTokens(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("fallback", explicitRoot).maxTokens; } function getFallbackTemperature(explicitRoot = null) { // Directly return value from config return getModelConfigForRole("fallback", explicitRoot).temperature; } // --- Global Settings Getters --- function getGlobalConfig(explicitRoot = null) { const config = getConfig(explicitRoot); // Ensure global defaults are applied if global section is missing return { ...defaultConfig.global, ...(config?.global || {}) }; } function getLogLevel(explicitRoot = null) { // Directly return value from config return getGlobalConfig(explicitRoot).logLevel.toLowerCase(); } function getDebugFlag(explicitRoot = null) { // Directly return value from config, ensure boolean return getGlobalConfig(explicitRoot).debug === true; } function getDefaultSubtasks(explicitRoot = null) { // Directly return value from config, ensure integer const val = getGlobalConfig(explicitRoot).defaultSubtasks; const parsedVal = parseInt(val, 10); return isNaN(parsedVal) ? defaultConfig.global.defaultSubtasks : parsedVal; } function getDefaultNumTasks(explicitRoot = null) { const val = getGlobalConfig(explicitRoot).defaultNumTasks; const parsedVal = parseInt(val, 10); return isNaN(parsedVal) ? defaultConfig.global.defaultNumTasks : parsedVal; } function getDefaultPriority(explicitRoot = null) { // Directly return value from config return getGlobalConfig(explicitRoot).defaultPriority; } function getProjectName(explicitRoot = null) { // Directly return value from config return getGlobalConfig(explicitRoot).projectName; } function getOllamaBaseURL(explicitRoot = null) { // Directly return value from config return getGlobalConfig(explicitRoot).ollamaBaseURL; } function getAzureBaseURL(explicitRoot = null) { // Directly return value from config return getGlobalConfig(explicitRoot).azureBaseURL; } /** * Gets the Google Cloud project ID for Vertex AI from configuration * @param {string|null} explicitRoot - Optional explicit path to the project root. * @returns {string|null} The project ID or null if not configured */ function getVertexProjectId(explicitRoot = null) { // Return value from config return getGlobalConfig(explicitRoot).vertexProjectId; } /** * Gets the Google Cloud location for Vertex AI from configuration * @param {string|null} explicitRoot - Optional explicit path to the project root. * @returns {string} The location or default value of "us-central1" */ function getVertexLocation(explicitRoot = null) { // Return value from config or default return getGlobalConfig(explicitRoot).vertexLocation || "us-central1"; } /** * Gets model parameters (maxTokens, temperature) for a specific role, * considering model-specific overrides from supported-models.json. * @param {string} role - The role ('main', 'research', 'fallback'). * @param {string|null} explicitRoot - Optional explicit path to the project root. * @returns {{maxTokens: number, temperature: number}} */ function getParametersForRole(role, explicitRoot = null) { const roleConfig = getModelConfigForRole(role, explicitRoot); const roleMaxTokens = roleConfig.maxTokens; const roleTemperature = roleConfig.temperature; const modelId = roleConfig.modelId; const providerName = roleConfig.provider; let effectiveMaxTokens = roleMaxTokens; // Start with the role's default try { // Find the model definition in MODEL_MAP const providerModels = MODEL_MAP[providerName]; if (providerModels && Array.isArray(providerModels)) { const modelDefinition = providerModels.find((m) => m.id === modelId); // Check if a model-specific max_tokens is defined and valid if ( modelDefinition && typeof modelDefinition.max_tokens === "number" && modelDefinition.max_tokens > 0 ) { const modelSpecificMaxTokens = modelDefinition.max_tokens; // Use the minimum of the role default and the model specific limit effectiveMaxTokens = Math.min(roleMaxTokens, modelSpecificMaxTokens); log( "debug", `Applying model-specific max_tokens (${modelSpecificMaxTokens}) for ${modelId}. Effective limit: ${effectiveMaxTokens}` ); } else { log( "debug", `No valid model-specific max_tokens override found for ${modelId}. Using role default: ${roleMaxTokens}` ); } } else { log( "debug", `No model definitions found for provider ${providerName} in MODEL_MAP. Using role default maxTokens: ${roleMaxTokens}` ); } } catch (lookupError) { log( "warn", `Error looking up model-specific max_tokens for ${modelId}: ${lookupError.message}. Using role default: ${roleMaxTokens}` ); // Fallback to role default on error effectiveMaxTokens = roleMaxTokens; } return { maxTokens: effectiveMaxTokens, temperature: roleTemperature, }; } /** * Checks if the API key for a given provider is set in the environment. * Checks process.env first, then session.env if session is provided, then .env file if projectRoot provided. * @param {string} providerName - The name of the provider (e.g., 'openai', 'anthropic'). * @param {object|null} [session=null] - The MCP session object (optional). * @param {string|null} [projectRoot=null] - The project root directory (optional, for .env file check). * @returns {boolean} True if the API key is set, false otherwise. */ function isApiKeySet(providerName, session = null, projectRoot = null) { // Define the expected environment variable name for each provider if (providerName?.toLowerCase() === "ollama") { return true; // Indicate key status is effectively "OK" } const keyMap = { openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY", google: "GOOGLE_API_KEY", perplexity: "PERPLEXITY_API_KEY", mistral: "MISTRAL_API_KEY", azure: "AZURE_OPENAI_API_KEY", openrouter: "OPENROUTER_API_KEY", xai: "XAI_API_KEY", vertex: "GOOGLE_API_KEY", // Vertex uses the same key as Google // Add other providers as needed }; const providerKey = providerName?.toLowerCase(); if (!providerKey || !keyMap[providerKey]) { log("warn", `Unknown provider name: ${providerName} in isApiKeySet check.`); return false; } const envVarName = keyMap[providerKey]; const apiKeyValue = resolveEnvVariable(envVarName, session, projectRoot); // Check if the key exists, is not empty, and is not a placeholder return ( apiKeyValue && apiKeyValue.trim() !== "" && !/YOUR_.*_API_KEY_HERE/.test(apiKeyValue) && // General placeholder check !apiKeyValue.includes("KEY_HERE") ); // Another common placeholder pattern } /** * Checks the API key status within .cursor/mcp.json for a given provider. * Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var. * @param {string} providerName The name of the provider. * @param {string|null} projectRoot - Optional explicit path to the project root. * @returns {boolean} True if the key exists and is not a placeholder, false otherwise. */ function getMcpApiKeyStatus(providerName, projectRoot = null) { const rootDir = projectRoot || findProjectRoot(); // Use existing root finding if (!rootDir) { console.warn( chalk.yellow("Warning: Could not find project root to check mcp.json.") ); return false; // Cannot check without root } const mcpConfigPath = path.join(rootDir, ".cursor", "mcp.json"); if (!fs.existsSync(mcpConfigPath)) { // console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.')); return false; // File doesn't exist } try { const mcpConfigRaw = fs.readFileSync(mcpConfigPath, "utf-8"); const mcpConfig = JSON.parse(mcpConfigRaw); const mcpEnv = mcpConfig?.mcpServers?.["taskmaster-ai"]?.env; if (!mcpEnv) { // console.warn(chalk.yellow('Warning: Could not find taskmaster-ai env in mcp.json.')); return false; // Structure missing } let apiKeyToCheck = null; let placeholderValue = null; switch (providerName) { case "anthropic": apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY; placeholderValue = "YOUR_ANTHROPIC_API_KEY_HERE"; break; case "openai": apiKeyToCheck = mcpEnv.OPENAI_API_KEY; placeholderValue = "YOUR_OPENAI_API_KEY_HERE"; // Assuming placeholder matches OPENAI break; case "openrouter": apiKeyToCheck = mcpEnv.OPENROUTER_API_KEY; placeholderValue = "YOUR_OPENROUTER_API_KEY_HERE"; break; case "google": apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; placeholderValue = "YOUR_GOOGLE_API_KEY_HERE"; break; case "perplexity": apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY; placeholderValue = "YOUR_PERPLEXITY_API_KEY_HERE"; break; case "xai": apiKeyToCheck = mcpEnv.XAI_API_KEY; placeholderValue = "YOUR_XAI_API_KEY_HERE"; break; case "ollama": return true; // No key needed case "mistral": apiKeyToCheck = mcpEnv.MISTRAL_API_KEY; placeholderValue = "YOUR_MISTRAL_API_KEY_HERE"; break; case "azure": apiKeyToCheck = mcpEnv.AZURE_OPENAI_API_KEY; placeholderValue = "YOUR_AZURE_OPENAI_API_KEY_HERE"; break; case "vertex": apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key placeholderValue = "YOUR_GOOGLE_API_KEY_HERE"; break; default: return false; // Unknown provider } return !!apiKeyToCheck && !/KEY_HERE$/.test(apiKeyToCheck); } catch (error) { console.error( chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`) ); return false; } } /** * Gets a list of available models based on the MODEL_MAP. * @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>} */ function getAvailableModels() { const available = []; for (const [provider, models] of Object.entries(MODEL_MAP)) { if (models.length > 0) { models.forEach((modelObj) => { // Basic name generation - can be improved const modelId = modelObj.id; const sweScore = modelObj.swe_score; const cost = modelObj.cost_per_1m_tokens; const allowedRoles = modelObj.allowed_roles || ["main", "fallback"]; const nameParts = modelId .split("-") .map((p) => p.charAt(0).toUpperCase() + p.slice(1)); // Handle specific known names better if needed let name = nameParts.join(" "); if (modelId === "claude-3.5-sonnet-20240620") name = "Claude 3.5 Sonnet"; if (modelId === "claude-3-7-sonnet-20250219") name = "Claude 3.7 Sonnet"; if (modelId === "gpt-4o") name = "GPT-4o"; if (modelId === "gpt-4-turbo") name = "GPT-4 Turbo"; if (modelId === "sonar-pro") name = "Perplexity Sonar Pro"; if (modelId === "sonar-mini") name = "Perplexity Sonar Mini"; available.push({ id: modelId, name: name, provider: provider, swe_score: sweScore, cost_per_1m_tokens: cost, allowed_roles: allowedRoles, }); }); } else { // For providers with empty lists (like ollama), maybe add a placeholder or skip available.push({ id: `[${provider}-any]`, name: `Any (${provider})`, provider: provider, }); } } return available; } /** * Writes the configuration object to the file. * @param {Object} config The configuration object to write. * @param {string|null} explicitRoot - Optional explicit path to the project root. * @returns {boolean} True if successful, false otherwise. */ function writeConfig(config, explicitRoot = null) { // ---> Determine root path reliably <--- let rootPath = explicitRoot; if (explicitRoot === null || explicitRoot === undefined) { // Logic matching _loadAndValidateConfig const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot *** if (!foundRoot) { console.error( chalk.red( "Error: Could not determine project root. Configuration not saved." ) ); return false; } rootPath = foundRoot; } // ---> End determine root path logic <--- const configPath = path.basename(rootPath) === CONFIG_FILE_NAME ? rootPath : path.join(rootPath, CONFIG_FILE_NAME); try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); loadedConfig = config; // Update the cache after successful write return true; } catch (error) { console.error( chalk.red( `Error writing configuration to ${configPath}: ${error.message}` ) ); return false; } } /** * Checks if the .taskmasterconfig file exists at the project root * @param {string|null} explicitRoot - Optional explicit path to the project root * @returns {boolean} True if the file exists, false otherwise */ function isConfigFilePresent(explicitRoot = null) { // ---> Determine root path reliably <--- let rootPath = explicitRoot; if (explicitRoot === null || explicitRoot === undefined) { // Logic matching _loadAndValidateConfig const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot *** if (!foundRoot) { return false; // Cannot check if root doesn't exist } rootPath = foundRoot; } // ---> End determine root path logic <--- const configPath = path.join(rootPath, CONFIG_FILE_NAME); return fs.existsSync(configPath); } /** * Gets the user ID from the configuration. * Returns a placeholder that triggers auth/init if no real userId exists. * @param {string|null} explicitRoot - Optional explicit path to the project root. * @returns {string|null} The user ID or placeholder, or null if auth unavailable. */ function getUserId(explicitRoot = null) { const config = getConfig(explicitRoot); // Ensure account section exists if (!config.account) { config.account = { ...defaultConfig.account }; } // If userId exists and is not the placeholder, return it if (config.account.userId && config.account.userId !== "1234567890") { return config.account.userId; } // If userId is null or the placeholder, return the placeholder // This signals to other code that auth/init needs to be attempted return "1234567890"; } /** * Gets a list of all provider names defined in the MODEL_MAP. * @returns {string[]} An array of provider names. */ function getAllProviders() { return Object.keys(MODEL_MAP || {}); } function getBaseUrlForRole(role, explicitRoot = null) { const roleConfig = getModelConfigForRole(role, explicitRoot); return roleConfig && typeof roleConfig.baseURL === "string" ? roleConfig.baseURL : undefined; } // Get telemetryEnabled from account section function getTelemetryEnabled(explicitRoot = null) { const config = getConfig(explicitRoot); return config.account?.telemetryEnabled ?? false; } // Update getUserEmail to use account function getUserEmail(explicitRoot = null) { const config = getConfig(explicitRoot); return config.account?.email || ""; } // Update getMode function to use account function getMode(explicitRoot = null) { const config = getConfig(explicitRoot); return config.account?.mode || "byok"; } export { // Core config access getConfig, writeConfig, ConfigurationError, isConfigFilePresent, // Validation validateProvider, validateProviderModelCombination, VALID_PROVIDERS, MODEL_MAP, getAvailableModels, // Role-specific getters (No env var overrides) getMainProvider, getMainModelId, getMainMaxTokens, getMainTemperature, getResearchProvider, getResearchModelId, getResearchMaxTokens, getResearchTemperature, getFallbackProvider, getFallbackModelId, getFallbackMaxTokens, getFallbackTemperature, getBaseUrlForRole, // Global setting getters (No env var overrides) getLogLevel, getDebugFlag, getDefaultNumTasks, getDefaultSubtasks, getDefaultPriority, getProjectName, getOllamaBaseURL, getAzureBaseURL, getParametersForRole, getUserId, // API Key Checkers (still relevant) isApiKeySet, getMcpApiKeyStatus, // ADD: Function to get all provider names getAllProviders, getVertexProjectId, getVertexLocation, // New getters getTelemetryEnabled, getUserEmail, getMode, };