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 3881912453
commit e4958c5e26
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. * 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. * 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. * **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. * 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`):** 2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs. * 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`. * 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). * 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 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. **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. `rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts. - 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.* *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 ## Task Listing & Viewing
### 3. Get Tasks (`get_tasks`) ### 3. Get Tasks (`get_tasks`)

View File

@@ -16,7 +16,7 @@
"provider": "anthropic", "provider": "anthropic",
"modelId": "claude-3.5-sonnet-20240620", "modelId": "claude-3.5-sonnet-20240620",
"maxTokens": 120000, "maxTokens": 120000,
"temperature": 0.1 "temperature": 0.2
} }
}, },
"global": { "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 { addDependencyDirect } from './direct-functions/add-dependency.js';
import { removeTaskDirect } from './direct-functions/remove-task.js'; import { removeTaskDirect } from './direct-functions/remove-task.js';
import { initializeProjectDirect } from './direct-functions/initialize-project-direct.js'; import { initializeProjectDirect } from './direct-functions/initialize-project-direct.js';
import { modelsDirect } from './direct-functions/models.js';
// Re-export utility functions // Re-export utility functions
export { findTasksJsonPath } from './utils/path-utils.js'; export { findTasksJsonPath } from './utils/path-utils.js';
@@ -66,7 +67,9 @@ export const directFunctions = new Map([
['fixDependenciesDirect', fixDependenciesDirect], ['fixDependenciesDirect', fixDependenciesDirect],
['complexityReportDirect', complexityReportDirect], ['complexityReportDirect', complexityReportDirect],
['addDependencyDirect', addDependencyDirect], ['addDependencyDirect', addDependencyDirect],
['removeTaskDirect', removeTaskDirect] ['removeTaskDirect', removeTaskDirect],
['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect]
]); ]);
// Re-export all direct function implementations // Re-export all direct function implementations
@@ -94,5 +97,6 @@ export {
complexityReportDirect, complexityReportDirect,
addDependencyDirect, addDependencyDirect,
removeTaskDirect, removeTaskDirect,
initializeProjectDirect initializeProjectDirect,
modelsDirect
}; };

View File

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

View File

@@ -11,6 +11,8 @@ import fs from 'fs';
import https from 'https'; import https from 'https';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import Table from 'cli-table3'; import Table from 'cli-table3';
import { exec } from 'child_process';
import readline from 'readline';
import { log, readJSON } from './utils.js'; import { log, readJSON } from './utils.js';
import { import {
@@ -70,6 +72,11 @@ import {
} from './ui.js'; } from './ui.js';
import { initializeProject } from '../init.js'; import { initializeProject } from '../init.js';
import {
getModelConfiguration,
getAvailableModelsList,
setModel
} from './task-manager/models.js'; // Import new core functions
/** /**
* Configure and register CLI commands * Configure and register CLI commands
@@ -1589,302 +1596,294 @@ function registerCommands(programInstance) {
) )
.option('--setup', 'Run interactive setup to configure models') .option('--setup', 'Run interactive setup to configure models')
.action(async (options) => { .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 { try {
// --- Set Operations ---
if (options.setMain || options.setResearch || options.setFallback) {
let resultSet = null;
if (options.setMain) { if (options.setMain) {
const modelId = options.setMain; resultSet = await setModel('main', options.setMain);
if (typeof modelId !== 'string' || modelId.trim() === '') { } else if (options.setResearch) {
console.error( resultSet = await setModel('research', options.setResearch);
chalk.red('Error: --set-main flag requires a valid model ID.') } else if (options.setFallback) {
); resultSet = await setModel('fallback', options.setFallback);
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;
} }
if (options.setResearch) { if (resultSet?.success) {
const modelId = options.setResearch; console.log(chalk.green(resultSet.data.message));
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'
)
);
} else { } else {
console.error( console.error(
chalk.red( 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); process.exit(1);
} }
return; // Exit after successful set operation return; // Exit after successful set operation
} }
// Handle interactive setup first (Keep existing setup logic) // --- Interactive Setup ---
if (options.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:')); console.log(chalk.cyan.bold('\nInteractive Model Setup:'));
// Filter out placeholder models for selection // Get all available models, including active ones
const selectableModels = availableModels const allModelsForSetup = availableModelsForSetup.map((model) => ({
.filter( name: `${model.provider} / ${model.modelId}`,
(model) => !(model.id.startsWith('[') && model.id.endsWith(']')) value: { provider: model.provider, id: model.modelId } // Use id here for comparison
)
.map((model) => ({
name: `${model.provider} / ${model.id}`,
value: { provider: model.provider, id: model.id }
})); }));
if (selectableModels.length === 0) { if (allModelsForSetup.length === 0) {
console.error( console.error(
chalk.red('Error: No selectable models found in configuration.') chalk.red('Error: No selectable models found in configuration.')
); );
process.exit(1); 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([ const answers = await inquirer.prompt([
{ {
type: 'list', type: 'list',
name: 'mainModel', name: 'mainModel',
message: 'Select the main model for generation/updates:', message: 'Select the main model for generation/updates:',
choices: selectableModels, choices: mainModelChoices,
default: selectableModels.findIndex( default: findDefaultIndex(currentModels.main?.modelId) + 2 // +2 for cancel option and separator
(m) => m.value.id === getMainModelId()
)
}, },
{ {
type: 'list', type: 'list',
name: 'researchModel', name: 'researchModel',
message: 'Select the research model:', message: 'Select the research model:',
// Filter choices to only include models allowed for research choices: researchModelChoices,
choices: selectableModels.filter((modelChoice) => { default: researchPromptData.default + 2, // +2 for cancel option and separator
// Need to find the original model data to check allowed_roles when: (answers) => answers.mainModel !== '__CANCEL__'
const originalModel = availableModels.find(
(m) => m.id === modelChoice.value.id
);
return originalModel?.allowed_roles?.includes('research');
}),
default: selectableModels.findIndex(
(m) => m.value.id === getResearchModelId()
)
}, },
{ {
type: 'list', type: 'list',
name: 'fallbackModel', name: 'fallbackModel',
message: 'Select the fallback model (optional):', message: 'Select the fallback model (optional):',
choices: [ choices: fallbackModelChoices,
{ name: 'None (disable fallback)', value: null }, default: fallbackPromptData.default + 2, // +2 for cancel option and separator
new inquirer.Separator(), when: (answers) =>
...selectableModels answers.mainModel !== '__CANCEL__' &&
], answers.researchModel !== '__CANCEL__'
default:
selectableModels.findIndex(
(m) => m.value.id === getFallbackModelId()
) + 2 // Adjust for separator and None
} }
]); ]);
let setupSuccess = true; // Clean up the keypress handler
let setupConfigModified = false; // Track if config was changed during setup process.stdin.removeAllListeners('keypress');
const configToUpdate = getConfig(); // Load the current config
// Set Main Model // Check if user canceled at any point
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
if ( if (
configToUpdate.models.fallback?.provider || answers.mainModel === '__CANCEL__' ||
configToUpdate.models.fallback?.modelId answers.researchModel === '__CANCEL__' ||
answers.fallbackModel === '__CANCEL__'
) { ) {
// Only mark as modified if something was actually cleared console.log(chalk.yellow('\nSetup canceled. No changes made.'));
configToUpdate.models.fallback = { return;
...configToUpdate.models.fallback, // Keep existing params like maxTokens }
provider: undefined, // Or null
modelId: undefined // Or null // 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.')); console.log(chalk.blue('Fallback model disabled.'));
setupConfigModified = true; setupConfigModified = true;
} } else {
}
// Save the updated configuration if changes were made and no errors occurred
if (setupConfigModified && setupSuccess) {
if (!writeConfig(configToUpdate)) {
console.error( console.error(
chalk.red( chalk.red('Failed to disable fallback model in config file.')
'Failed to save updated model configuration to .taskmasterconfig.'
)
); );
setupSuccess = false; setupSuccess = false;
} }
} else if (!setupSuccess) { }
console.error( // No action needed if fallback was already null/undefined and user selected None
chalk.red(
'Errors occurred during model selection. Configuration not saved.'
)
);
} }
if (setupSuccess && setupConfigModified) { if (setupSuccess && setupConfigModified) {
@@ -1893,49 +1892,56 @@ function registerCommands(programInstance) {
console.log( console.log(
chalk.yellow('\nNo changes made to model configuration.') chalk.yellow('\nNo changes made to model configuration.')
); );
} } else if (!setupSuccess) {
return; // Exit after setup console.error(
} chalk.red(
'\nErrors occurred during model selection. Please review and try again.'
// 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)' : ''}`
); );
} }
if (!researchCliKeyOk || !researchMcpKeyOk) { return; // Exit after setup attempt
warnings.push(
`Research model (${researchProvider}): API key missing for ${!researchCliKeyOk ? 'CLI (.env)' : ''}${!researchCliKeyOk && !researchMcpKeyOk ? ' / ' : ''}${!researchMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
);
} }
if (fallbackProvider && (!fallbackCliKeyOk || !fallbackMcpKeyOk)) {
warnings.push( // --- Default: Display Current Configuration ---
`Fallback model (${fallbackProvider}): API key missing for ${!fallbackCliKeyOk ? 'CLI (.env)' : ''}${!fallbackCliKeyOk && !fallbackMcpKeyOk ? ' / ' : ''}${!fallbackMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}` // 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) --- // --- Display Warning Banner (if any) ---
if (warnings.length > 0) { if (warnings.length > 0) {
@@ -1961,172 +1967,140 @@ function registerCommands(programInstance) {
'Role', 'Role',
'Provider', 'Provider',
'Model ID', 'Model ID',
'SWE Score', // Update column name 'SWE Score',
'Cost ($/1M tkns)', // Add Cost column 'Cost ($/1M tkns)',
'API Key Status' 'API Key Status'
].map((h) => chalk.cyan.bold(h)), ].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'] } 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 = allModels
const validScores = allAvailableModels .map((m) => m.sweScore)
.map((m) => m.swe_score)
.filter((s) => s !== null && s !== undefined && s > 0); .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; const n = sortedScores.length;
let minScore3Stars = -Infinity; let stars = chalk.gray('☆☆☆');
let minScore2Stars = -Infinity;
if (n > 0) { if (n > 0) {
const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1);
const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1);
minScore3Stars = sortedScores[topThirdIndex]; if (score >= sortedScores[topThirdIndex])
minScore2Stars = sortedScores[midThirdIndex]; 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}`; return `${formattedPercentage} ${stars}`;
}; };
// Helper to format cost
const formatCost = (costObj) => { const formatCost = (costObj) => {
if (!costObj) return 'N/A'; if (!costObj) return 'N/A';
const formatSingleCost = (costValue) => { const formatSingleCost = (costValue) => {
if (costValue === null || costValue === undefined) return 'N/A'; if (costValue === null || costValue === undefined) return 'N/A';
// Check if the number is an integer
const isInteger = Number.isInteger(costValue); const isInteger = Number.isInteger(costValue);
return `$${costValue.toFixed(isInteger ? 0 : 2)}`; return `$${costValue.toFixed(isInteger ? 0 : 2)}`;
}; };
return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(
const inputCost = formatSingleCost(costObj.input); costObj.output
const outputCost = formatSingleCost(costObj.output); )} out`;
return `${inputCost} in, ${outputCost} out`; // Use cleaner separator
}; };
const getCombinedStatus = (cliOk, mcpOk) => { const getCombinedStatus = (keyStatus) => {
const cliOk = keyStatus?.cli;
const mcpOk = keyStatus?.mcp;
const cliSymbol = cliOk ? chalk.green('✓') : chalk.red('✗'); const cliSymbol = cliOk ? chalk.green('✓') : chalk.red('✗');
const mcpSymbol = mcpOk ? chalk.green('✓') : chalk.red('✗'); const mcpSymbol = mcpOk ? chalk.green('✓') : chalk.red('✗');
if (cliOk && mcpOk) { if (cliOk && mcpOk) return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
// Both symbols green, default text color if (cliOk && !mcpOk)
return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
} else if (cliOk && !mcpOk) {
// Symbols colored individually, default text color
return `${cliSymbol} CLI OK / ${mcpSymbol} MCP Missing`; return `${cliSymbol} CLI OK / ${mcpSymbol} MCP Missing`;
} else if (!cliOk && mcpOk) { if (!cliOk && mcpOk)
// Symbols colored individually, default text color
return `${cliSymbol} CLI Missing / ${mcpSymbol} MCP OK`; 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`); return chalk.gray(`${cliSymbol} CLI & MCP Both Missing`);
}
}; };
const mainModelData = findModelData(mainModelId); // Get all available models data once for SWE Score calculation
const researchModelData = findModelData(researchModelId); const availableModelsResultForScore = await getAvailableModelsList();
const fallbackModelData = findModelData(fallbackModelId); const allAvailModelsForScore =
availableModelsResultForScore.data?.models || [];
// Populate Active Table
activeTable.push([ activeTable.push([
chalk.white('Main'), chalk.white('Main'),
mainProvider, active.main.provider,
mainModelId, active.main.modelId,
formatSweScoreWithTertileStars(mainModelData?.swe_score), // Use tertile formatter formatSweScoreWithTertileStars(
formatCost(mainModelData?.cost_per_1m_tokens), active.main.sweScore,
getCombinedStatus(mainCliKeyOk, mainMcpKeyOk) allAvailModelsForScore
),
formatCost(active.main.cost),
getCombinedStatus(active.main.keyStatus)
]); ]);
activeTable.push([ activeTable.push([
chalk.white('Research'), chalk.white('Research'),
researchProvider, active.research.provider,
researchModelId, active.research.modelId,
formatSweScoreWithTertileStars(researchModelData?.swe_score), // Use tertile formatter formatSweScoreWithTertileStars(
formatCost(researchModelData?.cost_per_1m_tokens), active.research.sweScore,
getCombinedStatus(researchCliKeyOk, researchMcpKeyOk) allAvailModelsForScore
),
formatCost(active.research.cost),
getCombinedStatus(active.research.keyStatus)
]); ]);
if (active.fallback) {
if (fallbackProvider && fallbackModelId) {
activeTable.push([ activeTable.push([
chalk.white('Fallback'), chalk.white('Fallback'),
fallbackProvider, active.fallback.provider,
fallbackModelId, active.fallback.modelId,
formatSweScoreWithTertileStars(fallbackModelData?.swe_score), // Use tertile formatter formatSweScoreWithTertileStars(
formatCost(fallbackModelData?.cost_per_1m_tokens), active.fallback.sweScore,
getCombinedStatus(fallbackCliKeyOk, fallbackMcpKeyOk) allAvailModelsForScore
),
formatCost(active.fallback.cost),
getCombinedStatus(active.fallback.keyStatus)
]); ]);
} }
console.log(activeTable.toString()); console.log(activeTable.toString());
// --- Available Models Section --- // --- Available Models Section ---
// const availableModels = getAvailableModels(); // Already fetched const availableResult = await getAvailableModelsList();
if (!allAvailableModels || allAvailableModels.length === 0) { if (availableResult.success && availableResult.data.models.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) {
console.log(chalk.cyan.bold('\nOther Available Models:')); console.log(chalk.cyan.bold('\nOther Available Models:'));
const availableTable = new Table({ const availableTable = new Table({
head: [ head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map(
'Provider', (h) => chalk.cyan.bold(h)
'Model ID', ),
'SWE Score', // Update column name colWidths: [15, 40, 18, 25],
'Cost ($/1M tkns)' // Add Cost column
].map((h) => chalk.cyan.bold(h)),
colWidths: [15, 40, 18, 25], // Adjust widths for stars
style: { head: ['cyan', 'bold'] } style: { head: ['cyan', 'bold'] }
}); });
availableResult.data.models.forEach((model) => {
filteredAvailable.forEach((model) => {
availableTable.push([ availableTable.push([
model.provider || 'N/A', model.provider,
model.id, model.modelId,
formatSweScoreWithTertileStars(model.swe_score), // Use tertile formatter formatSweScoreWithTertileStars(
formatCost(model.cost_per_1m_tokens) model.sweScore,
allAvailModelsForScore
),
formatCost(model.cost)
]); ]);
}); });
console.log(availableTable.toString()); console.log(availableTable.toString());
} else { } else if (availableResult.success) {
console.log( console.log(
chalk.gray('\n(All available models are currently configured)') 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 --- // --- Suggested Actions Section ---
@@ -2157,11 +2131,21 @@ function registerCommands(programInstance) {
} }
) )
); );
}
} catch (error) { } catch (error) {
log(`Error processing models command: ${error.message}`, 'error'); // Catch errors specifically from the core model functions
if (error.stack && getDebugFlag()) { console.error(
log(error.stack, 'debug'); 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); 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 { findNextTask, analyzeTaskComplexity } from './task-manager.js';
import { import {
getProjectName, getProjectName,
getDefaultSubtasks,
getMainModelId,
getMainMaxTokens,
getMainTemperature,
getDebugFlag, getDebugFlag,
getLogLevel, getDefaultSubtasks
getDefaultPriority
} from './config-manager.js'; } from './config-manager.js';
// Create a color gradient for the banner // Create a color gradient for the banner
@@ -386,6 +381,9 @@ function formatDependenciesWithStatus(
function displayHelp() { function displayHelp() {
displayBanner(); 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( console.log(
boxen(chalk.white.bold('Task Master CLI'), { boxen(chalk.white.bold('Task Master CLI'), {
padding: 1, padding: 1,
@@ -397,6 +395,42 @@ function displayHelp() {
// Command categories // Command categories
const commandCategories = [ 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', title: 'Task Generation',
color: 'cyan', color: 'cyan',
@@ -430,7 +464,17 @@ function displayHelp() {
{ {
name: 'update', name: 'update',
args: '--from=<id> --prompt="<context>"', 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', name: 'add-task',
@@ -438,20 +482,46 @@ function displayHelp() {
desc: 'Add a new task using AI' desc: 'Add a new task using AI'
}, },
{ {
name: 'add-dependency', name: 'remove-task',
args: '--id=<id> --depends-on=<id>', args: '--id=<id> [-y]',
desc: 'Add a dependency to a task' desc: 'Permanently remove a task or subtask'
},
{
name: 'remove-dependency',
args: '--id=<id> --depends-on=<id>',
desc: 'Remove a dependency from a task'
} }
] ]
}, },
{ {
title: 'Task Analysis & Detail', title: 'Subtask Management',
color: 'yellow', 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: [ commands: [
{ {
name: 'analyze-complexity', name: 'analyze-complexity',
@@ -472,17 +542,12 @@ function displayHelp() {
name: 'expand --all', name: 'expand --all',
args: '[--force] [--research]', args: '[--force] [--research]',
desc: 'Expand all pending tasks with subtasks' desc: 'Expand all pending tasks with subtasks'
},
{
name: 'clear-subtasks',
args: '--id=<id>',
desc: 'Remove subtasks from specified tasks'
} }
] ]
}, },
{ {
title: 'Task Navigation & Viewing', title: 'Task Navigation & Viewing',
color: 'magenta', color: 'cyan',
commands: [ commands: [
{ {
name: 'next', name: 'next',
@@ -500,6 +565,16 @@ function displayHelp() {
title: 'Dependency Management', title: 'Dependency Management',
color: 'blue', color: 'blue',
commands: [ 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', name: 'validate-dependencies',
args: '', 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({ const commandTable = new Table({
colWidths: [25, 40, 45], colWidths: [nameWidth, argsWidth, descWidth],
chars: { chars: {
top: '', top: '',
'top-mid': '', 'top-mid': '',
@@ -544,7 +624,8 @@ function displayHelp() {
'right-mid': '', 'right-mid': '',
middle: ' ' middle: ' '
}, },
style: { border: [], 'padding-left': 4 } style: { border: [], 'padding-left': 4 },
wordWrap: true
}); });
category.commands.forEach((cmd, index) => { category.commands.forEach((cmd, index) => {
@@ -559,9 +640,9 @@ function displayHelp() {
console.log(''); console.log('');
}); });
// Display environment variables section // Display configuration section
console.log( console.log(
boxen(chalk.cyan.bold('Environment Variables'), { boxen(chalk.cyan.bold('Configuration'), {
padding: { left: 2, right: 2, top: 0, bottom: 0 }, padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 }, margin: { top: 1, bottom: 0 },
borderColor: 'cyan', borderColor: 'cyan',
@@ -569,8 +650,19 @@ function displayHelp() {
}) })
); );
const envTable = new Table({ // Get terminal width if not already defined
colWidths: [30, 50, 30], 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: { chars: {
top: '', top: '',
'top-mid': '', 'top-mid': '',
@@ -588,69 +680,59 @@ function displayHelp() {
'right-mid': '', 'right-mid': '',
middle: ' ' 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.yellow('.taskmasterconfig')}${chalk.reset('')}`,
`${chalk.white('Your Anthropic API key')}${chalk.reset('')}`, `${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`,
`${chalk.dim('Required')}${chalk.reset('')}` `${chalk.dim('Managed by models cmd')}${chalk.reset('')}`
], ],
[ [
`${chalk.yellow('MODEL')}${chalk.reset('')}`, `${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`,
`${chalk.white('Claude model to use')}${chalk.reset('')}`, `${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getMainModelId()}`)}${chalk.reset('')}` `${chalk.dim('Required in .env file')}${chalk.reset('')}`
], ],
[ [
`${chalk.yellow('MAX_TOKENS')}${chalk.reset('')}`, `${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`,
`${chalk.white('Maximum tokens for responses')}${chalk.reset('')}`, `${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${getMainMaxTokens()}`)}${chalk.reset('')}` `${chalk.dim('Required in .cursor/')}${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('')}`
] ]
); );
console.log(envTable.toString()); console.log(configTable.toString());
console.log(''); 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
}
)
);
} }
/** /**