eat(models): Add MCP support for models command and improve configuration docs

This commit implements several related improvements to the models command and configuration system:

- Added MCP support for the models command:
  - Created new direct function implementation in models.js
  - Registered modelsDirect in task-master-core.js for proper export
  - Added models tool registration in tools/index.js
  - Ensured project name replacement when copying .taskmasterconfig in init.js

- Improved .taskmasterconfig copying during project initialization:
  - Added copyTemplateFile() call in createProjectStructure()
  - Ensured project name is properly replaced in the config

- Restructured tool registration in logical workflow groups:
  - Organized registration into 6 functional categories
  - Improved command ordering to follow typical workflow
  - Added clear group comments for maintainability

- Enhanced documentation in cursor rules:
  - Updated dev_workflow.mdc with clearer config management instructions
  - Added comprehensive models command reference to taskmaster.mdc
  - Clarified CLI vs MCP usage patterns and options
  - Added warning against manual .taskmasterconfig editing
This commit is contained in:
Eyal Toledano
2025-04-23 15:47:33 -04:00
parent 78a5376796
commit 6cb213ebbd
13 changed files with 1291 additions and 565 deletions

View File

@@ -0,0 +1,8 @@
---
'task-master-ai': patch
---
- Adds a 'models' CLI and MCP command to get the current model configuration, available models, and gives the ability to set main/research/fallback models."
- In the CLI, `task-master models` shows the current models config. Using the `--setup` flag launches an interactive set up that allows you to easily select the models you want to use for each of the three roles. Use `q` during the interactive setup to cancel the setup.
- In the MCP, responses are simplified in RESTful format (instead of the full CLI output). The agent can use the `models` tool with different arguments, including `listAvailableModels` to get available models. Run without arguments, it will return the current configuration. Arguments are available to set the model for each of the three roles. This allows you to manage Taskmaster AI providers and models directly from either the CLI or MCP or both.
- Updated the CLI help menu when you run `task-master` to include missing commands and .taskmasterconfig information.

View File

@@ -107,8 +107,8 @@ Taskmaster configuration is managed through two main mechanisms:
* Located in the project root directory.
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
* **View/Set specific models via `task-master models` command or `models` MCP tool.**
* Created automatically when you run `task-master models --setup` for the first time.
* View the current configuration using `task-master models`.
2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs.
@@ -116,7 +116,7 @@ Taskmaster configuration is managed through two main mechanisms:
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
* Available keys/variables: See `assets/env.example` or the `Configuration` section in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
**Important:** Non-API key settings (like `MODEL`, `MAX_TOKENS`, `LOG_LEVEL`) are **no longer configured via environment variables**. Use `task-master models --setup` instead.
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the mcp.json
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the .env in the root of the project.
@@ -215,5 +215,10 @@ Once a task has been broken down into subtasks using `expand_task` or similar me
`rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts.
---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*
`rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts.
---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

View File

@@ -55,6 +55,31 @@ This document provides a detailed reference for interacting with Taskmaster, cov
---
## AI Model Configuration
### 2. Manage Models (`models`)
* **MCP Tool:** `models`
* **CLI Command:** `task-master models [options]`
* **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback).`
* **Key MCP Parameters/Options:**
* `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
* `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
* `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
* `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
* `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
* **Key CLI Options:**
* `--set-main <model_id>`: `Set the primary model.`
* `--set-research <model_id>`: `Set the research model.`
* `--set-fallback <model_id>`: `Set the fallback model.`
* `--setup`: `Run interactive setup to configure models and other settings.`
* **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models.
* **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration.
* **Notes:** Configuration is stored in `.taskmasterconfig` in the project root. This command/tool modifies that file. Use `listAvailableModels` to ensure the selected model is supported.
* **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
* **Warning:** DO NOT MANUALLY EDIT THE .taskmasterconfig FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.
---
## Task Listing & Viewing
### 3. Get Tasks (`get_tasks`)

View File

@@ -16,7 +16,7 @@
"provider": "anthropic",
"modelId": "claude-3.5-sonnet-20240620",
"maxTokens": 120000,
"temperature": 0.1
"temperature": 0.2
}
},
"global": {

30
assets/.taskmasterconfig Normal file
View File

@@ -0,0 +1,30 @@
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-3-7-sonnet-20250219",
"maxTokens": 120000,
"temperature": 0.2
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"maxTokens": 8700,
"temperature": 0.1
},
"fallback": {
"provider": "anthropic",
"modelId": "claude-3.5-sonnet-20240620",
"maxTokens": 120000,
"temperature": 0.1
}
},
"global": {
"logLevel": "info",
"debug": false,
"defaultSubtasks": 5,
"defaultPriority": "medium",
"projectName": "Taskmaster",
"ollamaBaseUrl": "http://localhost:11434/api"
}
}

View File

@@ -0,0 +1,98 @@
/**
* models.js
* Direct function for managing AI model configurations via MCP
*/
import {
getModelConfiguration,
getAvailableModelsList,
setModel
} from '../../../../scripts/modules/task-manager/models.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
/**
* Get or update model configuration
* @param {Object} args - Arguments passed by the MCP tool
* @param {Object} log - MCP logger
* @param {Object} context - MCP context (contains session)
* @returns {Object} Result object with success, data/error fields
*/
export async function modelsDirect(args, log, context = {}) {
const { session } = context;
const { projectRoot } = args; // Extract projectRoot from args
// Create a logger wrapper that the core functions can use
const logWrapper = {
info: (message, ...args) => log.info(message, ...args),
warn: (message, ...args) => log.warn(message, ...args),
error: (message, ...args) => log.error(message, ...args),
debug: (message, ...args) =>
log.debug ? log.debug(message, ...args) : null,
success: (message, ...args) => log.info(message, ...args)
};
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
log.info(`Using project root: ${projectRoot}`);
try {
enableSilentMode();
try {
// Check for the listAvailableModels flag
if (args.listAvailableModels === true) {
return await getAvailableModelsList({
session,
mcpLog: logWrapper,
projectRoot // Pass projectRoot to function
});
}
// Handle setting a specific model
if (args.setMain) {
return await setModel('main', args.setMain, {
session,
mcpLog: logWrapper,
projectRoot // Pass projectRoot to function
});
}
if (args.setResearch) {
return await setModel('research', args.setResearch, {
session,
mcpLog: logWrapper,
projectRoot // Pass projectRoot to function
});
}
if (args.setFallback) {
return await setModel('fallback', args.setFallback, {
session,
mcpLog: logWrapper,
projectRoot // Pass projectRoot to function
});
}
// Default action: get current configuration
return await getModelConfiguration({
session,
mcpLog: logWrapper,
projectRoot // Pass projectRoot to function
});
} finally {
disableSilentMode();
}
} catch (error) {
log.error(`Error in models_direct: ${error.message}`);
return {
success: false,
error: {
code: 'DIRECT_FUNCTION_ERROR',
message: error.message,
details: error.stack
}
};
}
}

