mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-06-27 00:29:58 +00:00
799 lines
25 KiB
JavaScript
799 lines
25 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import chalk from 'chalk';
|
|
import { fileURLToPath } from 'url';
|
|
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
|
|
import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js';
|
|
import { findConfigPath } from '../../src/utils/path-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
|
|
}
|
|
|
|
// Define valid providers dynamically from the loaded MODEL_MAP
|
|
const VALID_PROVIDERS = Object.keys(MODEL_MAP || {});
|
|
|
|
// Default configuration values (used if config file is missing or incomplete)
|
|
const DEFAULTS = {
|
|
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
|
|
}
|
|
},
|
|
global: {
|
|
logLevel: 'info',
|
|
debug: false,
|
|
defaultSubtasks: 5,
|
|
defaultPriority: 'medium',
|
|
projectName: 'Task Master',
|
|
ollamaBaseURL: 'http://localhost:11434/api',
|
|
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com'
|
|
}
|
|
};
|
|
|
|
// --- 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 = DEFAULTS; // 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 <---
|
|
|
|
// --- Find configuration file using centralized path utility ---
|
|
const configPath = findConfigPath(null, { projectRoot: rootToUse });
|
|
let config = { ...defaults }; // Start with a deep copy of defaults
|
|
let configExists = false;
|
|
|
|
if (configPath) {
|
|
configExists = true;
|
|
const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE);
|
|
|
|
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 }
|
|
};
|
|
configSource = `file (${configPath})`; // Update source info
|
|
|
|
// Issue deprecation warning if using legacy config file
|
|
if (isLegacy) {
|
|
console.warn(
|
|
chalk.yellow(
|
|
`⚠️ DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
|
|
)
|
|
);
|
|
}
|
|
|
|
// --- 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: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
|
|
)
|
|
);
|
|
} else {
|
|
console.warn(
|
|
chalk.yellow(
|
|
`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
|
|
)
|
|
);
|
|
}
|
|
// Keep config as defaults
|
|
config = { ...defaults };
|
|
configSource = `defaults (no config file found at ${rootToUse})`;
|
|
}
|
|
|
|
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 DEFAULTS.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 { ...DEFAULTS.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 Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
|
|
}
|
|
|
|
function getDefaultNumTasks(explicitRoot = null) {
|
|
const val = getGlobalConfig(explicitRoot).defaultNumTasks;
|
|
const parsedVal = parseInt(val, 10);
|
|
return Number.isNaN(parsedVal) ? DEFAULTS.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;
|
|
}
|
|
|
|
function getBedrockBaseURL(explicitRoot = null) {
|
|
// Directly return value from config
|
|
return getGlobalConfig(explicitRoot).bedrockBaseURL;
|
|
}
|
|
|
|
/**
|
|
* 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 <---
|
|
|
|
// Use new config location: .taskmaster/config.json
|
|
const taskmasterDir = path.join(rootPath, '.taskmaster');
|
|
const configPath = path.join(taskmasterDir, 'config.json');
|
|
|
|
try {
|
|
// Ensure .taskmaster directory exists
|
|
if (!fs.existsSync(taskmasterDir)) {
|
|
fs.mkdirSync(taskmasterDir, { recursive: true });
|
|
}
|
|
|
|
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 a configuration file exists at the project root (new or legacy location)
|
|
* @param {string|null} explicitRoot - Optional explicit path to the project root
|
|
* @returns {boolean} True if the file exists, false otherwise
|
|
*/
|
|
function isConfigFilePresent(explicitRoot = null) {
|
|
return findConfigPath(null, { projectRoot: explicitRoot }) !== null;
|
|
}
|
|
|
|
/**
|
|
* Gets the user ID from the configuration.
|
|
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
|
* @returns {string|null} The user ID or null if not found.
|
|
*/
|
|
function getUserId(explicitRoot = null) {
|
|
const config = getConfig(explicitRoot);
|
|
if (!config.global) {
|
|
config.global = {}; // Ensure global object exists
|
|
}
|
|
if (!config.global.userId) {
|
|
config.global.userId = '1234567890';
|
|
// Attempt to write the updated config.
|
|
// It's important that writeConfig correctly resolves the path
|
|
// using explicitRoot, similar to how getConfig does.
|
|
const success = writeConfig(config, explicitRoot);
|
|
if (!success) {
|
|
// Log an error or handle the failure to write,
|
|
// though for now, we'll proceed with the in-memory default.
|
|
log(
|
|
'warning',
|
|
'Failed to write updated configuration with new userId. Please let the developers know.'
|
|
);
|
|
}
|
|
}
|
|
return config.global.userId;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
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,
|
|
getBedrockBaseURL,
|
|
getParametersForRole,
|
|
getUserId,
|
|
// API Key Checkers (still relevant)
|
|
isApiKeySet,
|
|
getMcpApiKeyStatus,
|
|
// ADD: Function to get all provider names
|
|
getAllProviders,
|
|
getVertexProjectId,
|
|
getVertexLocation
|
|
};
|