Merge branch 'next' of github.com:eyaltoledano/claude-task-master into add-complexity-score-to-task

This commit is contained in:
Shrey Paharia
2025-05-01 22:21:02 +05:30
183 changed files with 24876 additions and 15910 deletions

View File

@@ -8,15 +8,7 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getModelConfig
} from '../utils/ai-client-utils.js';
import {
_buildAddTaskPrompt,
parseTaskJsonResponse,
_handleAnthropicStream
} from '../../../../scripts/modules/ai-services.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for adding a new task with error handling.
@@ -29,20 +21,24 @@ import {
* @param {string} [args.testStrategy] - Test strategy (for manual task creation)
* @param {string} [args.dependencies] - Comma-separated list of task IDs this task depends on
* @param {string} [args.priority='medium'] - Task priority (high, medium, low)
* @param {string} [args.file='tasks/tasks.json'] - Path to the tasks file
* @param {string} [args.projectRoot] - Project root directory
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {boolean} [args.research=false] - Whether to use research capabilities for task creation
* @param {Object} log - Logger object
* @param {Object} context - Additional context (reportProgress, session)
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function addTaskDirect(args, log, context = {}) {
// Destructure expected args
// Destructure expected args (including research)
const { tasksJsonPath, prompt, dependencies, priority, research } = args;
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('addTaskDirect called without tasksJsonPath');
@@ -79,20 +75,17 @@ export async function addTaskDirect(args, log, context = {}) {
}
// Extract and prepare parameters
const taskPrompt = prompt;
const taskDependencies = Array.isArray(dependencies)
? dependencies
: dependencies
? dependencies // Already an array if passed directly
: dependencies // Check if dependencies exist and are a string
? String(dependencies)
.split(',')
.map((id) => parseInt(id.trim(), 10))
: [];
const taskPriority = priority || 'medium';
// Extract context parameters for advanced functionality
const { session } = context;
.map((id) => parseInt(id.trim(), 10)) // Split, trim, and parse
: []; // Default to empty array if null/undefined
const taskPriority = priority || 'medium'; // Default priority
let manualTaskData = null;
let newTaskId;
if (isManualCreation) {
// Create manual task data object
@@ -108,150 +101,61 @@ export async function addTaskDirect(args, log, context = {}) {
);
// Call the addTask function with manual task data
const newTaskId = await addTask(
newTaskId = await addTask(
tasksPath,
null, // No prompt needed for manual creation
null, // prompt is null for manual creation
taskDependencies,
priority,
taskPriority,
{
mcpLog: log,
session
session,
mcpLog
},
'json', // Use JSON output format to prevent console output
null, // No custom environment
manualTaskData // Pass the manual task data
'json', // outputFormat
manualTaskData, // Pass the manual task data
false // research flag is false for manual creation
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
taskId: newTaskId,
message: `Successfully added new task #${newTaskId}`
}
};
} else {
// AI-driven task creation
log.info(
`Adding new task with prompt: "${prompt}", dependencies: [${taskDependencies.join(', ')}], priority: ${priority}`
`Adding new task with prompt: "${prompt}", dependencies: [${taskDependencies.join(', ')}], priority: ${taskPriority}, research: ${research}`
);
// Initialize AI client with session environment
let localAnthropic;
try {
localAnthropic = getAnthropicClientForMCP(session, log);
} catch (error) {
log.error(`Failed to initialize Anthropic client: ${error.message}`);
disableSilentMode();
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
}
};
}
// Get model configuration from session
const modelConfig = getModelConfig(session);
// Read existing tasks to provide context
let tasksData;
try {
const fs = await import('fs');
tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
} catch (error) {
log.warn(`Could not read existing tasks for context: ${error.message}`);
tasksData = { tasks: [] };
}
// Build prompts for AI
const { systemPrompt, userPrompt } = _buildAddTaskPrompt(
prompt,
tasksData.tasks
);
// Make the AI call using the streaming helper
let responseText;
try {
responseText = await _handleAnthropicStream(
localAnthropic,
{
model: modelConfig.model,
max_tokens: modelConfig.maxTokens,
temperature: modelConfig.temperature,
messages: [{ role: 'user', content: userPrompt }],
system: systemPrompt
},
{
mcpLog: log
}
);
} catch (error) {
log.error(`AI processing failed: ${error.message}`);
disableSilentMode();
return {
success: false,
error: {
code: 'AI_PROCESSING_ERROR',
message: `Failed to generate task with AI: ${error.message}`
}
};
}
// Parse the AI response
let taskDataFromAI;
try {
taskDataFromAI = parseTaskJsonResponse(responseText);
} catch (error) {
log.error(`Failed to parse AI response: ${error.message}`);
disableSilentMode();
return {
success: false,
error: {
code: 'RESPONSE_PARSING_ERROR',
message: `Failed to parse AI response: ${error.message}`
}
};
}
// Call the addTask function with 'json' outputFormat to prevent console output when called via MCP
const newTaskId = await addTask(
// Call the addTask function, passing the research flag
newTaskId = await addTask(
tasksPath,
prompt,
prompt, // Use the prompt for AI creation
taskDependencies,
priority,
taskPriority,
{
mcpLog: log,
session
session,
mcpLog
},
'json',
null,
taskDataFromAI // Pass the parsed AI result as the manual task data
'json', // outputFormat
null, // manualTaskData is null for AI creation
research // Pass the research flag
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
taskId: newTaskId,
message: `Successfully added new task #${newTaskId}`
}
};
}
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
taskId: newTaskId,
message: `Successfully added new task #${newTaskId}`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addTaskDirect: ${error.message}`);
// Add specific error code checks if needed
return {
success: false,
error: {
code: 'ADD_TASK_ERROR',
code: error.code || 'ADD_TASK_ERROR', // Use error code if available
message: error.message
}
};

View File

@@ -2,37 +2,36 @@
* Direct function wrapper for analyzeTaskComplexity
*/
import { analyzeTaskComplexity } from '../../../../scripts/modules/task-manager.js';
import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
import {
enableSilentMode,
disableSilentMode,
isSilentMode,
readJSON
isSilentMode
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import path from 'path';
import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
/**
* Analyze task complexity and generate recommendations
* @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.outputPath - Explicit absolute path to save the report.
* @param {string} [args.model] - LLM model to use for analysis
* @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
* @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
* @param {Object} log - Logger object
* @param {Object} [context={}] - Context object containing session data
* @param {Object} [context.session] - MCP session object
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function analyzeTaskComplexityDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
const { session } = context; // Extract session
// Destructure expected args
const { tasksJsonPath, outputPath, model, threshold, research } = args;
const { tasksJsonPath, outputPath, model, threshold, research } = args; // Model is ignored by core function now
// --- Initial Checks (remain the same) ---
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
// Check if required paths were provided
if (!tasksJsonPath) {
log.error('analyzeTaskComplexityDirect called without tasksJsonPath');
return {
@@ -51,7 +50,6 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
};
}
// Use the provided paths
const tasksPath = tasksJsonPath;
const resolvedOutputPath = outputPath;
@@ -59,78 +57,91 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
log.info(`Output report will be saved to: ${resolvedOutputPath}`);
if (research) {
log.info('Using Perplexity AI for research-backed complexity analysis');
log.info('Using research role for complexity analysis');
}
// Create options object for analyzeTaskComplexity using provided paths
// Prepare options for the core function
const options = {
file: tasksPath,
output: resolvedOutputPath,
model: model,
// model: model, // No longer needed
threshold: threshold,
research: research === true
research: research === true // Ensure boolean
};
// --- End Initial Checks ---
// Enable silent mode to prevent console logs from interfering with JSON response
// --- Silent Mode and Logger Wrapper (remain the same) ---
const wasSilent = isSilentMode();
if (!wasSilent) {
enableSilentMode();
}
// Create a logWrapper that matches the expected mcpLog interface as specified in utilities.mdc
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),
success: (message, ...args) => log.info(message, ...args) // Map success to info
};
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
let report; // To store the result from the core function
try {
// Call the core function with session and logWrapper as mcpLog
await analyzeTaskComplexity(options, {
session,
mcpLog: logWrapper // Use the wrapper instead of passing log directly
// --- Call Core Function (Updated Context Passing) ---
// Call the core function, passing options and the context object { session, mcpLog }
report = await analyzeTaskComplexity(options, {
session, // Pass the session object
mcpLog // Pass the logger wrapper
});
// --- End Core Function Call ---
} catch (error) {
log.error(`Error in analyzeTaskComplexity: ${error.message}`);
log.error(
`Error in analyzeTaskComplexity core function: ${error.message}`
);
// Restore logging if we changed it
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
return {
success: false,
error: {
code: 'ANALYZE_ERROR',
message: `Error running complexity analysis: ${error.message}`
code: 'ANALYZE_CORE_ERROR', // More specific error code
message: `Error running core complexity analysis: ${error.message}`
}
};
} finally {
// Always restore normal logging in finally block, but only if we enabled it
if (!wasSilent) {
// Always restore normal logging in finally block if we enabled silent mode
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
}
// Verify the report file was created
// --- Result Handling (remains largely the same) ---
// Verify the report file was created (core function writes it)
if (!fs.existsSync(resolvedOutputPath)) {
return {
success: false,
error: {
code: 'ANALYZE_ERROR',
message: 'Analysis completed but no report file was created'
code: 'ANALYZE_REPORT_MISSING', // Specific code
message:
'Analysis completed but no report file was created at the expected path.'
}
};
}
// The core function now returns the report object directly
if (!report || !report.complexityAnalysis) {
log.error(
'Core analyzeTaskComplexity function did not return a valid report object.'
);
return {
success: false,
error: {
code: 'INVALID_CORE_RESPONSE',
message: 'Core analysis function returned an invalid response.'
}
};
}
// Read the report file
let report;
try {
report = JSON.parse(fs.readFileSync(resolvedOutputPath, 'utf8'));
const analysisArray = report.complexityAnalysis; // Already an array
// Important: Handle different report formats
// The core function might return an array or an object with a complexityAnalysis property
const analysisArray = Array.isArray(report)
? report
: report.complexityAnalysis || [];
// Count tasks by complexity
// Count tasks by complexity (remains the same)
const highComplexityTasks = analysisArray.filter(
(t) => t.complexityScore >= 8
).length;
@@ -152,29 +163,33 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
mediumComplexityTasks,
lowComplexityTasks
}
// Include the full report data if needed by the client
// fullReport: report
}
};
} catch (parseError) {
log.error(`Error parsing report file: ${parseError.message}`);
// Should not happen if core function returns object, but good safety check
log.error(`Internal error processing report data: ${parseError.message}`);
return {
success: false,
error: {
code: 'REPORT_PARSE_ERROR',
message: `Error parsing complexity report: ${parseError.message}`
code: 'REPORT_PROCESS_ERROR',
message: `Internal error processing complexity report: ${parseError.message}`
}
};
}
// --- End Result Handling ---
} catch (error) {
// Make sure to restore normal logging even if there's an error
// Catch errors from initial checks or path resolution
// Make sure to restore normal logging if silent mode was enabled
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
code: 'DIRECT_FUNCTION_SETUP_ERROR',
message: error.message
}
};

View File

@@ -9,7 +9,6 @@ import {
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { getCachedOrExecute } from '../../tools/utils.js';
import path from 'path';
/**
* Direct function wrapper for displaying the complexity report with error handling and caching.

View File

@@ -5,138 +5,85 @@
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode,
isSilentMode
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { getAnthropicClientForMCP } from '../utils/ai-client-utils.js';
import path from 'path';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Expand all pending tasks with subtasks
* Expand all pending tasks with subtasks (Direct Function Wrapper)
* @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {number|string} [args.num] - Number of subtasks to generate
* @param {boolean} [args.research] - Enable Perplexity AI for research-backed subtask generation
* @param {boolean} [args.research] - Enable research-backed subtask generation
* @param {string} [args.prompt] - Additional context to guide subtask generation
* @param {boolean} [args.force] - Force regeneration of subtasks for tasks that already have them
* @param {Object} log - Logger object
* @param {Object} log - Logger object from FastMCP
* @param {Object} context - Context object containing session
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function expandAllTasksDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
const { session } = context; // Extract session
// Destructure expected args
const { tasksJsonPath, num, research, prompt, force } = args;
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('expandAllTasksDirect called without tasksJsonPath');
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Enable silent mode early to prevent any console output
enableSilentMode();
try {
// Remove internal path finding
/*
const tasksPath = findTasksJsonPath(args, log);
*/
// Use provided path
const tasksPath = tasksJsonPath;
// Parse parameters
const numSubtasks = num ? parseInt(num, 10) : undefined;
const useResearch = research === true;
const additionalContext = prompt || '';
const forceFlag = force === true;
log.info(
`Expanding all tasks with ${numSubtasks || 'default'} subtasks each...`
);
if (useResearch) {
log.info('Using Perplexity AI for research-backed subtask generation');
// Initialize AI client for research-backed expansion
try {
await getAnthropicClientForMCP(session, log);
} catch (error) {
// Ensure silent mode is disabled before returning error
disableSilentMode();
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
}
};
}
}
if (additionalContext) {
log.info(`Additional context: "${additionalContext}"`);
}
if (forceFlag) {
log.info('Force regeneration of subtasks is enabled');
}
// Call the core function with session context for AI operations
// and outputFormat as 'json' to prevent UI elements
const result = await expandAllTasks(
tasksPath,
numSubtasks,
useResearch,
additionalContext,
forceFlag,
{ mcpLog: log, session },
'json' // Use JSON output format to prevent UI elements
);
// The expandAllTasks function now returns a result object
return {
success: true,
data: {
message: 'Successfully expanded all pending tasks with subtasks',
details: {
numSubtasks: numSubtasks,
research: useResearch,
prompt: additionalContext,
force: forceFlag,
tasksExpanded: result.expandedCount,
totalEligibleTasks: result.tasksToExpand
}
}
};
} finally {
// Restore normal logging in finally block to ensure it runs even if there's an error
disableSilentMode();
}
} catch (error) {
// Ensure silent mode is disabled if an error occurs
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error in expandAllTasksDirect: ${error.message}`);
if (!tasksJsonPath) {
log.error('expandAllTasksDirect called without tasksJsonPath');
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message: error.message
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
enableSilentMode(); // Enable silent mode for the core function call
try {
log.info(
`Calling core expandAllTasks with args: ${JSON.stringify({ num, research, prompt, force })}`
);
// Parse parameters (ensure correct types)
const numSubtasks = num ? parseInt(num, 10) : undefined;
const useResearch = research === true;
const additionalContext = prompt || '';
const forceFlag = force === true;
// Call the core function, passing options and the context object { session, mcpLog }
const result = await expandAllTasks(
tasksJsonPath,
numSubtasks,
useResearch,
additionalContext,
forceFlag,
{ session, mcpLog }
);
// Core function now returns a summary object
return {
success: true,
data: {
message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`,
details: result // Include the full result details
}
};
} catch (error) {
// Log the error using the MCP logger
log.error(`Error during core expandAllTasks execution: ${error.message}`);
// Optionally log stack trace if available and debug enabled
// if (error.stack && log.debug) { log.debug(error.stack); }
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible
message: error.message
}
};
} finally {
disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled
}
}

View File

@@ -3,7 +3,7 @@
* Direct function implementation for expanding a task into subtasks
*/
import { expandTask } from '../../../../scripts/modules/task-manager.js';
import expandTask from '../../../../scripts/modules/task-manager/expand-task.js';
import {
readJSON,
writeJSON,
@@ -11,12 +11,9 @@ import {
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getModelConfig
} from '../utils/ai-client-utils.js';
import path from 'path';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for expanding a task into subtasks with error handling.
@@ -25,15 +22,16 @@ import fs from 'fs';
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task to expand.
* @param {number|string} [args.num] - Number of subtasks to generate.
* @param {boolean} [args.research] - Enable Perplexity AI for research-backed subtask generation.
* @param {boolean} [args.research] - Enable research role for subtask generation.
* @param {string} [args.prompt] - Additional context to guide subtask generation.
* @param {boolean} [args.force] - Force expansion even if subtasks exist.
* @param {Object} log - Logger object
* @param {Object} context - Context object containing session and reportProgress
* @param {Object} context - Context object containing session
* @param {Object} [context.session] - MCP Session object
* @returns {Promise<Object>} - Task expansion result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
*/
export async function expandTaskDirect(args, log, context = {}) {
const { session } = context;
const { session } = context; // Extract session
// Destructure expected args
const { tasksJsonPath, id, num, research, prompt, force } = args;
@@ -85,28 +83,9 @@ export async function expandTaskDirect(args, log, context = {}) {
const additionalContext = prompt || '';
const forceFlag = force === true;
// Initialize AI client if needed (for expandTask function)
try {
// This ensures the AI client is available by checking it
if (useResearch) {
log.info('Verifying AI client for research-backed expansion');
await getAnthropicClientForMCP(session, log);
}
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
try {
log.info(
`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}`
`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}`
);
// Read tasks data
@@ -202,23 +181,27 @@ export async function expandTaskDirect(args, log, context = {}) {
// Save tasks.json with potentially empty subtasks array
writeJSON(tasksPath, data);
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
// Process the request
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
const wasSilent = isSilentMode();
if (!wasSilent) enableSilentMode();
// Call expandTask with session context to ensure AI client is properly initialized
// Call the core expandTask function with the wrapped logger
const result = await expandTask(
tasksPath,
taskId,
numSubtasks,
useResearch,
additionalContext,
{ mcpLog: log, session } // Only pass mcpLog and session, NOT reportProgress
{ mcpLog, session }
);
// Restore normal logging
disableSilentMode();
if (!wasSilent && isSilentMode()) disableSilentMode();
// Read the updated data
const updatedData = readJSON(tasksPath);
@@ -244,7 +227,7 @@ export async function expandTaskDirect(args, log, context = {}) {
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
if (!wasSilent && isSilentMode()) disableSilentMode();
log.error(`Error expanding task: ${error.message}`);
return {

View File

@@ -8,7 +8,6 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import path from 'path';
/**
* Direct function wrapper for generateTaskFiles with error handling.

View File

@@ -10,7 +10,7 @@ import os from 'os'; // Import os module for home directory check
/**
* Direct function wrapper for initializing a project.
* Derives target directory from session, sets CWD, and calls core init logic.
* @param {object} args - Arguments containing project details and options (projectName, projectDescription, yes, etc.)
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot)
* @param {object} log - The FastMCP logger instance.
* @param {object} context - The context object, must contain { session }.
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
@@ -92,12 +92,8 @@ export async function initializeProjectDirect(args, log, context = {}) {
try {
// Always force yes: true when called via MCP to avoid interactive prompts
const options = {
name: args.projectName,
description: args.projectDescription,
version: args.projectVersion,
author: args.authorName,
skipInstall: args.skipInstall,
aliases: args.addAliases,
skipInstall: args.skipInstall,
yes: true // Force yes mode
};

View File

@@ -0,0 +1,121 @@
/**
* 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';
import { createLogWrapper } from '../../tools/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 mcpLog = createLogWrapper(log);
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
log.info(`Using project root: ${projectRoot}`);
// Validate flags: cannot use both openrouter and ollama simultaneously
if (args.openrouter && args.ollama) {
log.error(
'Error: Cannot use both openrouter and ollama flags simultaneously.'
);
return {
success: false,
error: {
code: 'INVALID_ARGS',
message: 'Cannot use both openrouter and ollama flags simultaneously.'
}
};
}
try {
enableSilentMode();
try {
// Check for the listAvailableModels flag
if (args.listAvailableModels === true) {
return await getAvailableModelsList({
session,
mcpLog,
projectRoot // Pass projectRoot to function
});
}
// Handle setting a specific model
if (args.setMain) {
return await setModel('main', args.setMain, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setResearch) {
return await setModel('research', args.setResearch, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setFallback) {
return await setModel('fallback', args.setFallback, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
// Default action: get current configuration
return await getModelConfiguration({
session,
mcpLog,
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

@@ -5,48 +5,27 @@
import path from 'path';
import fs from 'fs';
import os from 'os'; // Import os module for home directory check
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getModelConfig
} from '../utils/ai-client-utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
*
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output options.
* @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options.
* @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function parsePRDDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
const { session } = context; // Only extract session
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Initialize AI client for PRD parsing
let aiClient;
try {
aiClient = getAnthropicClientForMCP(session, log);
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
// Validate required parameters
if (!args.projectRoot) {
const errorMessage = 'Project root is required for parsePRDDirect';
@@ -57,7 +36,6 @@ export async function parsePRDDirect(args, log, context = {}) {
fromCache: false
};
}
if (!args.input) {
const errorMessage = 'Input file path is required for parsePRDDirect';
log.error(errorMessage);
@@ -67,7 +45,6 @@ export async function parsePRDDirect(args, log, context = {}) {
fromCache: false
};
}
if (!args.output) {
const errorMessage = 'Output file path is required for parsePRDDirect';
log.error(errorMessage);
@@ -124,21 +101,22 @@ export async function parsePRDDirect(args, log, context = {}) {
}
}
// Extract the append flag from args
const append = Boolean(args.append) === true;
// Log key parameters including append flag
log.info(
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
);
// Create the logger wrapper for proper logging in the core function
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),
success: (message, ...args) => log.info(message, ...args) // Map success to info
};
// --- Logger Wrapper ---
const mcpLog = createLogWrapper(log);
// Get model config from session
const modelConfig = getModelConfig(session);
// Prepare options for the core function
const options = {
mcpLog,
session
};
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
@@ -151,13 +129,14 @@ export async function parsePRDDirect(args, log, context = {}) {
}
// Execute core parsePRD function with AI client
await parsePRD(
const tasksDataResult = await parsePRD(
inputPath,
outputPath,
numTasks,
{
mcpLog: logWrapper,
session
session,
append
},
aiClient,
modelConfig
@@ -167,16 +146,24 @@ export async function parsePRDDirect(args, log, context = {}) {
// to return it to the caller
if (fs.existsSync(outputPath)) {
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
log.info(
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`
);
const actionVerb = append ? 'appended' : 'generated';
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
if (!tasksDataResult || !tasksDataResult.tasks || !tasksData) {
throw new Error(
'Core parsePRD function did not return valid task data.'
);
}
log.info(message);
return {
success: true,
data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0,
outputPath
message,
taskCount: tasksDataResult.tasks?.length || 0,
outputPath,
appended: append
},
fromCache: false // This operation always modifies state and should never be cached
};
@@ -201,7 +188,7 @@ export async function parsePRDDirect(args, log, context = {}) {
return {
success: false,
error: {
code: 'PARSE_PRD_ERROR',
code: error.code || 'PARSE_PRD_ERROR', // Use error code if available
message: error.message || 'Unknown error parsing PRD'
},
fromCache: false

View File

@@ -3,18 +3,23 @@
* Direct function implementation for removing a task
*/
import { removeTask } from '../../../../scripts/modules/task-manager.js';
import {
removeTask,
taskExists
} from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
disableSilentMode,
readJSON
} from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for removeTask with error handling.
* Supports removing multiple tasks at once with comma-separated IDs.
*
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task or subtask to remove.
* @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
*/
@@ -36,8 +41,7 @@ export async function removeTaskDirect(args, log) {
}
// Validate task ID parameter
const taskId = id;
if (!taskId) {
if (!id) {
log.error('Task ID is required');
return {
success: false,
@@ -49,46 +53,103 @@ export async function removeTaskDirect(args, log) {
};
}
// Skip confirmation in the direct function since it's handled by the client
log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`);
// Split task IDs if comma-separated
const taskIdArray = id.split(',').map((taskId) => taskId.trim());
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
log.info(
`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
);
// Call the core removeTask function using the provided path
const result = await removeTask(tasksJsonPath, taskId);
// Restore normal logging
disableSilentMode();
log.info(`Successfully removed task: ${taskId}`);
// Return the result
return {
success: true,
data: {
message: result.message,
taskId: taskId,
tasksPath: tasksJsonPath,
removedTask: result.removedTask
},
fromCache: false
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error removing task: ${error.message}`);
// Validate all task IDs exist before proceeding
const data = readJSON(tasksJsonPath);
if (!data || !data.tasks) {
return {
success: false,
error: {
code: error.code || 'REMOVE_TASK_ERROR',
message: error.message || 'Failed to remove task'
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksJsonPath}`
},
fromCache: false
};
}
const invalidTasks = taskIdArray.filter(
(taskId) => !taskExists(data.tasks, taskId)
);
if (invalidTasks.length > 0) {
return {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: `The following tasks were not found: ${invalidTasks.join(', ')}`
},
fromCache: false
};
}
// Remove tasks one by one
const results = [];
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
try {
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksJsonPath, taskId);
results.push({
taskId,
success: true,
message: result.message,
removedTask: result.removedTask
});
log.info(`Successfully removed task: ${taskId}`);
} catch (error) {
results.push({
taskId,
success: false,
error: error.message
});
log.error(`Error removing task ${taskId}: ${error.message}`);
}
}
} finally {
// Restore normal logging
disableSilentMode();
}
// Check if all tasks were successfully removed
const successfulRemovals = results.filter((r) => r.success);
const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length === 0) {
// All removals failed
return {
success: false,
error: {
code: 'REMOVE_TASK_ERROR',
message: 'Failed to remove any tasks',
details: failedRemovals
.map((r) => `${r.taskId}: ${r.error}`)
.join('; ')
},
fromCache: false
};
}
// At least some tasks were removed successfully
return {
success: true,
data: {
totalTasks: taskIdArray.length,
successful: successfulRemovals.length,
failed: failedRemovals.length,
results: results,
tasksPath: tasksJsonPath
},
fromCache: false
};
} catch (error) {
// Ensure silent mode is disabled even if an outer error occurs
disableSilentMode();

View File

@@ -21,12 +21,13 @@ import {
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task or subtask to show.
* @param {string} args.reportPath - Explicit path to the complexity report file.
* @param {string} [args.status] - Optional status to filter subtasks by.
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Task details result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
*/
export async function showTaskDirect(args, log) {
// Destructure expected args
const { tasksJsonPath, reportPath, id } = args;
const { tasksJsonPath, reportPath, id, status } = args;
if (!tasksJsonPath) {
log.error('showTaskDirect called without tasksJsonPath');
@@ -54,8 +55,8 @@ export async function showTaskDirect(args, log) {
};
}
// Generate cache key using the provided task path and ID
const cacheKey = `showTask:${tasksJsonPath}:${taskId}:${reportPath}`;
// Generate cache key using the provided task path, ID, report path, and status filter
const cacheKey = `showTask:${tasksJsonPath}:${taskId}:${reportPath}:${status || 'all'}`;
// Define the action function to be executed on cache miss
const coreShowTaskAction = async () => {
@@ -64,7 +65,7 @@ export async function showTaskDirect(args, log) {
enableSilentMode();
log.info(
`Retrieving task details for ID: ${taskId} from ${tasksJsonPath}`
`Retrieving task details for ID: ${taskId} from ${tasksJsonPath}${status ? ` (filtering by status: ${status})` : ''}`
);
// Read tasks data using the provided path
@@ -83,8 +84,13 @@ export async function showTaskDirect(args, log) {
// Read the complexity report
const complexityReport = readComplexityReport(reportPath);
// Find the specific task
const task = findTaskById(data.tasks, taskId, complexityReport);
// Find the specific task, passing the status filter
const { task, originalSubtaskCount } = findTaskById(
data.tasks,
taskId,
complexityReport,
status
);
if (!task) {
disableSilentMode(); // Disable before returning
@@ -92,7 +98,7 @@ export async function showTaskDirect(args, log) {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
message: `Task with ID ${taskId} not found${status ? ` or no subtasks match status '${status}'` : ''}`
}
};
}
@@ -100,13 +106,16 @@ export async function showTaskDirect(args, log) {
// Restore normal logging
disableSilentMode();
// Return the task data with the full tasks array for reference
// (needed for formatDependenciesWithStatus function in UI)
log.info(`Successfully found task ${taskId}`);
// Return the task data, the original subtask count (if applicable),
// and the full tasks array for reference (needed for formatDependenciesWithStatus function in UI)
log.info(
`Successfully found task ${taskId}${status ? ` (with status filter: ${status})` : ''}`
);
return {
success: true,
data: {
task,
originalSubtaskCount,
allTasks: data.tasks
}
};

View File

@@ -8,10 +8,7 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for updateSubtaskById with error handling.
@@ -95,40 +92,12 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}`
);
// Initialize the appropriate AI client based on research flag
try {
if (useResearch) {
// Initialize Perplexity client
await getPerplexityClientForMCP(session);
} else {
// Initialize Anthropic client
await getAnthropicClientForMCP(session);
}
} catch (error) {
log.error(`AI client initialization error: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: error.message || 'Failed to initialize AI client'
},
fromCache: false
};
}
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create a logger wrapper object to handle logging without breaking the mcpLog[level] calls
// This ensures outputFormat is set to 'json' while still supporting proper logging
const logWrapper = {
info: (message) => log.info(message),
warn: (message) => log.warn(message),
error: (message) => log.error(message),
debug: (message) => log.debug && log.debug(message),
success: (message) => log.info(message) // Map success to info if needed
};
// Create the logger wrapper using the utility function
const mcpLog = createLogWrapper(log);
// Execute core updateSubtaskById function
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
@@ -139,7 +108,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
useResearch,
{
session,
mcpLog: logWrapper
mcpLog
}
);

View File

@@ -8,10 +8,7 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for updateTaskById with error handling.
@@ -92,28 +89,6 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
// Get research flag
const useResearch = research === true;
// Initialize appropriate AI client based on research flag
let aiClient;
try {
if (useResearch) {
log.info('Using Perplexity AI for research-backed task update');
aiClient = await getPerplexityClientForMCP(session, log);
} else {
log.info('Using Claude AI for task update');
aiClient = getAnthropicClientForMCP(session, log);
}
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
log.info(
`Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}`
);
@@ -122,14 +97,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create a logger wrapper that matches what updateTaskById expects
const logWrapper = {
info: (message) => log.info(message),
warn: (message) => log.warn(message),
error: (message) => log.error(message),
debug: (message) => log.debug && log.debug(message),
success: (message) => log.info(message) // Map success to info since many loggers don't have success
};
// Create the logger wrapper using the utility function
const mcpLog = createLogWrapper(log);
// Execute core updateTaskById function with proper parameters
await updateTaskById(
@@ -138,7 +107,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
prompt,
useResearch,
{
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
mcpLog, // Pass the wrapped logger
session
},
'json'

View File

@@ -8,180 +8,123 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for updating tasks based on new context/prompt.
*
* @param {Object} args - Command arguments containing fromId, prompt, useResearch and tasksJsonPath.
* @param {Object} args - Command arguments containing from, prompt, research and tasksJsonPath.
* @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function updateTasksDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
const { session } = context; // Extract session
const { tasksJsonPath, from, prompt, research } = args;
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
// Create the standard logger wrapper
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),
success: (message, ...args) => log.info(message, ...args)
};
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
const errorMessage = 'tasksJsonPath is required but was not provided.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: errorMessage },
fromCache: false
};
}
// Check for the common mistake of using 'id' instead of 'from'
if (args.id !== undefined && from === undefined) {
const errorMessage =
"You specified 'id' parameter but 'update' requires 'from' parameter. Use 'from' for this tool or use 'update_task' tool if you want to update a single task.";
log.error(errorMessage);
return {
success: false,
error: {
code: 'PARAMETER_MISMATCH',
message: errorMessage,
suggestion:
"Use 'from' parameter instead of 'id', or use the 'update_task' tool for single task updates"
},
fromCache: false
};
}
// Check required parameters
if (!from) {
const errorMessage =
'No from ID specified. Please provide a task ID to start updating from.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_FROM_ID', message: errorMessage },
fromCache: false
};
}
if (!prompt) {
const errorMessage =
'No prompt specified. Please provide a prompt with new context for task updates.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_PROMPT', message: errorMessage },
fromCache: false
};
}
// Parse fromId - handle both string and number values
let fromId;
if (typeof from === 'string') {
fromId = parseInt(from, 10);
if (isNaN(fromId)) {
const errorMessage = `Invalid from ID: ${from}. Task ID must be a positive integer.`;
log.error(errorMessage);
return {
success: false,
error: { code: 'INVALID_FROM_ID', message: errorMessage },
fromCache: false
};
}
} else {
fromId = from;
}
// Get research flag
const useResearch = research === true;
// Initialize appropriate AI client based on research flag
let aiClient;
try {
if (useResearch) {
log.info('Using Perplexity AI for research-backed task updates');
aiClient = await getPerplexityClientForMCP(session, log);
} else {
log.info('Using Claude AI for task updates');
aiClient = getAnthropicClientForMCP(session, log);
}
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
log.info(
`Updating tasks from ID ${fromId} with prompt "${prompt}" and research: ${useResearch}`
);
// Create the logger wrapper to ensure compatibility with core functions
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), // Handle optional debug
success: (message, ...args) => log.info(message, ...args) // Map success to info if needed
// --- Input Validation (Keep existing checks) ---
if (!tasksJsonPath) {
log.error('updateTasksDirect called without tasksJsonPath');
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' },
fromCache: false
};
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Execute core updateTasks function, passing the AI client and session
await updateTasks(tasksJsonPath, fromId, prompt, useResearch, {
mcpLog: logWrapper, // Pass the wrapper instead of the raw log object
session
});
// Since updateTasks doesn't return a value but modifies the tasks file,
// we'll return a success message
return {
success: true,
data: {
message: `Successfully updated tasks from ID ${fromId} based on the prompt`,
fromId,
tasksPath: tasksJsonPath,
useResearch
},
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
log.error(`Error updating tasks: ${error.message}`);
return {
success: false,
error: {
code: 'UPDATE_TASKS_ERROR',
message: error.message || 'Unknown error updating tasks'
},
fromCache: false
};
} finally {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
}
} catch (error) {
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Error updating tasks: ${error.message}`);
}
if (args.id !== undefined && from === undefined) {
// Keep 'from' vs 'id' check
const errorMessage =
"Use 'from' parameter, not 'id', or use 'update_task' tool.";
log.error(errorMessage);
return {
success: false,
error: { code: 'PARAMETER_MISMATCH', message: errorMessage },
fromCache: false
};
}
if (!from) {
log.error('Missing from ID.');
return {
success: false,
error: { code: 'MISSING_FROM_ID', message: 'No from ID specified.' },
fromCache: false
};
}
if (!prompt) {
log.error('Missing prompt.');
return {
success: false,
error: { code: 'MISSING_PROMPT', message: 'No prompt specified.' },
fromCache: false
};
}
let fromId;
try {
fromId = parseInt(from, 10);
if (isNaN(fromId) || fromId <= 0) throw new Error();
} catch {
log.error(`Invalid from ID: ${from}`);
return {
success: false,
error: {
code: 'UPDATE_TASKS_ERROR',
message: error.message || 'Unknown error updating tasks'
code: 'INVALID_FROM_ID',
message: `Invalid from ID: ${from}. Must be a positive integer.`
},
fromCache: false
};
}
const useResearch = research === true;
// --- End Input Validation ---
log.info(`Updating tasks from ID ${fromId}. Research: ${useResearch}`);
enableSilentMode(); // Enable silent mode
try {
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
// Execute core updateTasks function, passing session context
await updateTasks(
tasksJsonPath,
fromId,
prompt,
useResearch,
// Pass context with logger wrapper and session
{ mcpLog, session },
'json' // Explicitly request JSON format for MCP
);
// Since updateTasks modifies file and doesn't return data, create success message
return {
success: true,
data: {
message: `Successfully initiated update for tasks from ID ${fromId} based on the prompt.`,
fromId,
tasksPath: tasksJsonPath,
useResearch
},
fromCache: false // Modifies state
};
} catch (error) {
log.error(`Error executing core updateTasks: ${error.message}`);
return {
success: false,
error: {
code: 'UPDATE_TASKS_CORE_ERROR',
message: error.message || 'Unknown error updating tasks'
},
fromCache: false
};
} finally {
disableSilentMode(); // Ensure silent mode is disabled
}
}

View File

@@ -29,19 +29,11 @@ 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';
// Re-export AI client utilities
export {
getAnthropicClientForMCP,
getPerplexityClientForMCP,
getModelConfig,
getBestAvailableAIModel,
handleClaudeError
} from './utils/ai-client-utils.js';
// Use Map for potential future enhancements like introspection or dynamic dispatch
export const directFunctions = new Map([
['listTasksDirect', listTasksDirect],
@@ -66,7 +58,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 +88,6 @@ export {
complexityReportDirect,
addDependencyDirect,
removeTaskDirect,
initializeProjectDirect
initializeProjectDirect,
modelsDirect
};

View File

@@ -1,213 +0,0 @@
/**
* ai-client-utils.js
* Utility functions for initializing AI clients in MCP context
*/
import { Anthropic } from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
// Load environment variables for CLI mode
dotenv.config();
// Default model configuration from CLI environment
const DEFAULT_MODEL_CONFIG = {
model: 'claude-3-7-sonnet-20250219',
maxTokens: 64000,
temperature: 0.2
};
/**
* Get an Anthropic client instance initialized with MCP session environment variables
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {Anthropic} Anthropic client instance
* @throws {Error} If API key is missing
*/
export function getAnthropicClientForMCP(session, log = console) {
try {
// Extract API key from session.env or fall back to environment variables
const apiKey =
session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error(
'ANTHROPIC_API_KEY not found in session environment or process.env'
);
}
// Initialize and return a new Anthropic client
return new Anthropic({
apiKey,
defaultHeaders: {
'anthropic-beta': 'output-128k-2025-02-19' // Include header for increased token limit
}
});
} catch (error) {
log.error(`Failed to initialize Anthropic client: ${error.message}`);
throw error;
}
}
/**
* Get a Perplexity client instance initialized with MCP session environment variables
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {OpenAI} OpenAI client configured for Perplexity API
* @throws {Error} If API key is missing or OpenAI package can't be imported
*/
export async function getPerplexityClientForMCP(session, log = console) {
try {
// Extract API key from session.env or fall back to environment variables
const apiKey =
session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY;
if (!apiKey) {
throw new Error(
'PERPLEXITY_API_KEY not found in session environment or process.env'
);
}
// Dynamically import OpenAI (it may not be used in all contexts)
const { default: OpenAI } = await import('openai');
// Initialize and return a new OpenAI client configured for Perplexity
return new OpenAI({
apiKey,
baseURL: 'https://api.perplexity.ai'
});
} catch (error) {
log.error(`Failed to initialize Perplexity client: ${error.message}`);
throw error;
}
}
/**
* Get model configuration from session environment or fall back to defaults
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [defaults] - Default model configuration to use if not in session
* @returns {Object} Model configuration with model, maxTokens, and temperature
*/
export function getModelConfig(session, defaults = DEFAULT_MODEL_CONFIG) {
// Get values from session or fall back to defaults
return {
model: session?.env?.MODEL || defaults.model,
maxTokens: parseInt(session?.env?.MAX_TOKENS || defaults.maxTokens),
temperature: parseFloat(session?.env?.TEMPERATURE || defaults.temperature)
};
}
/**
* Returns the best available AI model based on specified options
* @param {Object} session - Session object from MCP containing environment variables
* @param {Object} options - Options for model selection
* @param {boolean} [options.requiresResearch=false] - Whether the operation requires research capabilities
* @param {boolean} [options.claudeOverloaded=false] - Whether Claude is currently overloaded
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {Promise<Object>} Selected model info with type and client
* @throws {Error} If no AI models are available
*/
export async function getBestAvailableAIModel(
session,
options = {},
log = console
) {
const { requiresResearch = false, claudeOverloaded = false } = options;
// Test case: When research is needed but no Perplexity, use Claude
if (
requiresResearch &&
!(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY) &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
log.warn('Perplexity not available for research, using Claude');
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.error(`Claude not available: ${error.message}`);
throw new Error('No AI models available for research');
}
}
// Regular path: Perplexity for research when available
if (
requiresResearch &&
(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY)
) {
try {
const client = await getPerplexityClientForMCP(session, log);
return { type: 'perplexity', client };
} catch (error) {
log.warn(`Perplexity not available: ${error.message}`);
// Fall through to Claude as backup
}
}
// Test case: Claude for overloaded scenario
if (
claudeOverloaded &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
log.warn(
'Claude is overloaded but no alternatives are available. Proceeding with Claude anyway.'
);
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.error(
`Claude not available despite being overloaded: ${error.message}`
);
throw new Error('No AI models available');
}
}
// Default case: Use Claude when available and not overloaded
if (
!claudeOverloaded &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.warn(`Claude not available: ${error.message}`);
// Fall through to error if no other options
}
}
// If we got here, no models were successfully initialized
throw new Error('No AI models available. Please check your API keys.');
}
/**
* Handle Claude API errors with user-friendly messages
* @param {Error} error - The error from Claude API
* @returns {string} User-friendly error message
*/
export function handleClaudeError(error) {
// Check if it's a structured error response
if (error.type === 'error' && error.error) {
switch (error.error.type) {
case 'overloaded_error':
return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.';
case 'rate_limit_error':
return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.';
case 'invalid_request_error':
return 'There was an issue with the request format. If this persists, please report it as a bug.';
default:
return `Claude API error: ${error.error.message}`;
}
}
// Check for network/timeout errors
if (error.message?.toLowerCase().includes('timeout')) {
return 'The request to Claude timed out. Please try again.';
}
if (error.message?.toLowerCase().includes('network')) {
return 'There was a network error connecting to Claude. Please check your internet connection and try again.';
}
// Default error message
return `Error communicating with Claude: ${error.message}`;
}

View File

@@ -1,251 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
class AsyncOperationManager {
constructor() {
this.operations = new Map(); // Stores active operation state
this.completedOperations = new Map(); // Stores completed operations
this.maxCompletedOperations = 100; // Maximum number of completed operations to store
this.listeners = new Map(); // For potential future notifications
}
/**
* Adds an operation to be executed asynchronously.
* @param {Function} operationFn - The async function to execute (e.g., a Direct function).
* @param {Object} args - Arguments to pass to the operationFn.
* @param {Object} context - The MCP tool context { log, reportProgress, session }.
* @returns {string} The unique ID assigned to this operation.
*/
addOperation(operationFn, args, context) {
const operationId = `op-${uuidv4()}`;
const operation = {
id: operationId,
status: 'pending',
startTime: Date.now(),
endTime: null,
result: null,
error: null,
// Store necessary parts of context, especially log for background execution
log: context.log,
reportProgress: context.reportProgress, // Pass reportProgress through
session: context.session // Pass session through if needed by the operationFn
};
this.operations.set(operationId, operation);
this.log(operationId, 'info', `Operation added.`);
// Start execution in the background (don't await here)
this._runOperation(operationId, operationFn, args, context).catch((err) => {
// Catch unexpected errors during the async execution setup itself
this.log(
operationId,
'error',
`Critical error starting operation: ${err.message}`,
{ stack: err.stack }
);
operation.status = 'failed';
operation.error = {
code: 'MANAGER_EXECUTION_ERROR',
message: err.message
};
operation.endTime = Date.now();
// Move to completed operations
this._moveToCompleted(operationId);
});
return operationId;
}
/**
* Internal function to execute the operation.
* @param {string} operationId - The ID of the operation.
* @param {Function} operationFn - The async function to execute.
* @param {Object} args - Arguments for the function.
* @param {Object} context - The original MCP tool context.
*/
async _runOperation(operationId, operationFn, args, context) {
const operation = this.operations.get(operationId);
if (!operation) return; // Should not happen
operation.status = 'running';
this.log(operationId, 'info', `Operation running.`);
this.emit('statusChanged', { operationId, status: 'running' });
try {
// Pass the necessary context parts to the direct function
// The direct function needs to be adapted if it needs reportProgress
// We pass the original context's log, plus our wrapped reportProgress
const result = await operationFn(args, operation.log, {
reportProgress: (progress) =>
this._handleProgress(operationId, progress),
mcpLog: operation.log, // Pass log as mcpLog if direct fn expects it
session: operation.session
});
operation.status = result.success ? 'completed' : 'failed';
operation.result = result.success ? result.data : null;
operation.error = result.success ? null : result.error;
this.log(
operationId,
'info',
`Operation finished with status: ${operation.status}`
);
} catch (error) {
this.log(
operationId,
'error',
`Operation failed with error: ${error.message}`,
{ stack: error.stack }
);
operation.status = 'failed';
operation.error = {
code: 'OPERATION_EXECUTION_ERROR',
message: error.message
};
} finally {
operation.endTime = Date.now();
this.emit('statusChanged', {
operationId,
status: operation.status,
result: operation.result,
error: operation.error
});
// Move to completed operations if done or failed
if (operation.status === 'completed' || operation.status === 'failed') {
this._moveToCompleted(operationId);
}
}
}
/**
* Move an operation from active operations to completed operations history.
* @param {string} operationId - The ID of the operation to move.
* @private
*/
_moveToCompleted(operationId) {
const operation = this.operations.get(operationId);
if (!operation) return;
// Store only the necessary data in completed operations
const completedData = {
id: operation.id,
status: operation.status,
startTime: operation.startTime,
endTime: operation.endTime,
result: operation.result,
error: operation.error
};
this.completedOperations.set(operationId, completedData);
this.operations.delete(operationId);
// Trim completed operations if exceeding maximum
if (this.completedOperations.size > this.maxCompletedOperations) {
// Get the oldest operation (sorted by endTime)
const oldest = [...this.completedOperations.entries()].sort(
(a, b) => a[1].endTime - b[1].endTime
)[0];
if (oldest) {
this.completedOperations.delete(oldest[0]);
}
}
}
/**
* Handles progress updates from the running operation and forwards them.
* @param {string} operationId - The ID of the operation reporting progress.
* @param {Object} progress - The progress object { progress, total? }.
*/
_handleProgress(operationId, progress) {
const operation = this.operations.get(operationId);
if (operation && operation.reportProgress) {
try {
// Use the reportProgress function captured from the original context
operation.reportProgress(progress);
this.log(
operationId,
'debug',
`Reported progress: ${JSON.stringify(progress)}`
);
} catch (err) {
this.log(
operationId,
'warn',
`Failed to report progress: ${err.message}`
);
// Don't stop the operation, just log the reporting failure
}
}
}
/**
* Retrieves the status and result/error of an operation.
* @param {string} operationId - The ID of the operation.
* @returns {Object | null} The operation details or null if not found.
*/
getStatus(operationId) {
// First check active operations
const operation = this.operations.get(operationId);
if (operation) {
return {
id: operation.id,
status: operation.status,
startTime: operation.startTime,
endTime: operation.endTime,
result: operation.result,
error: operation.error
};
}
// Then check completed operations
const completedOperation = this.completedOperations.get(operationId);
if (completedOperation) {
return completedOperation;
}
// Operation not found in either active or completed
return {
error: {
code: 'OPERATION_NOT_FOUND',
message: `Operation ID ${operationId} not found. It may have been completed and removed from history, or the ID may be invalid.`
},
status: 'not_found'
};
}
/**
* Internal logging helper to prefix logs with the operation ID.
* @param {string} operationId - The ID of the operation.
* @param {'info'|'warn'|'error'|'debug'} level - Log level.
* @param {string} message - Log message.
* @param {Object} [meta] - Additional metadata.
*/
log(operationId, level, message, meta = {}) {
const operation = this.operations.get(operationId);
// Use the logger instance associated with the operation if available, otherwise console
const logger = operation?.log || console;
const logFn = logger[level] || logger.log || console.log; // Fallback
logFn(`[AsyncOp ${operationId}] ${message}`, meta);
}
// --- Basic Event Emitter ---
on(eventName, listener) {
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName).push(listener);
}
emit(eventName, data) {
if (this.listeners.has(eventName)) {
this.listeners.get(eventName).forEach((listener) => listener(data));
}
}
}
// Export a singleton instance
const asyncOperationManager = new AsyncOperationManager();
// Export the manager and potentially the class if needed elsewhere
export { asyncOperationManager, AsyncOperationManager };

View File

@@ -5,7 +5,6 @@ import { fileURLToPath } from 'url';
import fs from 'fs';
import logger from './logger.js';
import { registerTaskMasterTools } from './tools/index.js';
import { asyncOperationManager } from './core/utils/async-manager.js';
// Load environment variables
dotenv.config();
@@ -35,9 +34,6 @@ class TaskMasterMCPServer {
this.server.addResourceTemplate({});
// Make the manager accessible (e.g., pass it to tool registration)
this.asyncManager = asyncOperationManager;
// Bind methods
this.init = this.init.bind(this);
this.start = this.start.bind(this);
@@ -88,7 +84,4 @@ class TaskMasterMCPServer {
}
}
// Export the manager from here as well, if needed elsewhere
export { asyncOperationManager };
export default TaskMasterMCPServer;

View File

@@ -1,5 +1,6 @@
import chalk from 'chalk';
import { isSilentMode } from '../../scripts/modules/utils.js';
import { getLogLevel } from '../../scripts/modules/config-manager.js';
// Define log levels
const LOG_LEVELS = {
@@ -10,10 +11,8 @@ const LOG_LEVELS = {
success: 4
};
// Get log level from environment or default to info
const LOG_LEVEL = process.env.LOG_LEVEL
? (LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()] ?? LOG_LEVELS.info)
: LOG_LEVELS.info;
// Get log level from config manager or default to info
const LOG_LEVEL = LOG_LEVELS[getLogLevel().toLowerCase()] ?? LOG_LEVELS.info;
/**
* Logs a message with the specified level

View File

@@ -6,9 +6,7 @@
import { z } from 'zod';
import {
createErrorResponse,
createContentResponse,
getProjectRootFromSession,
executeTaskMasterCommand,
handleApiResult
} from './utils.js';
import { addTaskDirect } from '../core/task-master-core.js';
@@ -101,6 +99,10 @@ export function registerAddTaskTool(server) {
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
prompt: args.prompt,
title: args.title,
description: args.description,
details: args.details,
testStrategy: args.testStrategy,
dependencies: args.dependencies,
priority: args.priority,
research: args.research

View File

@@ -4,14 +4,11 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
getProjectRootFromSession
} from './utils.js';
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js';
import { handleApiResult, createErrorResponse } from './utils.js';
import { analyzeTaskComplexityDirect } from '../core/direct-functions/analyze-task-complexity.js';
import { findTasksJsonPath } from '../core/utils/path-utils.js';
import path from 'path';
import fs from 'fs';
/**
* Register the analyze tool with the MCP server
@@ -27,13 +24,7 @@ export function registerAnalyzeTool(server) {
.string()
.optional()
.describe(
'Output file path for the report (default: scripts/task-complexity-report.json)'
),
model: z
.string()
.optional()
.describe(
'LLM model to use for analysis (defaults to configured model)'
'Output file path relative to project root (default: scripts/task-complexity-report.json)'
),
threshold: z.coerce
.number()
@@ -47,12 +38,13 @@ export function registerAnalyzeTool(server) {
.string()
.optional()
.describe(
'Absolute path to the tasks file (default: tasks/tasks.json)'
'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
),
research: z
.boolean()
.optional()
.describe('Use Perplexity AI for research-backed complexity analysis'),
.default(false)
.describe('Use research role for complexity analysis'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
@@ -60,17 +52,15 @@ export function registerAnalyzeTool(server) {
execute: async (args, { log, session }) => {
try {
log.info(
`Analyzing task complexity with args: ${JSON.stringify(args)}`
`Executing analyze_project_complexity tool with args: ${JSON.stringify(args)}`
);
// Get project root from args or session
const rootFolder =
args.projectRoot || getProjectRootFromSession(session, log);
const rootFolder = args.projectRoot;
if (!rootFolder) {
return createErrorResponse(
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
);
return createErrorResponse('projectRoot is required.');
}
if (!path.isAbsolute(rootFolder)) {
return createErrorResponse('projectRoot must be an absolute path.');
}
let tasksJsonPath;
@@ -82,7 +72,7 @@ export function registerAnalyzeTool(server) {
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
`Failed to find tasks.json within project root '${rootFolder}': ${error.message}`
);
}
@@ -90,11 +80,25 @@ export function registerAnalyzeTool(server) {
? path.resolve(rootFolder, args.output)
: path.resolve(rootFolder, 'scripts', 'task-complexity-report.json');
const outputDir = path.dirname(outputPath);
try {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
log.info(`Created output directory: ${outputDir}`);
}
} catch (dirError) {
log.error(
`Failed to create output directory ${outputDir}: ${dirError.message}`
);
return createErrorResponse(
`Failed to create output directory: ${dirError.message}`
);
}
const result = await analyzeTaskComplexityDirect(
{
tasksJsonPath: tasksJsonPath,
outputPath: outputPath,
model: args.model,
threshold: args.threshold,
research: args.research
},
@@ -103,20 +107,17 @@ export function registerAnalyzeTool(server) {
);
if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`);
log.info(
`Report summary: ${JSON.stringify(result.data.reportSummary)}`
);
log.info(`Tool analyze_project_complexity finished successfully.`);
} else {
log.error(
`Failed to analyze task complexity: ${result.error.message}`
`Tool analyze_project_complexity failed: ${result.error?.message || 'Unknown error'}`
);
}
return handleApiResult(result, log, 'Error analyzing task complexity');
} catch (error) {
log.error(`Error in analyze tool: ${error.message}`);
return createErrorResponse(error.message);
log.error(`Critical error in analyze tool execute: ${error.message}`);
return createErrorResponse(`Internal tool error: ${error.message}`);
}
}
});

