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:
@@ -17,4 +17,4 @@ DEFAULT_SUBTASKS=5 # Default number of subtasks
|
|||||||
DEFAULT_PRIORITY=medium # Default priority for generated tasks (high, medium, low)
|
DEFAULT_PRIORITY=medium # Default priority for generated tasks (high, medium, low)
|
||||||
|
|
||||||
# Project Metadata (Optional)
|
# 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
|
||||||
@@ -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.
|
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_API_KEY=your_azure_key_here # Optional, for Azure OpenAI models.
|
||||||
AZURE_OPENAI_ENDPOINT=your_azure_endpoint_here # Optional, for Azure OpenAI.
|
AZURE_OPENAI_ENDPOINT=your_azure_endpoint_here # Optional, for Azure OpenAI.
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434/api # Base URL for local Ollama instance (Optional)
|
||||||
# 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)
|
|
||||||
@@ -53,7 +53,8 @@ import {
|
|||||||
getMcpApiKeyStatus,
|
getMcpApiKeyStatus,
|
||||||
getDebugFlag,
|
getDebugFlag,
|
||||||
getConfig,
|
getConfig,
|
||||||
writeConfig
|
writeConfig,
|
||||||
|
ConfigurationError // Import the custom error
|
||||||
} from './config-manager.js';
|
} from './config-manager.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -2377,6 +2378,8 @@ async function runCLI(argv = process.argv) {
|
|||||||
const updateCheckPromise = checkForUpdate();
|
const updateCheckPromise = checkForUpdate();
|
||||||
|
|
||||||
// Setup and parse
|
// 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();
|
const programInstance = setupCLI();
|
||||||
await programInstance.parseAsync(argv);
|
await programInstance.parseAsync(argv);
|
||||||
|
|
||||||
@@ -2389,10 +2392,56 @@ async function runCLI(argv = process.argv) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(chalk.red(`Error: ${error.message}`));
|
// ** Specific catch block for missing configuration file **
|
||||||
|
if (error instanceof ConfigurationError) {
|
||||||
if (getDebugFlag()) {
|
console.error(
|
||||||
console.error(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);
|
process.exit(1);
|
||||||
|
|||||||
@@ -75,10 +75,19 @@ const DEFAULTS = {
|
|||||||
// --- Internal Config Loading ---
|
// --- Internal Config Loading ---
|
||||||
let loadedConfig = null; // Cache for loaded config
|
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) {
|
function _loadAndValidateConfig(explicitRoot = null) {
|
||||||
// Determine the root path to use
|
// Determine the root path to use
|
||||||
const rootToUse = explicitRoot || findProjectRoot();
|
const rootToUse = explicitRoot || findProjectRoot();
|
||||||
const defaults = DEFAULTS; // Use the defined defaults
|
const defaults = DEFAULTS; // Use the defined defaults
|
||||||
|
let configExists = false;
|
||||||
|
|
||||||
if (!rootToUse) {
|
if (!rootToUse) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -86,34 +95,37 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
|||||||
'Warning: Could not determine project root. Using default configuration.'
|
'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 {
|
try {
|
||||||
const rawData = fs.readFileSync(configPath, 'utf-8');
|
const rawData = fs.readFileSync(configPath, 'utf-8');
|
||||||
const parsedConfig = JSON.parse(rawData);
|
const parsedConfig = JSON.parse(rawData);
|
||||||
|
|
||||||
// Deep merge with defaults
|
// Deep merge parsed config onto defaults
|
||||||
const config = {
|
config = {
|
||||||
models: {
|
models: {
|
||||||
main: { ...defaults.models.main, ...parsedConfig?.models?.main },
|
main: { ...defaults.models.main, ...parsedConfig?.models?.main },
|
||||||
research: {
|
research: {
|
||||||
...defaults.models.research,
|
...defaults.models.research,
|
||||||
...parsedConfig?.models?.research
|
...parsedConfig?.models?.research
|
||||||
},
|
},
|
||||||
// Fallback needs careful merging - only merge if provider/model exist
|
|
||||||
fallback:
|
fallback:
|
||||||
parsedConfig?.models?.fallback?.provider &&
|
parsedConfig?.models?.fallback?.provider &&
|
||||||
parsedConfig?.models?.fallback?.modelId
|
parsedConfig?.models?.fallback?.modelId
|
||||||
? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
|
? { ...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 }
|
global: { ...defaults.global, ...parsedConfig?.global }
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Validation ---
|
// --- Validation (Only warn if file exists but content is invalid) ---
|
||||||
// Validate main provider/model
|
// Validate main provider/model
|
||||||
if (!validateProvider(config.models.main.provider)) {
|
if (!validateProvider(config.models.main.provider)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -123,7 +135,6 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
|||||||
);
|
);
|
||||||
config.models.main = { ...defaults.models.main };
|
config.models.main = { ...defaults.models.main };
|
||||||
}
|
}
|
||||||
// Optional: Add warning for model combination if desired
|
|
||||||
|
|
||||||
// Validate research provider/model
|
// Validate research provider/model
|
||||||
if (!validateProvider(config.models.research.provider)) {
|
if (!validateProvider(config.models.research.provider)) {
|
||||||
@@ -134,7 +145,6 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
|||||||
);
|
);
|
||||||
config.models.research = { ...defaults.models.research };
|
config.models.research = { ...defaults.models.research };
|
||||||
}
|
}
|
||||||
// Optional: Add warning for model combination if desired
|
|
||||||
|
|
||||||
// Validate fallback provider if it exists
|
// Validate fallback provider if it exists
|
||||||
if (
|
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.`
|
`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.provider = undefined;
|
||||||
config.models.fallback.modelId = undefined;
|
config.models.fallback.modelId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(
|
chalk.red(
|
||||||
`Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
|
`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 {
|
} else {
|
||||||
// Config file doesn't exist, use defaults
|
// Config file doesn't exist
|
||||||
return defaults;
|
// **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 config = getConfig(explicitRoot);
|
||||||
const roleConfig = config?.models?.[role];
|
const roleConfig = config?.models?.[role];
|
||||||
if (!roleConfig) {
|
if (!roleConfig) {
|
||||||
log('warn', `No model configuration found for role: ${role}`);
|
// This shouldn't happen if _loadAndValidateConfig ensures defaults
|
||||||
return DEFAULTS.models[role] || {}; // Fallback to default for the role
|
// 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;
|
return roleConfig;
|
||||||
}
|
}
|
||||||
@@ -233,10 +251,12 @@ function getMainModelId(explicitRoot = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMainMaxTokens(explicitRoot = null) {
|
function getMainMaxTokens(explicitRoot = null) {
|
||||||
|
// Directly return value from config (which includes defaults)
|
||||||
return getModelConfigForRole('main', explicitRoot).maxTokens;
|
return getModelConfigForRole('main', explicitRoot).maxTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMainTemperature(explicitRoot = null) {
|
function getMainTemperature(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getModelConfigForRole('main', explicitRoot).temperature;
|
return getModelConfigForRole('main', explicitRoot).temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,30 +269,32 @@ function getResearchModelId(explicitRoot = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getResearchMaxTokens(explicitRoot = null) {
|
function getResearchMaxTokens(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getModelConfigForRole('research', explicitRoot).maxTokens;
|
return getModelConfigForRole('research', explicitRoot).maxTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResearchTemperature(explicitRoot = null) {
|
function getResearchTemperature(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getModelConfigForRole('research', explicitRoot).temperature;
|
return getModelConfigForRole('research', explicitRoot).temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFallbackProvider(explicitRoot = null) {
|
function getFallbackProvider(explicitRoot = null) {
|
||||||
// Specifically check if provider is set, as fallback is optional
|
// Directly return value from config (will be undefined if not set)
|
||||||
return getModelConfigForRole('fallback', explicitRoot).provider || undefined;
|
return getModelConfigForRole('fallback', explicitRoot).provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFallbackModelId(explicitRoot = null) {
|
function getFallbackModelId(explicitRoot = null) {
|
||||||
// Specifically check if modelId is set
|
// Directly return value from config
|
||||||
return getModelConfigForRole('fallback', explicitRoot).modelId || undefined;
|
return getModelConfigForRole('fallback', explicitRoot).modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFallbackMaxTokens(explicitRoot = null) {
|
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;
|
return getModelConfigForRole('fallback', explicitRoot).maxTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFallbackTemperature(explicitRoot = null) {
|
function getFallbackTemperature(explicitRoot = null) {
|
||||||
// Return fallback temp even if provider/model isn't set
|
// Directly return value from config
|
||||||
return getModelConfigForRole('fallback', explicitRoot).temperature;
|
return getModelConfigForRole('fallback', explicitRoot).temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,32 +302,39 @@ function getFallbackTemperature(explicitRoot = null) {
|
|||||||
|
|
||||||
function getGlobalConfig(explicitRoot = null) {
|
function getGlobalConfig(explicitRoot = null) {
|
||||||
const config = getConfig(explicitRoot);
|
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) {
|
function getLogLevel(explicitRoot = null) {
|
||||||
return getGlobalConfig(explicitRoot).logLevel;
|
// Directly return value from config
|
||||||
|
return getGlobalConfig(explicitRoot).logLevel.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDebugFlag(explicitRoot = null) {
|
function getDebugFlag(explicitRoot = null) {
|
||||||
// Ensure boolean type
|
// Directly return value from config, ensure boolean
|
||||||
return getGlobalConfig(explicitRoot).debug === true;
|
return getGlobalConfig(explicitRoot).debug === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultSubtasks(explicitRoot = null) {
|
function getDefaultSubtasks(explicitRoot = null) {
|
||||||
// Ensure integer type
|
// Directly return value from config, ensure integer
|
||||||
return parseInt(getGlobalConfig(explicitRoot).defaultSubtasks, 10);
|
const val = getGlobalConfig(explicitRoot).defaultSubtasks;
|
||||||
|
const parsedVal = parseInt(val, 10);
|
||||||
|
return isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultPriority(explicitRoot = null) {
|
function getDefaultPriority(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getGlobalConfig(explicitRoot).defaultPriority;
|
return getGlobalConfig(explicitRoot).defaultPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectName(explicitRoot = null) {
|
function getProjectName(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getGlobalConfig(explicitRoot).projectName;
|
return getGlobalConfig(explicitRoot).projectName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOllamaBaseUrl(explicitRoot = null) {
|
function getOllamaBaseUrl(explicitRoot = null) {
|
||||||
|
// Directly return value from config
|
||||||
return getGlobalConfig(explicitRoot).ollamaBaseUrl;
|
return getGlobalConfig(explicitRoot).ollamaBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,8 +529,9 @@ function writeConfig(config, explicitRoot = null) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
// Core config access
|
// Core config access
|
||||||
getConfig, // Might still be useful for getting the whole object
|
getConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
|
ConfigurationError, // Export custom error type
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
validateProvider,
|
validateProvider,
|
||||||
@@ -510,7 +540,7 @@ export {
|
|||||||
MODEL_MAP,
|
MODEL_MAP,
|
||||||
getAvailableModels,
|
getAvailableModels,
|
||||||
|
|
||||||
// Role-specific getters
|
// Role-specific getters (No env var overrides)
|
||||||
getMainProvider,
|
getMainProvider,
|
||||||
getMainModelId,
|
getMainModelId,
|
||||||
getMainMaxTokens,
|
getMainMaxTokens,
|
||||||
@@ -524,7 +554,7 @@ export {
|
|||||||
getFallbackMaxTokens,
|
getFallbackMaxTokens,
|
||||||
getFallbackTemperature,
|
getFallbackTemperature,
|
||||||
|
|
||||||
// Global setting getters
|
// Global setting getters (No env var overrides)
|
||||||
getLogLevel,
|
getLogLevel,
|
||||||
getDebugFlag,
|
getDebugFlag,
|
||||||
getDefaultSubtasks,
|
getDefaultSubtasks,
|
||||||
|
|||||||
Reference in New Issue
Block a user