View File

@@ -29,6 +29,7 @@ import { complexityReportDirect } from './direct-functions/complexity-report.js'
import { addDependencyDirect } from './direct-functions/add-dependency.js';
import { removeTaskDirect } from './direct-functions/remove-task.js';
import { initializeProjectDirect } from './direct-functions/initialize-project-direct.js';
import { modelsDirect } from './direct-functions/models.js';
// Re-export utility functions
export { findTasksJsonPath } from './utils/path-utils.js';
@@ -66,7 +67,9 @@ export const directFunctions = new Map([
['fixDependenciesDirect', fixDependenciesDirect],
['complexityReportDirect', complexityReportDirect],
['addDependencyDirect', addDependencyDirect],
['removeTaskDirect', removeTaskDirect]
['removeTaskDirect', removeTaskDirect],
['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect]
]);
// Re-export all direct function implementations
@@ -94,5 +97,6 @@ export {
complexityReportDirect,
addDependencyDirect,
removeTaskDirect,
initializeProjectDirect
initializeProjectDirect,
modelsDirect
};

View File

@@ -27,6 +27,7 @@ import { registerComplexityReportTool } from './complexity-report.js';
import { registerAddDependencyTool } from './add-dependency.js';
import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.js';
/**
* Register all Task Master tools with the MCP server
@@ -34,30 +35,43 @@ import { registerInitializeProjectTool } from './initialize-project.js';
*/
export function registerTaskMasterTools(server) {
try {
// Register each tool
registerListTasksTool(server);
registerSetTaskStatusTool(server);
// Register each tool in a logical workflow order
// Group 1: Initialization & Setup
registerInitializeProjectTool(server);
registerModelsTool(server);
registerParsePRDTool(server);
// Group 2: Task Listing & Viewing
registerListTasksTool(server);
registerShowTaskTool(server);
registerNextTaskTool(server);
registerComplexityReportTool(server);
// Group 3: Task Status & Management
registerSetTaskStatusTool(server);
registerGenerateTool(server);
// Group 4: Task Creation & Modification
registerAddTaskTool(server);
registerAddSubtaskTool(server);
registerUpdateTool(server);
registerUpdateTaskTool(server);
registerUpdateSubtaskTool(server);
registerGenerateTool(server);
registerShowTaskTool(server);
registerNextTaskTool(server);
registerExpandTaskTool(server);
registerAddTaskTool(server);
registerAddSubtaskTool(server);
registerRemoveTaskTool(server);
registerRemoveSubtaskTool(server);
registerAnalyzeTool(server);
registerClearSubtasksTool(server);
// Group 5: Task Analysis & Expansion
registerAnalyzeTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 6: Dependency Management
registerAddDependencyTool(server);
registerRemoveDependencyTool(server);
registerValidateDependenciesTool(server);
registerFixDependenciesTool(server);
registerComplexityReportTool(server);
registerAddDependencyTool(server);
registerRemoveTaskTool(server);
registerInitializeProjectTool(server);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;

View File

@@ -0,0 +1,81 @@
/**
* models.js
* MCP tool for managing AI model configurations
*/
import { z } from 'zod';
import {
getProjectRootFromSession,
handleApiResult,
createErrorResponse
} from './utils.js';
import { modelsDirect } from '../core/task-master-core.js';
/**
* Register the models tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerModelsTool(server) {
server.addTool({
name: 'models',
description:
'Get information about available AI models or set model configurations. Run without arguments to get the current model configuration and API key status for the selected model providers.',
parameters: z.object({
setMain: z
.string()
.optional()
.describe(
'Set the primary model for task generation/updates. Model provider API key is required in the MCP config ENV.'
),
setResearch: z
.string()
.optional()
.describe(
'Set the model for research-backed operations. Model provider API key is required in the MCP config ENV.'
),
setFallback: z
.string()
.optional()
.describe(
'Set the model to use if the primary fails. Model provider API key is required in the MCP config ENV.'
),
listAvailableModels: z
.boolean()
.optional()
.describe('List all available models not currently in use.'),
projectRoot: z
.string()
.optional()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: async (args, { log, session }) => {
try {
log.info(`Starting models tool with args: ${JSON.stringify(args)}`);
// Get project root from args or session
const rootFolder =
args.projectRoot || getProjectRootFromSession(session, log);
// Ensure project root was determined
if (!rootFolder) {
return createErrorResponse(
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
);
}
// Call the direct function
const result = await modelsDirect(
{ ...args, projectRoot: rootFolder },
log,
{ session }
);
// Handle and return the result
return handleApiResult(result, log);
} catch (error) {
log.error(`Error in models tool: ${error.message}`);
return createErrorResponse(error.message);
}
}
});
}

View File

@@ -701,6 +701,15 @@ function createProjectStructure(
replacements
);
// Copy .taskmasterconfig with project name
copyTemplateFile(
'.taskmasterconfig',
path.join(targetDir, '.taskmasterconfig'),
{
...replacements
}
);
// Copy .gitignore
copyTemplateFile('gitignore', path.join(targetDir, '.gitignore'));

View File

@@ -11,6 +11,8 @@ import fs from 'fs';
import https from 'https';
import inquirer from 'inquirer';
import Table from 'cli-table3';
import { exec } from 'child_process';
import readline from 'readline';
import { log, readJSON } from './utils.js';
import {
@@ -70,6 +72,11 @@ import {
} from './ui.js';
import { initializeProject } from '../init.js';
import {
getModelConfiguration,
getAvailableModelsList,
setModel
} from './task-manager/models.js'; // Import new core functions
/**
* Configure and register CLI commands
@@ -1589,302 +1596,294 @@ function registerCommands(programInstance) {
)
.option('--setup', 'Run interactive setup to configure models')
.action(async (options) => {
let configModified = false; // Track if config needs saving
const availableModels = getAvailableModels(); // Get available models once
const currentConfig = getConfig(); // Load current config once
// Helper to find provider for a given model ID
const findModelData = (modelId) => {
return availableModels.find((m) => m.id === modelId);
};
try {
// --- Set Operations ---
if (options.setMain || options.setResearch || options.setFallback) {
let resultSet = null;
if (options.setMain) {
const modelId = options.setMain;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-main flag requires a valid model ID.')
);
process.exit(1);
}
const modelData = findModelData(modelId);
if (!modelData || !modelData.provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found or invalid in available models.`
)
);
process.exit(1);
}
// Update the loaded config object
currentConfig.models.main = {
...currentConfig.models.main, // Keep existing params like maxTokens
provider: modelData.provider,
modelId: modelId
};
console.log(
chalk.blue(
`Preparing to set main model to: ${modelId} (Provider: ${modelData.provider})`
)
);
configModified = true;
resultSet = await setModel('main', options.setMain);
} else if (options.setResearch) {
resultSet = await setModel('research', options.setResearch);
} else if (options.setFallback) {
resultSet = await setModel('fallback', options.setFallback);
}
if (options.setResearch) {
const modelId = options.setResearch;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-research flag requires a valid model ID.')
);
process.exit(1);
}
const modelData = findModelData(modelId);
if (!modelData || !modelData.provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found or invalid in available models.`
)
);
process.exit(1);
}
// Update the loaded config object
currentConfig.models.research = {
...currentConfig.models.research, // Keep existing params like maxTokens
provider: modelData.provider,
modelId: modelId
};
console.log(
chalk.blue(
`Preparing to set research model to: ${modelId} (Provider: ${modelData.provider})`
)
);
configModified = true;
}
if (options.setFallback) {
const modelId = options.setFallback;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-fallback flag requires a valid model ID.')
);
process.exit(1);
}
const modelData = findModelData(modelId);
if (!modelData || !modelData.provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found or invalid in available models.`
)
);
process.exit(1);
}
// Update the loaded config object
currentConfig.models.fallback = {
...currentConfig.models.fallback, // Keep existing params like maxTokens
provider: modelData.provider,
modelId: modelId
};
console.log(
chalk.blue(
`Preparing to set fallback model to: ${modelId} (Provider: ${modelData.provider})`
)
);
configModified = true;
}
// If any config was modified, write it back to the file
if (configModified) {
if (writeConfig(currentConfig)) {
console.log(
chalk.green(
'Configuration successfully updated in .taskmasterconfig'
)
);
if (resultSet?.success) {
console.log(chalk.green(resultSet.data.message));
} else {
console.error(
chalk.red(
'Error writing updated configuration to .taskmasterconfig'
`Error setting model: ${resultSet?.error?.message || 'Unknown error'}`
)
);
if (resultSet?.error?.code === 'MODEL_NOT_FOUND') {
console.log(
chalk.yellow(
'\nRun `task-master models` to see available models.'
)
);
}
process.exit(1);
}
return; // Exit after successful set operation
}
// Handle interactive setup first (Keep existing setup logic)
// --- Interactive Setup ---
if (options.setup) {
// Get available models for interactive setup
const availableModelsResult = await getAvailableModelsList();
if (!availableModelsResult.success) {
console.error(
chalk.red(
`Error fetching available models: ${availableModelsResult.error?.message || 'Unknown error'}`
)
);
process.exit(1);
}
const availableModelsForSetup = availableModelsResult.data.models;
const currentConfigResult = await getModelConfiguration();
if (!currentConfigResult.success) {
console.error(
chalk.red(
`Error fetching current configuration: ${currentConfigResult.error?.message || 'Unknown error'}`
)
);
// Allow setup even if current config fails (might be first time run)
}
const currentModels = currentConfigResult.data?.activeModels || {
main: {},
research: {},
fallback: {}
};
console.log(chalk.cyan.bold('\nInteractive Model Setup:'));
// Filter out placeholder models for selection
const selectableModels = availableModels
.filter(
(model) => !(model.id.startsWith('[') && model.id.endsWith(']'))
)
.map((model) => ({
name: `${model.provider} / ${model.id}`,
value: { provider: model.provider, id: model.id }
// Get all available models, including active ones
const allModelsForSetup = availableModelsForSetup.map((model) => ({
name: `${model.provider} / ${model.modelId}`,
value: { provider: model.provider, id: model.modelId } // Use id here for comparison
}));
if (selectableModels.length === 0) {
if (allModelsForSetup.length === 0) {
console.error(
chalk.red('Error: No selectable models found in configuration.')
);
process.exit(1);
}
// Function to find the index of the currently selected model ID
// Ensure it correctly searches the unfiltered selectableModels list
const findDefaultIndex = (roleModelId) => {
if (!roleModelId) return -1; // Handle cases where a role isn't set
return allModelsForSetup.findIndex(
(m) => m.value.id === roleModelId // Compare using the 'id' from the value object
);
};
// Helper to get research choices and default index
const getResearchChoicesAndDefault = () => {
const researchChoices = allModelsForSetup.filter((modelChoice) =>
availableModelsForSetup
.find((m) => m.modelId === modelChoice.value.id)
?.allowedRoles?.includes('research')
);
const defaultIndex = researchChoices.findIndex(
(m) => m.value.id === currentModels.research?.modelId
);
return { choices: researchChoices, default: defaultIndex };
};
// Helper to get fallback choices and default index
const getFallbackChoicesAndDefault = () => {
const choices = [
{ name: 'None (disable fallback)', value: null },
new inquirer.Separator(),
...allModelsForSetup
];
const currentFallbackId = currentModels.fallback?.modelId;
let defaultIndex = 0; // Default to 'None'
if (currentFallbackId) {
const foundIndex = allModelsForSetup.findIndex(
(m) => m.value.id === currentFallbackId
);
if (foundIndex !== -1) {
defaultIndex = foundIndex + 2; // +2 because of 'None' and Separator
}
}
return { choices, default: defaultIndex };
};
const researchPromptData = getResearchChoicesAndDefault();
const fallbackPromptData = getFallbackChoicesAndDefault();
// Add cancel option for all prompts
const cancelOption = {
name: 'Cancel setup (q)',
value: '__CANCEL__'
};
const mainModelChoices = [
cancelOption,
new inquirer.Separator(),
...allModelsForSetup
];
const researchModelChoices = [
cancelOption,
new inquirer.Separator(),
...researchPromptData.choices
];
const fallbackModelChoices = [
cancelOption,
new inquirer.Separator(),
...fallbackPromptData.choices
];
// Add key press handler for 'q' to cancel
process.stdin.on('keypress', (str, key) => {
if (key.name === 'q') {
process.stdin.pause();
console.log(chalk.yellow('\nSetup canceled. No changes made.'));
process.exit(0);
}
});
console.log(chalk.gray('Press "q" at any time to cancel the setup.'));
const answers = await inquirer.prompt([
{
type: 'list',
name: 'mainModel',
message: 'Select the main model for generation/updates:',
choices: selectableModels,
default: selectableModels.findIndex(
(m) => m.value.id === getMainModelId()
)
choices: mainModelChoices,
default: findDefaultIndex(currentModels.main?.modelId) + 2 // +2 for cancel option and separator
},
{
type: 'list',
name: 'researchModel',
message: 'Select the research model:',
// Filter choices to only include models allowed for research
choices: selectableModels.filter((modelChoice) => {
// Need to find the original model data to check allowed_roles
const originalModel = availableModels.find(
(m) => m.id === modelChoice.value.id
);
return originalModel?.allowed_roles?.includes('research');
}),
default: selectableModels.findIndex(
(m) => m.value.id === getResearchModelId()
)
choices: researchModelChoices,
default: researchPromptData.default + 2, // +2 for cancel option and separator
when: (answers) => answers.mainModel !== '__CANCEL__'
},
{
type: 'list',
name: 'fallbackModel',
message: 'Select the fallback model (optional):',
choices: [
{ name: 'None (disable fallback)', value: null },
new inquirer.Separator(),
...selectableModels
],
default:
selectableModels.findIndex(
(m) => m.value.id === getFallbackModelId()
) + 2 // Adjust for separator and None
choices: fallbackModelChoices,
default: fallbackPromptData.default + 2, // +2 for cancel option and separator
when: (answers) =>
answers.mainModel !== '__CANCEL__' &&
answers.researchModel !== '__CANCEL__'
}
]);
let setupSuccess = true;
let setupConfigModified = false; // Track if config was changed during setup
const configToUpdate = getConfig(); // Load the current config
// Clean up the keypress handler
process.stdin.removeAllListeners('keypress');
// Set Main Model
if (answers.mainModel) {
const modelData = findModelData(answers.mainModel.id); // Find full model data
if (modelData) {
configToUpdate.models.main = {
...configToUpdate.models.main, // Keep existing params
provider: modelData.provider,
modelId: modelData.id
};
console.log(
chalk.blue(
`Selected main model: ${modelData.provider} / ${modelData.id}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error finding model data for main selection: ${answers.mainModel.id}`
)
);
setupSuccess = false;
}
}
// Set Research Model
if (answers.researchModel) {
const modelData = findModelData(answers.researchModel.id); // Find full model data
if (modelData) {
configToUpdate.models.research = {
...configToUpdate.models.research, // Keep existing params
provider: modelData.provider,
modelId: modelData.id
};
console.log(
chalk.blue(
`Selected research model: ${modelData.provider} / ${modelData.id}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error finding model data for research selection: ${answers.researchModel.id}`
)
);
setupSuccess = false;
}
}
// Set Fallback Model
if (answers.fallbackModel) {
// User selected a specific fallback model
const modelData = findModelData(answers.fallbackModel.id); // Find full model data
if (modelData) {
configToUpdate.models.fallback = {
...configToUpdate.models.fallback, // Keep existing params
provider: modelData.provider,
modelId: modelData.id
};
console.log(
chalk.blue(
`Selected fallback model: ${modelData.provider} / ${modelData.id}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error finding model data for fallback selection: ${answers.fallbackModel.id}`
)
);
setupSuccess = false;
}
} else {
// User selected None - ensure fallback is disabled
// Check if user canceled at any point
if (
configToUpdate.models.fallback?.provider ||
configToUpdate.models.fallback?.modelId
answers.mainModel === '__CANCEL__' ||
answers.researchModel === '__CANCEL__' ||
answers.fallbackModel === '__CANCEL__'
) {
// Only mark as modified if something was actually cleared
configToUpdate.models.fallback = {
...configToUpdate.models.fallback, // Keep existing params like maxTokens
provider: undefined, // Or null
modelId: undefined // Or null
console.log(chalk.yellow('\nSetup canceled. No changes made.'));
return;
}
// Apply changes using setModel
let setupSuccess = true;
let setupConfigModified = false;
if (
answers.mainModel &&
answers.mainModel.id !== currentModels.main?.modelId
) {
const result = await setModel('main', answers.mainModel.id);
if (result.success) {
console.log(
chalk.blue(
`Selected main model: ${result.data.provider} / ${result.data.modelId}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error setting main model: ${result.error?.message || 'Unknown'}`
)
);
setupSuccess = false;
}
}
if (
answers.researchModel &&
answers.researchModel.id !== currentModels.research?.modelId
) {
const result = await setModel('research', answers.researchModel.id);
if (result.success) {
console.log(
chalk.blue(
`Selected research model: ${result.data.provider} / ${result.data.modelId}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error setting research model: ${result.error?.message || 'Unknown'}`
)
);
setupSuccess = false;
}
}
// Set Fallback Model - Handle 'None' selection
const currentFallbackId = currentModels.fallback?.modelId;
const selectedFallbackId = answers.fallbackModel?.id; // Will be null if 'None' selected
if (selectedFallbackId !== currentFallbackId) {
if (selectedFallbackId) {
// User selected a specific fallback model
const result = await setModel('fallback', selectedFallbackId);
if (result.success) {
console.log(
chalk.blue(
`Selected fallback model: ${result.data.provider} / ${result.data.modelId}`
)
);
setupConfigModified = true;
} else {
console.error(
chalk.red(
`Error setting fallback model: ${result.error?.message || 'Unknown'}`
)
);
setupSuccess = false;
}
} else if (currentFallbackId) {
// User selected 'None' but a fallback was previously set
// Need to explicitly clear it in the config file
const currentCfg = getConfig();
currentCfg.models.fallback = {
...currentCfg.models.fallback,
provider: undefined,
modelId: undefined
};
if (writeConfig(currentCfg)) {
console.log(chalk.blue('Fallback model disabled.'));
setupConfigModified = true;
}
}
// Save the updated configuration if changes were made and no errors occurred
if (setupConfigModified && setupSuccess) {
if (!writeConfig(configToUpdate)) {
} else {
console.error(
chalk.red(
'Failed to save updated model configuration to .taskmasterconfig.'
)
chalk.red('Failed to disable fallback model in config file.')
);
setupSuccess = false;
}
} else if (!setupSuccess) {
console.error(
chalk.red(
'Errors occurred during model selection. Configuration not saved.'
)
);
}
// No action needed if fallback was already null/undefined and user selected None
}
if (setupSuccess && setupConfigModified) {
@@ -1893,49 +1892,56 @@ function registerCommands(programInstance) {
console.log(
chalk.yellow('\nNo changes made to model configuration.')
);
}
return; // Exit after setup
}
// If no set flags were used and not in setup mode, list the models (Keep existing list logic)
if (!configModified && !options.setup) {
// Fetch current settings
const mainProvider = getMainProvider();
const mainModelId = getMainModelId();
const researchProvider = getResearchProvider();
const researchModelId = getResearchModelId();
const fallbackProvider = getFallbackProvider(); // May be undefined
const fallbackModelId = getFallbackModelId(); // May be undefined
// Check API keys for both CLI (.env) and MCP (mcp.json)
const mainCliKeyOk = isApiKeySet(mainProvider); // <-- Use correct function name
const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider);
const researchCliKeyOk = isApiKeySet(researchProvider); // <-- Use correct function name
const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider);
const fallbackCliKeyOk = fallbackProvider
? isApiKeySet(fallbackProvider) // <-- Use correct function name
: true; // No key needed if no fallback is set
const fallbackMcpKeyOk = fallbackProvider
? getMcpApiKeyStatus(fallbackProvider)
: true; // No key needed if no fallback is set
// --- Generate Warning Messages ---
const warnings = [];
if (!mainCliKeyOk || !mainMcpKeyOk) {
warnings.push(
`Main model (${mainProvider}): API key missing for ${!mainCliKeyOk ? 'CLI (.env)' : ''}${!mainCliKeyOk && !mainMcpKeyOk ? ' / ' : ''}${!mainMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
} else if (!setupSuccess) {
console.error(
chalk.red(
'\nErrors occurred during model selection. Please review and try again.'
)
);
}
if (!researchCliKeyOk || !researchMcpKeyOk) {
warnings.push(
`Research model (${researchProvider}): API key missing for ${!researchCliKeyOk ? 'CLI (.env)' : ''}${!researchCliKeyOk && !researchMcpKeyOk ? ' / ' : ''}${!researchMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
);
return; // Exit after setup attempt
}
if (fallbackProvider && (!fallbackCliKeyOk || !fallbackMcpKeyOk)) {
warnings.push(
`Fallback model (${fallbackProvider}): API key missing for ${!fallbackCliKeyOk ? 'CLI (.env)' : ''}${!fallbackCliKeyOk && !fallbackMcpKeyOk ? ' / ' : ''}${!fallbackMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
);
// --- Default: Display Current Configuration ---
// No longer need to check configModified here, as the set/setup logic returns early
// Fetch configuration using the core function
const result = await getModelConfiguration();
if (!result.success) {
// Handle specific CONFIG_MISSING error gracefully
if (result.error?.code === 'CONFIG_MISSING') {
console.error(
boxen(
chalk.red.bold('Configuration File Missing!') +
'\n\n' +
chalk.white(
'The .taskmasterconfig file was not found in your project root.\n\n' +
'Run the interactive setup to create and configure it:'
) +
'\n' +
chalk.green(' task-master models --setup'),
{
padding: 1,
margin: { top: 1 },
borderColor: 'red',
borderStyle: 'round'
}
)
);
process.exit(0); // Exit gracefully, user needs to run setup
} else {
console.error(
chalk.red(
`Error fetching model configuration: ${result.error?.message || 'Unknown error'}`
)
);
process.exit(1);
}
}
const configData = result.data;
const active = configData.activeModels;
const warnings = configData.warnings || []; // Warnings now come from core function
// --- Display Warning Banner (if any) ---
if (warnings.length > 0) {
@@ -1961,172 +1967,140 @@ function registerCommands(programInstance) {
'Role',
'Provider',
'Model ID',
'SWE Score', // Update column name
'Cost ($/1M tkns)', // Add Cost column
'SWE Score',
'Cost ($/1M tkns)',
'API Key Status'
].map((h) => chalk.cyan.bold(h)),
colWidths: [10, 14, 30, 18, 20, 28], // Adjust widths for stars
colWidths: [10, 14, 30, 18, 20, 28],
style: { head: ['cyan', 'bold'] }
});
const allAvailableModels = getAvailableModels(); // Get all models once for lookup
// --- Helper functions for formatting (can be moved to ui.js if complex) ---
const formatSweScoreWithTertileStars = (score, allModels) => {
if (score === null || score === undefined || score <= 0) return 'N/A';
const formattedPercentage = `${(score * 100).toFixed(1)}%`;
// --- Calculate Tertile Thresholds for SWE Scores ---
const validScores = allAvailableModels
.map((m) => m.swe_score)
const validScores = allModels
.map((m) => m.sweScore)
.filter((s) => s !== null && s !== undefined && s > 0);
const sortedScores = [...validScores].sort((a, b) => b - a); // Sort descending
const sortedScores = [...validScores].sort((a, b) => b - a);
const n = sortedScores.length;
let minScore3Stars = -Infinity;
let minScore2Stars = -Infinity;
let stars = chalk.gray('☆☆☆');
if (n > 0) {
const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1);
const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1);
minScore3Stars = sortedScores[topThirdIndex];
minScore2Stars = sortedScores[midThirdIndex];
if (score >= sortedScores[topThirdIndex])
stars = chalk.yellow('★★★');
else if (score >= sortedScores[midThirdIndex])
stars = chalk.yellow('★★') + chalk.gray('☆');
else stars = chalk.yellow('★') + chalk.gray('☆☆');
}
// Helper to find the full model object
const findModelData = (modelId) => {
return allAvailableModels.find((m) => m.id === modelId);
};
// --- Helper to format SWE score and add tertile stars ---
const formatSweScoreWithTertileStars = (score) => {
if (score === null || score === undefined || score <= 0)
return 'N/A'; // Handle non-positive scores
const formattedPercentage = `${(score * 100).toFixed(1)}%`;
let stars = '';
if (n === 0) {
// No valid scores to compare against
stars = chalk.gray('☆☆☆');
} else if (score >= minScore3Stars) {
stars = chalk.yellow('★★★'); // Top Third
} else if (score >= minScore2Stars) {
stars = chalk.yellow('★★') + chalk.gray('☆'); // Middle Third
} else {
stars = chalk.yellow('★') + chalk.gray('☆☆'); // Bottom Third (but > 0)
}
return `${formattedPercentage} ${stars}`;
};
// Helper to format cost
const formatCost = (costObj) => {
if (!costObj) return 'N/A';
const formatSingleCost = (costValue) => {
if (costValue === null || costValue === undefined) return 'N/A';
// Check if the number is an integer
const isInteger = Number.isInteger(costValue);
return `$${costValue.toFixed(isInteger ? 0 : 2)}`;
};
const inputCost = formatSingleCost(costObj.input);
const outputCost = formatSingleCost(costObj.output);
return `${inputCost} in, ${outputCost} out`; // Use cleaner separator
return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(
costObj.output
)} out`;
};
const getCombinedStatus = (cliOk, mcpOk) => {
const getCombinedStatus = (keyStatus) => {
const cliOk = keyStatus?.cli;
const mcpOk = keyStatus?.mcp;
const cliSymbol = cliOk ? chalk.green('✓') : chalk.red('✗');
const mcpSymbol = mcpOk ? chalk.green('✓') : chalk.red('✗');
if (cliOk && mcpOk) {
// Both symbols green, default text color
return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
} else if (cliOk && !mcpOk) {
// Symbols colored individually, default text color
if (cliOk && mcpOk) return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
if (cliOk && !mcpOk)
return `${cliSymbol} CLI OK / ${mcpSymbol} MCP Missing`;
} else if (!cliOk && mcpOk) {
// Symbols colored individually, default text color
if (!cliOk && mcpOk)
return `${cliSymbol} CLI Missing / ${mcpSymbol} MCP OK`;
} else {
// Both symbols gray, apply overall gray to text as well
return chalk.gray(`${cliSymbol} CLI & MCP Both Missing`);
}
};
const mainModelData = findModelData(mainModelId);
const researchModelData = findModelData(researchModelId);
const fallbackModelData = findModelData(fallbackModelId);
// Get all available models data once for SWE Score calculation
const availableModelsResultForScore = await getAvailableModelsList();
const allAvailModelsForScore =
availableModelsResultForScore.data?.models || [];
// Populate Active Table
activeTable.push([
chalk.white('Main'),
mainProvider,
mainModelId,
formatSweScoreWithTertileStars(mainModelData?.swe_score), // Use tertile formatter
formatCost(mainModelData?.cost_per_1m_tokens),
getCombinedStatus(mainCliKeyOk, mainMcpKeyOk)
active.main.provider,
active.main.modelId,
formatSweScoreWithTertileStars(
active.main.sweScore,
allAvailModelsForScore
),
formatCost(active.main.cost),
getCombinedStatus(active.main.keyStatus)
]);
activeTable.push([
chalk.white('Research'),
researchProvider,
researchModelId,
formatSweScoreWithTertileStars(researchModelData?.swe_score), // Use tertile formatter
formatCost(researchModelData?.cost_per_1m_tokens),
getCombinedStatus(researchCliKeyOk, researchMcpKeyOk)
active.research.provider,
active.research.modelId,
formatSweScoreWithTertileStars(
active.research.sweScore,
allAvailModelsForScore
),
formatCost(active.research.cost),
getCombinedStatus(active.research.keyStatus)
]);
if (fallbackProvider && fallbackModelId) {
if (active.fallback) {
activeTable.push([
chalk.white('Fallback'),
fallbackProvider,
fallbackModelId,
formatSweScoreWithTertileStars(fallbackModelData?.swe_score), // Use tertile formatter
formatCost(fallbackModelData?.cost_per_1m_tokens),
getCombinedStatus(fallbackCliKeyOk, fallbackMcpKeyOk)
active.fallback.provider,
active.fallback.modelId,
formatSweScoreWithTertileStars(
active.fallback.sweScore,
allAvailModelsForScore
),
formatCost(active.fallback.cost),
getCombinedStatus(active.fallback.keyStatus)
]);
}
console.log(activeTable.toString());
// --- Available Models Section ---
// const availableModels = getAvailableModels(); // Already fetched
if (!allAvailableModels || allAvailableModels.length === 0) {
console.log(chalk.yellow('\nNo available models defined.'));
return;
}
// Filter out placeholders and active models for the available list
const activeIds = [
mainModelId,
researchModelId,
fallbackModelId
].filter(Boolean);
const filteredAvailable = allAvailableModels.filter(
(model) =>
!(model.id.startsWith('[') && model.id.endsWith(']')) &&
!activeIds.includes(model.id)
);
if (filteredAvailable.length > 0) {
const availableResult = await getAvailableModelsList();
if (availableResult.success && availableResult.data.models.length > 0) {
console.log(chalk.cyan.bold('\nOther Available Models:'));
const availableTable = new Table({
head: [
'Provider',
'Model ID',
'SWE Score', // Update column name
'Cost ($/1M tkns)' // Add Cost column
].map((h) => chalk.cyan.bold(h)),
colWidths: [15, 40, 18, 25], // Adjust widths for stars
head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map(
(h) => chalk.cyan.bold(h)
),
colWidths: [15, 40, 18, 25],
style: { head: ['cyan', 'bold'] }
});
filteredAvailable.forEach((model) => {
availableResult.data.models.forEach((model) => {
availableTable.push([
model.provider || 'N/A',
model.id,
formatSweScoreWithTertileStars(model.swe_score), // Use tertile formatter
formatCost(model.cost_per_1m_tokens)
model.provider,
model.modelId,
formatSweScoreWithTertileStars(
model.sweScore,
allAvailModelsForScore
),
formatCost(model.cost)
]);
});
console.log(availableTable.toString());
} else {
} else if (availableResult.success) {
console.log(
chalk.gray('\n(All available models are currently configured)')
);
} else {
console.warn(
chalk.yellow(
`Could not fetch available models list: ${availableResult.error?.message}`
)
);
}
// --- Suggested Actions Section ---
@@ -2157,11 +2131,21 @@ function registerCommands(programInstance) {
}
)
);
}
} catch (error) {
log(`Error processing models command: ${error.message}`, 'error');
if (error.stack && getDebugFlag()) {
log(error.stack, 'debug');
// Catch errors specifically from the core model functions
console.error(
chalk.red(`Error processing models command: ${error.message}`)
);
if (error instanceof ConfigurationError) {
// Provide specific guidance if it's a config error
console.error(
chalk.yellow(
'This might be a configuration file issue. Try running `task-master models --setup`.'
)
);
}
if (getDebugFlag()) {
console.error(error.stack);
}
process.exit(1);
}

View File

@@ -0,0 +1,386 @@
/**
* models.js
* Core functionality for managing AI model configurations
*/
import path from 'path';
import fs from 'fs';
import {
getMainModelId,
getResearchModelId,
getFallbackModelId,
getAvailableModels,
VALID_PROVIDERS,
getMainProvider,
getResearchProvider,
getFallbackProvider,
isApiKeySet,
getMcpApiKeyStatus,
getConfig,
writeConfig,
isConfigFilePresent
} from '../config-manager.js';
/**
* Get the current model configuration
* @param {Object} [options] - Options for the operation
* @param {Object} [options.session] - Session object containing environment variables (for MCP)
* @param {Function} [options.mcpLog] - MCP logger object (for MCP)
* @param {string} [options.projectRoot] - Project root directory
* @returns {Object} RESTful response with current model configuration
*/
async function getModelConfiguration(options = {}) {
const { mcpLog, projectRoot } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
}
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
}
try {
// Get current settings - these should use the config from the found path automatically
const mainProvider = getMainProvider(projectRoot);
const mainModelId = getMainModelId(projectRoot);
const researchProvider = getResearchProvider(projectRoot);
const researchModelId = getResearchModelId(projectRoot);
const fallbackProvider = getFallbackProvider(projectRoot);
const fallbackModelId = getFallbackModelId(projectRoot);
// Check API keys
const mainCliKeyOk = isApiKeySet(mainProvider);
const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider);
const researchCliKeyOk = isApiKeySet(researchProvider);
const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider);
const fallbackCliKeyOk = fallbackProvider
? isApiKeySet(fallbackProvider)
: true;
const fallbackMcpKeyOk = fallbackProvider
? getMcpApiKeyStatus(fallbackProvider)
: true;
// Get available models to find detailed info
const availableModels = getAvailableModels(projectRoot);
// Find model details
const mainModelData = availableModels.find((m) => m.id === mainModelId);
const researchModelData = availableModels.find(
(m) => m.id === researchModelId
);
const fallbackModelData = fallbackModelId
? availableModels.find((m) => m.id === fallbackModelId)
: null;
// Return structured configuration data
return {
success: true,
data: {
activeModels: {
main: {
provider: mainProvider,
modelId: mainModelId,
sweScore: mainModelData?.swe_score || null,
cost: mainModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: mainCliKeyOk,
mcp: mainMcpKeyOk
}
},
research: {
provider: researchProvider,
modelId: researchModelId,
sweScore: researchModelData?.swe_score || null,
cost: researchModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: researchCliKeyOk,
mcp: researchMcpKeyOk
}
},
fallback: fallbackProvider
? {
provider: fallbackProvider,
modelId: fallbackModelId,
sweScore: fallbackModelData?.swe_score || null,
cost: fallbackModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: fallbackCliKeyOk,
mcp: fallbackMcpKeyOk
}
}
: null
},
message: 'Successfully retrieved current model configuration'
}
};
} catch (error) {
report('error', `Error getting model configuration: ${error.message}`);
return {
success: false,
error: {
code: 'CONFIG_ERROR',
message: error.message
}
};
}
}
/**
* Get all available models not currently in use
* @param {Object} [options] - Options for the operation
* @param {Object} [options.session] - Session object containing environment variables (for MCP)
* @param {Function} [options.mcpLog] - MCP logger object (for MCP)
* @param {string} [options.projectRoot] - Project root directory
* @returns {Object} RESTful response with available models
*/
async function getAvailableModelsList(options = {}) {
const { mcpLog, projectRoot } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
}
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
}
try {
// Get all available models
const allAvailableModels = getAvailableModels(projectRoot);
if (!allAvailableModels || allAvailableModels.length === 0) {
return {
success: true,
data: {
models: [],
message: 'No available models found'
}
};
}
// Get currently used model IDs
const mainModelId = getMainModelId(projectRoot);
const researchModelId = getResearchModelId(projectRoot);
const fallbackModelId = getFallbackModelId(projectRoot);
// Filter out placeholder models and active models
const activeIds = [mainModelId, researchModelId, fallbackModelId].filter(
Boolean
);
const otherAvailableModels = allAvailableModels.map((model) => ({
provider: model.provider || 'N/A',
modelId: model.id,
sweScore: model.swe_score || null,
cost: model.cost_per_1m_tokens || null,
allowedRoles: model.allowed_roles || []
}));
return {
success: true,
data: {
models: otherAvailableModels,
message: `Successfully retrieved ${otherAvailableModels.length} available models`
}
};
} catch (error) {
report('error', `Error getting available models: ${error.message}`);
return {
success: false,
error: {
code: 'MODELS_LIST_ERROR',
message: error.message
}
};
}
}
/**
* Update a specific model in the configuration
* @param {string} role - The model role to update ('main', 'research', 'fallback')
* @param {string} modelId - The model ID to set for the role
* @param {Object} [options] - Options for the operation
* @param {Object} [options.session] - Session object containing environment variables (for MCP)
* @param {Function} [options.mcpLog] - MCP logger object (for MCP)
* @param {string} [options.projectRoot] - Project root directory
* @returns {Object} RESTful response with result of update operation
*/
async function setModel(role, modelId, options = {}) {
const { mcpLog, projectRoot } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
}
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
}
// Validate role
if (!['main', 'research', 'fallback'].includes(role)) {
return {
success: false,
error: {
code: 'INVALID_ROLE',
message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
}
};
}
// Validate model ID
if (typeof modelId !== 'string' || modelId.trim() === '') {
return {
success: false,
error: {
code: 'INVALID_MODEL_ID',
message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
}
};
}
try {
const availableModels = getAvailableModels(projectRoot);
const currentConfig = getConfig(projectRoot);
// Find the model data
const modelData = availableModels.find((m) => m.id === modelId);
if (!modelData || !modelData.provider) {
return {
success: false,
error: {
code: 'MODEL_NOT_FOUND',
message: `Model ID "${modelId}" not found or invalid in available models.`
}
};
}
// Update configuration
currentConfig.models[role] = {
...currentConfig.models[role], // Keep existing params like maxTokens
provider: modelData.provider,
modelId: modelId
};
// Write updated configuration
const writeResult = writeConfig(currentConfig, projectRoot);
if (!writeResult) {
return {
success: false,
error: {
code: 'WRITE_ERROR',
message: 'Error writing updated configuration to .taskmasterconfig'
}
};
}
report(
'info',
`Set ${role} model to: ${modelId} (Provider: ${modelData.provider})`
);
return {
success: true,
data: {
role,
provider: modelData.provider,
modelId,
message: `Successfully set ${role} model to ${modelId} (Provider: ${modelData.provider})`
}
};
} catch (error) {
report('error', `Error setting ${role} model: ${error.message}`);
return {
success: false,
error: {
code: 'SET_MODEL_ERROR',
message: error.message
}
};
}
}
export { getModelConfiguration, getAvailableModelsList, setModel };