View File

@@ -19,22 +19,27 @@ import { findTasksJsonPath } from '../core/utils/path-utils.js';
export function registerExpandAllTool(server) {
server.addTool({
name: 'expand_all',
description: 'Expand all pending tasks into subtasks',
description:
'Expand all pending tasks into subtasks based on complexity or defaults',
parameters: z.object({
num: z
.string()
.optional()
.describe('Number of subtasks to generate for each task'),
.describe(
'Target number of subtasks per task (uses complexity/defaults otherwise)'
),
research: z
.boolean()
.optional()
.describe(
'Enable Perplexity AI for research-backed subtask generation'
'Enable research-backed subtask generation (e.g., using Perplexity)'
),
prompt: z
.string()
.optional()
.describe('Additional context to guide subtask generation'),
.describe(
'Additional context to guide subtask generation for all tasks'
),
force: z
.boolean()
.optional()
@@ -45,34 +50,37 @@ export function registerExpandAllTool(server) {
.string()
.optional()
.describe(
'Absolute path to the tasks file (default: tasks/tasks.json)'
'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
.optional()
.describe(
'Absolute path to the project root directory (derived from session if possible)'
)
}),
execute: async (args, { log, session }) => {
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
log.info(
`Tool expand_all execution started with args: ${JSON.stringify(args)}`
);
// Get project root from args or session
const rootFolder =
args.projectRoot || getProjectRootFromSession(session, log);
// Ensure project root was determined
const rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder) {
log.error('Could not determine project root from session.');
return createErrorResponse(
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
'Could not determine project root from session.'
);
}
log.info(`Project root determined: ${rootFolder}`);
// Resolve the path to tasks.json
let tasksJsonPath;
try {
tasksJsonPath = findTasksJsonPath(
{ projectRoot: rootFolder, file: args.file },
log
);
log.info(`Resolved tasks.json path: ${tasksJsonPath}`);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
@@ -82,9 +90,7 @@ export function registerExpandAllTool(server) {
const result = await expandAllTasksDirect(
{
// Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
num: args.num,
research: args.research,
prompt: args.prompt,
@@ -94,18 +100,17 @@ export function registerExpandAllTool(server) {
{ session }
);
if (result.success) {
log.info(`Successfully expanded all tasks: ${result.data.message}`);
} else {
log.error(
`Failed to expand all tasks: ${result.error?.message || 'Unknown error'}`
);
}
return handleApiResult(result, log, 'Error expanding all tasks');
} catch (error) {
log.error(`Error in expand-all tool: ${error.message}`);
return createErrorResponse(error.message);
log.error(
`Unexpected error in expand_all tool execute: ${error.message}`
);
if (error.stack) {
log.error(error.stack);
}
return createErrorResponse(
`An unexpected error occurred: ${error.message}`
);
}
}
});

View File

@@ -11,8 +11,6 @@ import {
} from './utils.js';
import { expandTaskDirect } from '../core/task-master-core.js';
import { findTasksJsonPath } from '../core/utils/path-utils.js';
import fs from 'fs';
import path from 'path';
/**
* Register the expand-task tool with the MCP server
@@ -28,16 +26,26 @@ export function registerExpandTaskTool(server) {
research: z
.boolean()
.optional()
.describe('Use Perplexity AI for research-backed generation'),
.default(false)
.describe('Use research role for generation'),
prompt: z
.string()
.optional()
.describe('Additional context for subtask generation'),
file: z.string().optional().describe('Absolute path to the tasks file'),
file: z
.string()
.optional()
.describe(
'Path to the tasks file relative to project root (e.g., tasks/tasks.json)'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.'),
force: z.boolean().optional().describe('Force the expansion')
force: z
.boolean()
.optional()
.default(false)
.describe('Force expansion even if subtasks exist')
}),
execute: async (args, { log, session }) => {
try {

View File

@@ -43,6 +43,10 @@ export function registerShowTaskTool(server) {
description: 'Get detailed information about a specific task',
parameters: z.object({
id: z.string().describe('Task ID to get'),
status: z
.string()
.optional()
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
file: z.string().optional().describe('Absolute path to the tasks file'),
complexityReport: z
.string()
@@ -61,11 +65,9 @@ export function registerShowTaskTool(server) {
); // Use JSON.stringify for better visibility
try {
log.info(`Getting task details for ID: ${args.id}`);
log.info(
`Session object received in execute: ${JSON.stringify(session)}`
); // Use JSON.stringify for better visibility
`Getting task details for ID: ${args.id}${args.status ? ` (filtering subtasks by status: ${args.status})` : ''}`
);
// Get project root from args or session
const rootFolder =
@@ -111,11 +113,11 @@ export function registerShowTaskTool(server) {
}
const result = await showTaskDirect(
{
// Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath,
// Pass other relevant args
id: args.id
id: args.id,
status: args.status
},
log
);

View File

@@ -27,39 +27,51 @@ 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 { asyncOperationManager } from '../core/utils/async-manager.js';
import { registerModelsTool } from './models.js';
/**
* Register all Task Master tools with the MCP server
* @param {Object} server - FastMCP server instance
* @param {asyncOperationManager} asyncManager - The async operation manager instance
*/
export function registerTaskMasterTools(server, asyncManager) {
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, asyncManager);
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

@@ -1,41 +1,13 @@
import { z } from 'zod';
import {
createContentResponse,
createErrorResponse,
handleApiResult
} from './utils.js';
import { createErrorResponse, handleApiResult } from './utils.js';
import { initializeProjectDirect } from '../core/task-master-core.js';
export function registerInitializeProjectTool(server) {
server.addTool({
name: 'initialize_project',
description:
"Initializes a new Task Master project structure by calling the core initialization logic. Derives target directory from client session. If project details (name, description, author) are not provided, prompts the user or skips if 'yes' flag is true. DO NOT run without parameters.",
'Initializes a new Task Master project structure by calling the core initialization logic. Creates necessary folders and configuration files for Task Master in the current directory.',
parameters: z.object({
projectName: z
.string()
.optional()
.describe(
'The name for the new project. If not provided, prompt the user for it.'
),
projectDescription: z
.string()
.optional()
.describe(
'A brief description for the project. If not provided, prompt the user for it.'
),
projectVersion: z
.string()
.optional()
.describe(
"The initial version for the project (e.g., '0.1.0'). User input not needed unless user requests to override."
),
authorName: z
.string()
.optional()
.describe(
"The author's name. User input not needed unless user requests to override."
),
skipInstall: z
.boolean()
.optional()
@@ -47,15 +19,13 @@ export function registerInitializeProjectTool(server) {
.boolean()
.optional()
.default(false)
.describe(
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
),
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
yes: z
.boolean()
.optional()
.default(false)
.default(true)
.describe(
"Skip prompts and use default values or provided arguments. Use true if you wish to skip details like the project name, etc. If the project information required for the initialization is not available or provided by the user, prompt if the user wishes to provide them (name, description, author) or skip them. If the user wishes to skip, set the 'yes' flag to true and do not set any other parameters."
'Skip prompts and use default values. Always set to true for MCP tools.'
),
projectRoot: z
.string()

View File

@@ -0,0 +1,89 @@
/**
* 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.'),
openrouter: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom OpenRouter model.'),
ollama: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom Ollama model.')
}),
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

@@ -10,11 +10,7 @@ import {
createErrorResponse
} from './utils.js';
import { parsePRDDirect } from '../core/task-master-core.js';
import {
resolveProjectPaths,
findPRDDocumentPath,
resolveTasksOutputPath
} from '../core/utils/path-utils.js';
import { resolveProjectPaths } from '../core/utils/path-utils.js';
/**
* Register the parsePRD tool with the MCP server
@@ -47,6 +43,12 @@ export function registerParsePRDTool(server) {
.boolean()
.optional()
.describe('Allow overwriting an existing tasks.json file.'),
append: z
.boolean()
.optional()
.describe(
'Append new tasks to existing tasks.json instead of overwriting'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be absolute path.')
@@ -86,7 +88,8 @@ export function registerParsePRDTool(server) {
input: prdPath,
output: tasksJsonPath,
numTasks: args.numTasks,
force: args.force
force: args.force,
append: args.append
},
log,
{ session }

View File

@@ -23,7 +23,9 @@ export function registerRemoveTaskTool(server) {
parameters: z.object({
id: z
.string()
.describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"),
.describe(
"ID of the task or subtask to remove (e.g., '5' or '5.2'). Can be comma-separated to update multiple tasks/subtasks at once."
),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@@ -35,7 +37,7 @@ export function registerRemoveTaskTool(server) {
}),
execute: async (args, { log, session }) => {
try {
log.info(`Removing task with ID: ${args.id}`);
log.info(`Removing task(s) with ID(s): ${args.id}`);
// Get project root from args or session
const rootFolder =

View File

@@ -24,7 +24,7 @@ export function registerSetTaskStatusTool(server) {
id: z
.string()
.describe(
"Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated for multiple updates."
"Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once."
),
status: z
.string()

View File

@@ -20,12 +20,12 @@ export function registerUpdateSubtaskTool(server) {
server.addTool({
name: 'update_subtask',
description:
'Appends additional information to a specific subtask without replacing existing content',
'Appends timestamped information to a specific subtask without replacing existing content',
parameters: z.object({
id: z
.string()
.describe(
'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2")'
'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask.'
),
prompt: z.string().describe('Information to add to the subtask'),
research: z

View File

@@ -24,7 +24,9 @@ export function registerUpdateTaskTool(server) {
parameters: z.object({
id: z
.string()
.describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
.describe(
"ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool."
),
prompt: z
.string()
.describe('New information or context to incorporate into the task'),

View File

@@ -4,13 +4,10 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
getProjectRootFromSession
} from './utils.js';
import { handleApiResult, createErrorResponse } from './utils.js';
import { updateTasksDirect } from '../core/task-master-core.js';
import { findTasksJsonPath } from '../core/utils/path-utils.js';
import path from 'path';
/**
* Register the update tool with the MCP server
@@ -20,7 +17,7 @@ export function registerUpdateTool(server) {
server.addTool({
name: 'update',
description:
"Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task.",
"Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task or 'update_subtask' for subtasks.",
parameters: z.object({
from: z
.string()
@@ -41,26 +38,25 @@ export function registerUpdateTool(server) {
}),
execute: async (args, { log, session }) => {
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
log.info(`Executing update 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) {
// 1. Get Project Root
const rootFolder = args.projectRoot;
if (!rootFolder || !path.isAbsolute(rootFolder)) {
return createErrorResponse(
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
'projectRoot is required and must be absolute.'
);
}
log.info(`Project root: ${rootFolder}`);
// Resolve the path to tasks.json
// 2. Resolve Path
let tasksJsonPath;
try {
tasksJsonPath = findTasksJsonPath(
{ projectRoot: rootFolder, file: args.file },
log
);
log.info(`Resolved tasks path: ${tasksJsonPath}`);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
@@ -68,6 +64,7 @@ export function registerUpdateTool(server) {
);
}
// 3. Call Direct Function
const result = await updateTasksDirect(
{
tasksJsonPath: tasksJsonPath,
@@ -79,20 +76,12 @@ export function registerUpdateTool(server) {
{ session }
);
if (result.success) {
log.info(
`Successfully updated tasks from ID ${args.from}: ${result.data.message}`
);
} else {
log.error(
`Failed to update tasks: ${result.error?.message || 'Unknown error'}`
);
}
// 4. Handle Result
log.info(`updateTasksDirect result: success=${result.success}`);
return handleApiResult(result, log, 'Error updating tasks');
} catch (error) {
log.error(`Error in update tool: ${error.message}`);
return createErrorResponse(error.message);
log.error(`Critical error in update tool execute: ${error.message}`);
return createErrorResponse(`Internal tool error: ${error.message}`);
}
}
});

View File

@@ -443,7 +443,7 @@ function createContentResponse(content) {
* @param {string} errorMessage - Error message to include in response
* @returns {Object} - Error content response object in FastMCP format
*/
export function createErrorResponse(errorMessage) {
function createErrorResponse(errorMessage) {
return {
content: [
{
@@ -455,6 +455,25 @@ export function createErrorResponse(errorMessage) {
};
}
/**
* Creates a logger wrapper object compatible with core function expectations.
* Adapts the MCP logger to the { info, warn, error, debug, success } structure.
* @param {Object} log - The MCP logger instance.
* @returns {Object} - The logger wrapper object.
*/
function createLogWrapper(log) {
return {
info: (message, ...args) => log.info(message, ...args),
warn: (message, ...args) => log.warn(message, ...args),
error: (message, ...args) => log.error(message, ...args),
// Handle optional debug method
debug: (message, ...args) =>
log.debug ? log.debug(message, ...args) : null,
// Map success to info as a common fallback
success: (message, ...args) => log.info(message, ...args)
};
}
// Ensure all functions are exported
export {
getProjectRoot,
@@ -463,5 +482,7 @@ export {
executeTaskMasterCommand,
getCachedOrExecute,
processMCPResponseData,
createContentResponse
createContentResponse,
createErrorResponse,
createLogWrapper
};