refactor(config)!: Enforce .taskmasterconfig and remove env var overrides

BREAKING CHANGE: Taskmaster now requires a `.taskmasterconfig` file for model/parameter settings. Environment variables (except API keys) are no longer used for overrides.

- Throws an error if `.taskmasterconfig` is missing, guiding user to run `task-master models --setup`." -m "- Removed env var checks from config getters in `config-manager.js`." -m "- Updated `env.example` to remove obsolete variables." -m "- Refined missing config file error message in `commands.js`.
This commit is contained in:
Eyal Toledano
2025-04-21 22:25:04 -04:00
parent a40805adf7
commit 515dcae965
4 changed files with 118 additions and 50 deletions

View File

@@ -17,4 +17,4 @@ DEFAULT_SUBTASKS=5 # Default number of subtasks
DEFAULT_PRIORITY=medium # Default priority for generated tasks (high, medium, low)
# Project Metadata (Optional)
PROJECT_NAME=Your Project Name # Override default project name in tasks.json
PROJECT_NAME=Your Project Name # Override default project name in tasks.json

View File

@@ -7,15 +7,4 @@ GROK_API_KEY=your_grok_api_key_here # Optional, for XAI Grok mod
MISTRAL_API_KEY=your_mistral_key_here # Optional, for Mistral AI models.
AZURE_OPENAI_API_KEY=your_azure_key_here # Optional, for Azure OpenAI models.
AZURE_OPENAI_ENDPOINT=your_azure_endpoint_here # Optional, for Azure OpenAI.
# Optional - defaults shown
MODEL=claude-3-7-sonnet-20250219 # Recommended models: claude-3-7-sonnet-20250219, claude-3-opus-20240229 (Required)
PERPLEXITY_MODEL=sonar-pro # Make sure you have access to sonar-pro otherwise you can use sonar regular (Optional)
MAX_TOKENS=64000 # Maximum tokens for model responses (Required)
TEMPERATURE=0.2 # Temperature for model responses (0.0-1.0) - lower = less creativity and follow your prompt closely (Required)
DEBUG=false # Enable debug logging (true/false)
LOG_LEVEL=info # Log level (debug, info, warn, error)
DEFAULT_SUBTASKS=5 # Default number of subtasks when expanding
DEFAULT_PRIORITY=medium # Default priority for generated tasks (high, medium, low)
PROJECT_NAME={{projectName}} # Project name for tasks.json metadata
OLLAMA_BASE_URL=http://localhost:11434/api # Base URL for local Ollama instance (Optional)
OLLAMA_BASE_URL=http://localhost:11434/api # Base URL for local Ollama instance (Optional)

View File