View File

@@ -21,13 +21,8 @@ import fs from 'fs';
import { findNextTask, analyzeTaskComplexity } from './task-manager.js';
import {
getProjectName,
getDefaultSubtasks,
getMainModelId,
getMainMaxTokens,
getMainTemperature,
getDebugFlag,
getLogLevel,
getDefaultPriority
getDefaultSubtasks
} from './config-manager.js';
// Create a color gradient for the banner
@@ -386,6 +381,9 @@ function formatDependenciesWithStatus(
function displayHelp() {
displayBanner();
// Get terminal width - moved to top of function to make it available throughout
const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect
console.log(
boxen(chalk.white.bold('Task Master CLI'), {
padding: 1,
@@ -397,6 +395,42 @@ function displayHelp() {
// Command categories
const commandCategories = [
{
title: 'Project Setup & Configuration',
color: 'blue',
commands: [
{
name: 'init',
args: '[--name=<name>] [--description=<desc>] [-y]',
desc: 'Initialize a new project with Task Master structure'
},
{
name: 'models',
args: '',
desc: 'View current AI model configuration and available models'
},
{
name: 'models --setup',
args: '',
desc: 'Run interactive setup to configure AI models'
},
{
name: 'models --set-main',
args: '<model_id>',
desc: 'Set the primary model for task generation'
},
{
name: 'models --set-research',
args: '<model_id>',
desc: 'Set the model for research operations'
},
{
name: 'models --set-fallback',
args: '<model_id>',
desc: 'Set the fallback model (optional)'
}
]
},
{
title: 'Task Generation',
color: 'cyan',
@@ -430,7 +464,17 @@ function displayHelp() {
{
name: 'update',
args: '--from=<id> --prompt="<context>"',
desc: 'Update tasks based on new requirements'
desc: 'Update multiple tasks based on new requirements'
},
{
name: 'update-task',
args: '--id=<id> --prompt="<context>"',
desc: 'Update a single specific task with new information'
},
{
name: 'update-subtask',
args: '--id=<parentId.subtaskId> --prompt="<context>"',
desc: 'Append additional information to a subtask'
},
{
name: 'add-task',
@@ -438,20 +482,46 @@ function displayHelp() {
desc: 'Add a new task using AI'
},
{
name: 'add-dependency',
args: '--id=<id> --depends-on=<id>',
desc: 'Add a dependency to a task'
},
{
name: 'remove-dependency',
args: '--id=<id> --depends-on=<id>',
desc: 'Remove a dependency from a task'
name: 'remove-task',
args: '--id=<id> [-y]',
desc: 'Permanently remove a task or subtask'
}
]
},
{
title: 'Task Analysis & Detail',
title: 'Subtask Management',
color: 'yellow',
commands: [
{
name: 'add-subtask',
args: '--parent=<id> --title="<title>" [--description="<desc>"]',
desc: 'Add a new subtask to a parent task'
},
{
name: 'add-subtask',
args: '--parent=<id> --task-id=<id>',
desc: 'Convert an existing task into a subtask'
},
{
name: 'remove-subtask',
args: '--id=<parentId.subtaskId> [--convert]',
desc: 'Remove a subtask (optionally convert to standalone task)'
},
{
name: 'clear-subtasks',
args: '--id=<id>',
desc: 'Remove all subtasks from specified tasks'
},
{
name: 'clear-subtasks --all',
args: '',
desc: 'Remove subtasks from all tasks'
}
]
},
{
title: 'Task Analysis & Breakdown',
color: 'magenta',
commands: [
{
name: 'analyze-complexity',
@@ -472,17 +542,12 @@ function displayHelp() {
name: 'expand --all',
args: '[--force] [--research]',
desc: 'Expand all pending tasks with subtasks'
},
{
name: 'clear-subtasks',
args: '--id=<id>',
desc: 'Remove subtasks from specified tasks'
}
]
},
{
title: 'Task Navigation & Viewing',
color: 'magenta',
color: 'cyan',
commands: [
{
name: 'next',
@@ -500,6 +565,16 @@ function displayHelp() {
title: 'Dependency Management',
color: 'blue',
commands: [
{
name: 'add-dependency',
args: '--id=<id> --depends-on=<id>',
desc: 'Add a dependency to a task'
},
{
name: 'remove-dependency',
args: '--id=<id> --depends-on=<id>',
desc: 'Remove a dependency from a task'
},
{
name: 'validate-dependencies',
args: '',
@@ -525,8 +600,13 @@ function displayHelp() {
})
);
// Calculate dynamic column widths - adjust ratios as needed
const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25
const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40
const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer
const commandTable = new Table({
colWidths: [25, 40, 45],
colWidths: [nameWidth, argsWidth, descWidth],
chars: {
top: '',
'top-mid': '',
@@ -544,7 +624,8 @@ function displayHelp() {
'right-mid': '',
middle: ' '
},
style: { border: [], 'padding-left': 4 }
style: { border: [], 'padding-left': 4 },
wordWrap: true
});
category.commands.forEach((cmd, index) => {
@@ -559,9 +640,9 @@ function displayHelp() {
console.log('');
});
// Display environment variables section
// Display configuration section
console.log(
boxen(chalk.cyan.bold('Environment Variables'), {
boxen(chalk.cyan.bold('Configuration'), {
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: 'cyan',
@@ -569,8 +650,19 @@ function displayHelp() {
})
);
const envTable = new Table({
colWidths: [30, 50, 30],
// Get terminal width if not already defined
const configTerminalWidth = terminalWidth || process.stdout.columns || 100;
// Calculate dynamic column widths for config table
const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25));
const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45));
const configValueWidth = Math.max(
30,
Math.floor(configTerminalWidth * 0.3) - 10
);
const configTable = new Table({
colWidths: [configKeyWidth, configDescWidth, configValueWidth],
chars: {
top: '',
'top-mid': '',
@@ -588,69 +680,59 @@ function displayHelp() {
'right-mid': '',
middle: ' '
},
style: { border: [], 'padding-left': 4 }
style: { border: [], 'padding-left': 4 },
wordWrap: true
});
envTable.push(
configTable.push(
[
`${chalk.yellow('ANTHROPIC_API_KEY')}${chalk.reset('')}`,
`${chalk.white('Your Anthropic API key')}${chalk.reset('')}`,
`${chalk.dim('Required')}${chalk.reset('')}`
`${chalk.yellow('.taskmasterconfig')}${chalk.reset('')}`,
`${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`,
`${chalk.dim('Managed by models cmd')}${chalk.reset('')}`
],
[
`${chalk.yellow('MODEL')}${chalk.reset('')}`,
`${chalk.white('Claude model to use')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getMainModelId()}`)}${chalk.reset('')}`
`${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`,
`${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`,
`${chalk.dim('Required in .env file')}${chalk.reset('')}`
],
[
`${chalk.yellow('MAX_TOKENS')}${chalk.reset('')}`,
`${chalk.white('Maximum tokens for responses')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getMainMaxTokens()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('TEMPERATURE')}${chalk.reset('')}`,
`${chalk.white('Temperature for model responses')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getMainTemperature()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('PERPLEXITY_API_KEY')}${chalk.reset('')}`,
`${chalk.white('Perplexity API key for research')}${chalk.reset('')}`,
`${chalk.dim('Optional')}${chalk.reset('')}`
],
[
`${chalk.yellow('PERPLEXITY_MODEL')}${chalk.reset('')}`,
`${chalk.white('Perplexity model to use')}${chalk.reset('')}`,
`${chalk.dim('Default: sonar-pro')}${chalk.reset('')}`
],
[
`${chalk.yellow('DEBUG')}${chalk.reset('')}`,
`${chalk.white('Enable debug logging')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getDebugFlag()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('LOG_LEVEL')}${chalk.reset('')}`,
`${chalk.white('Console output level (debug,info,warn,error)')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getLogLevel()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('DEFAULT_SUBTASKS')}${chalk.reset('')}`,
`${chalk.white('Default number of subtasks to generate')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getDefaultSubtasks()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('DEFAULT_PRIORITY')}${chalk.reset('')}`,
`${chalk.white('Default task priority')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getDefaultPriority()}`)}${chalk.reset('')}`
],
[
`${chalk.yellow('PROJECT_NAME')}${chalk.reset('')}`,
`${chalk.white('Project name displayed in UI')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getProjectName()}`)}${chalk.reset('')}`
`${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`,
`${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`,
`${chalk.dim('Required in .cursor/')}${chalk.reset('')}`
]
);
console.log(envTable.toString());
console.log(configTable.toString());
console.log('');
// Show helpful hints
console.log(
boxen(
chalk.white.bold('Quick Start:') +
'\n\n' +
chalk.cyan('1. Create Project: ') +
chalk.white('task-master init') +
'\n' +
chalk.cyan('2. Setup Models: ') +
chalk.white('task-master models --setup') +
'\n' +
chalk.cyan('3. Parse PRD: ') +
chalk.white('task-master parse-prd --input=<prd-file>') +
'\n' +
chalk.cyan('4. List Tasks: ') +
chalk.white('task-master list') +
'\n' +
chalk.cyan('5. Find Next Task: ') +
chalk.white('task-master next'),
{
padding: 1,
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 },
width: Math.min(configTerminalWidth - 10, 100) // Limit width to terminal width minus padding, max 100
}
)
);
}
/**