@@ -53,7 +53,8 @@ import {
getMcpApiKeyStatus,
getDebugFlag,
getConfig,
writeConfig
writeConfig,
ConfigurationError // Import the custom error
} from './config-manager.js';
import {
@@ -2377,6 +2378,8 @@ async function runCLI(argv = process.argv) {
const updateCheckPromise = checkForUpdate();
// Setup and parse
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
// This means the ConfigurationError might be thrown here if .taskmasterconfig is missing.
const programInstance = setupCLI();
await programInstance.parseAsync(argv);
@@ -2389,10 +2392,56 @@ async function runCLI(argv = process.argv) {
);
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (getDebugFlag()) {
console.error(error);
// ** Specific catch block for missing configuration file **
if (error instanceof ConfigurationError) {
console.error(
boxen(
chalk.red.bold('Configuration Update Required!') +
'\n\n' +
chalk.white('Taskmaster now uses the ') +
chalk.yellow.bold('.taskmasterconfig') +
chalk.white(
' file in your project root for AI model choices and settings.\n\n' +
'This file appears to be '
) +
chalk.red.bold('missing') +
chalk.white('. No worries though.\n\n') +
chalk.cyan.bold('To create this file, run the interactive setup:') +
'\n' +
chalk.green(' task-master models --setup') +
'\n\n' +
chalk.white.bold('Key Points:') +
'\n' +
chalk.white('* ') +
chalk.yellow.bold('.taskmasterconfig') +
chalk.white(
': Stores your AI model settings (do not manually edit)\n'
) +
chalk.white('* ') +
chalk.yellow.bold('.env & .mcp.json') +
chalk.white(': Still used ') +
chalk.red.bold('only') +
chalk.white(' for your AI provider API keys.\n\n') +
chalk.cyan(
'`task-master models` to check your config & available models\n'
) +
chalk.cyan(
'`task-master models --setup` to adjust the AI models used by Taskmaster'
),
{
padding: 1,
margin: { top: 1 },
borderColor: 'red',
borderStyle: 'round'
}
)
);
} else {
// Generic error handling for other errors
console.error(chalk.red(`Error: ${error.message}`));
if (getDebugFlag()) {
console.error(error);
}
}
process.exit(1);

View File

@@ -75,10 +75,19 @@ const DEFAULTS = {
// --- Internal Config Loading ---
let loadedConfig = null; // Cache for loaded config
// Custom Error for configuration issues
class ConfigurationError extends Error {
constructor(message) {
super(message);
this.name = 'ConfigurationError';
}
}
function _loadAndValidateConfig(explicitRoot = null) {
// Determine the root path to use
const rootToUse = explicitRoot || findProjectRoot();
const defaults = DEFAULTS; // Use the defined defaults
let configExists = false;
if (!rootToUse) {
console.warn(
@@ -86,34 +95,37 @@ function _loadAndValidateConfig(explicitRoot = null) {
'Warning: Could not determine project root. Using default configuration.'
)
);
return defaults;
// Allow proceeding with defaults if root finding fails, but validation later might trigger error
// Or perhaps throw here? Let's throw later based on file existence check.
}
const configPath = path.join(rootToUse, CONFIG_FILE_NAME);
const configPath = rootToUse ? path.join(rootToUse, CONFIG_FILE_NAME) : null;
if (fs.existsSync(configPath)) {
let config = defaults; // Start with defaults
if (configPath && fs.existsSync(configPath)) {
configExists = true;
try {
const rawData = fs.readFileSync(configPath, 'utf-8');
const parsedConfig = JSON.parse(rawData);
// Deep merge with defaults
const config = {
// Deep merge parsed config onto defaults
config = {
models: {
main: { ...defaults.models.main, ...parsedConfig?.models?.main },
research: {
...defaults.models.research,
...parsedConfig?.models?.research
},
// Fallback needs careful merging - only merge if provider/model exist
fallback:
parsedConfig?.models?.fallback?.provider &&
parsedConfig?.models?.fallback?.modelId
? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
: { ...defaults.models.fallback } // Use default params even if provider/model missing
: { ...defaults.models.fallback }
},
global: { ...defaults.global, ...parsedConfig?.global }
};
// --- Validation ---
// --- Validation (Only warn if file exists but content is invalid) ---
// Validate main provider/model
if (!validateProvider(config.models.main.provider)) {
console.warn(
@@ -123,7 +135,6 @@ function _loadAndValidateConfig(explicitRoot = null) {
);
config.models.main = { ...defaults.models.main };
}
// Optional: Add warning for model combination if desired
// Validate research provider/model
if (!validateProvider(config.models.research.provider)) {
@@ -134,7 +145,6 @@ function _loadAndValidateConfig(explicitRoot = null) {
);
config.models.research = { ...defaults.models.research };
}
// Optional: Add warning for model combination if desired
// Validate fallback provider if it exists
if (
@@ -146,24 +156,27 @@ function _loadAndValidateConfig(explicitRoot = null) {
`Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${CONFIG_FILE_NAME}. Fallback model configuration will be ignored.`
)
);
// Clear invalid fallback provider/model, but keep default params if needed elsewhere
config.models.fallback.provider = undefined;
config.models.fallback.modelId = undefined;
}
return config;
} catch (error) {
console.error(
chalk.red(
`Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
)
);
return defaults;
config = defaults; // Reset to defaults on parse error
// Do not throw ConfigurationError here, allow fallback to defaults if file is corrupt
}
} else {
// Config file doesn't exist, use defaults
return defaults;
// Config file doesn't exist
// **Strict Check**: Throw error if config file is missing
throw new ConfigurationError(
`${CONFIG_FILE_NAME} not found at project root (${rootToUse || 'unknown'}).`
);
}
return config;
}
/**
@@ -218,8 +231,13 @@ 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}`);
return DEFAULTS.models[role] || {}; // Fallback to default for the role
// This shouldn't happen if _loadAndValidateConfig ensures defaults
// But as a safety net, log and return defaults
log(
'warn',
`No model configuration found for role: ${role}. Returning default.`
);
return DEFAULTS.models[role] || {};
}
return roleConfig;
}
@@ -233,10 +251,12 @@ function getMainModelId(explicitRoot = null) {
}
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;
}
@@ -249,30 +269,32 @@ function getResearchModelId(explicitRoot = null) {
}
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) {
// Specifically check if provider is set, as fallback is optional
return getModelConfigForRole('fallback', explicitRoot).provider || undefined;
// Directly return value from config (will be undefined if not set)
return getModelConfigForRole('fallback', explicitRoot).provider;
}
function getFallbackModelId(explicitRoot = null) {
// Specifically check if modelId is set
return getModelConfigForRole('fallback', explicitRoot).modelId || undefined;
// Directly return value from config
return getModelConfigForRole('fallback', explicitRoot).modelId;
}
function getFallbackMaxTokens(explicitRoot = null) {
// Return fallback tokens even if provider/model isn't set, in case it's needed generically
// Directly return value from config
return getModelConfigForRole('fallback', explicitRoot).maxTokens;
}
function getFallbackTemperature(explicitRoot = null) {
// Return fallback temp even if provider/model isn't set
// Directly return value from config
return getModelConfigForRole('fallback', explicitRoot).temperature;
}
@@ -280,32 +302,39 @@ function getFallbackTemperature(explicitRoot = null) {
function getGlobalConfig(explicitRoot = null) {
const config = getConfig(explicitRoot);
return config?.global || DEFAULTS.global;
// Ensure global defaults are applied if global section is missing
return { ...DEFAULTS.global, ...(config?.global || {}) };
}
function getLogLevel(explicitRoot = null) {
return getGlobalConfig(explicitRoot).logLevel;
// Directly return value from config
return getGlobalConfig(explicitRoot).logLevel.toLowerCase();
}
function getDebugFlag(explicitRoot = null) {
// Ensure boolean type
// Directly return value from config, ensure boolean
return getGlobalConfig(explicitRoot).debug === true;
}
function getDefaultSubtasks(explicitRoot = null) {
// Ensure integer type
return parseInt(getGlobalConfig(explicitRoot).defaultSubtasks, 10);
// Directly return value from config, ensure integer
const val = getGlobalConfig(explicitRoot).defaultSubtasks;
const parsedVal = parseInt(val, 10);
return isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : 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;
}
@@ -500,8 +529,9 @@ function writeConfig(config, explicitRoot = null) {
export {
// Core config access
getConfig, // Might still be useful for getting the whole object
getConfig,
writeConfig,
ConfigurationError, // Export custom error type
// Validation
validateProvider,
@@ -510,7 +540,7 @@ export {
MODEL_MAP,
getAvailableModels,
// Role-specific getters
// Role-specific getters (No env var overrides)
getMainProvider,
getMainModelId,
getMainMaxTokens,
@@ -524,7 +554,7 @@ export {
getFallbackMaxTokens,
getFallbackTemperature,
// Global setting getters
// Global setting getters (No env var overrides)
getLogLevel,
getDebugFlag,
getDefaultSubtasks,