Revert "Release 0.13.0"
This commit is contained in:
@@ -8,7 +8,15 @@ import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getModelConfig
|
||||
} from '../utils/ai-client-utils.js';
|
||||
import {
|
||||
_buildAddTaskPrompt,
|
||||
parseTaskJsonResponse,
|
||||
_handleAnthropicStream
|
||||
} from '../../../../scripts/modules/ai-services.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for adding a new task with error handling.
|
||||
@@ -21,32 +29,20 @@ import { createLogWrapper } from '../../tools/utils.js';
|
||||
* @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.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
|
||||
* @param {string} [args.file='tasks/tasks.json'] - Path to the tasks file
|
||||
* @param {string} [args.projectRoot] - Project root directory
|
||||
* @param {boolean} [args.research=false] - Whether to use research capabilities for task creation
|
||||
* @param {string} [args.projectRoot] - Project root path
|
||||
* @param {Object} log - Logger object
|
||||
* @param {Object} context - Additional context (session)
|
||||
* @param {Object} context - Additional context (reportProgress, 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 (including research and projectRoot)
|
||||
const {
|
||||
tasksJsonPath,
|
||||
prompt,
|
||||
dependencies,
|
||||
priority,
|
||||
research,
|
||||
projectRoot
|
||||
} = args;
|
||||
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);
|
||||
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, prompt, dependencies, priority, research } = args;
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
log.error('addTaskDirect called without tasksJsonPath');
|
||||
@@ -83,17 +79,20 @@ export async function addTaskDirect(args, log, context = {}) {
|
||||
}
|
||||
|
||||
// Extract and prepare parameters
|
||||
const taskPrompt = prompt;
|
||||
const taskDependencies = Array.isArray(dependencies)
|
||||
? dependencies // Already an array if passed directly
|
||||
: dependencies // Check if dependencies exist and are a string
|
||||
? dependencies
|
||||
: dependencies
|
||||
? String(dependencies)
|
||||
.split(',')
|
||||
.map((id) => parseInt(id.trim(), 10)) // Split, trim, and parse
|
||||
: []; // Default to empty array if null/undefined
|
||||
const taskPriority = priority || 'medium'; // Default priority
|
||||
.map((id) => parseInt(id.trim(), 10))
|
||||
: [];
|
||||
const taskPriority = priority || 'medium';
|
||||
|
||||
// Extract context parameters for advanced functionality
|
||||
const { session } = context;
|
||||
|
||||
let manualTaskData = null;
|
||||
let newTaskId;
|
||||
|
||||
if (isManualCreation) {
|
||||
// Create manual task data object
|
||||
@@ -109,64 +108,150 @@ export async function addTaskDirect(args, log, context = {}) {
|
||||
);
|
||||
|
||||
// Call the addTask function with manual task data
|
||||
newTaskId = await addTask(
|
||||
const newTaskId = await addTask(
|
||||
tasksPath,
|
||||
null, // prompt is null for manual creation
|
||||
null, // No prompt needed for manual creation
|
||||
taskDependencies,
|
||||
taskPriority,
|
||||
priority,
|
||||
{
|
||||
session,
|
||||
mcpLog,
|
||||
projectRoot
|
||||
mcpLog: log,
|
||||
session
|
||||
},
|
||||
'json', // outputFormat
|
||||
manualTaskData, // Pass the manual task data
|
||||
false, // research flag is false for manual creation
|
||||
projectRoot // Pass projectRoot
|
||||
'json', // Use JSON output format to prevent console output
|
||||
null, // No custom environment
|
||||
manualTaskData // Pass the manual task data
|
||||
);
|
||||
|
||||
// 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: ${taskPriority}, research: ${research}`
|
||||
`Adding new task with prompt: "${prompt}", dependencies: [${taskDependencies.join(', ')}], priority: ${priority}`
|
||||
);
|
||||
|
||||
// Call the addTask function, passing the research flag
|
||||
newTaskId = await addTask(
|
||||
tasksPath,
|
||||
prompt, // Use the prompt for AI creation
|
||||
taskDependencies,
|
||||
taskPriority,
|
||||
{
|
||||
session,
|
||||
mcpLog,
|
||||
projectRoot
|
||||
},
|
||||
'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}`
|
||||
// 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(
|
||||
tasksPath,
|
||||
prompt,
|
||||
taskDependencies,
|
||||
priority,
|
||||
{
|
||||
mcpLog: log,
|
||||
session
|
||||
},
|
||||
'json',
|
||||
null,
|
||||
taskDataFromAI // Pass the parsed AI result as the manual task data
|
||||
);
|
||||
|
||||
// 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: error.code || 'ADD_TASK_ERROR', // Use error code if available
|
||||
code: 'ADD_TASK_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,38 +2,37 @@
|
||||
* Direct function wrapper for analyzeTaskComplexity
|
||||
*/
|
||||
|
||||
import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
|
||||
import { analyzeTaskComplexity } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
isSilentMode,
|
||||
readJSON
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 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 {string} [args.projectRoot] - Project root path.
|
||||
* @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;
|
||||
const { tasksJsonPath, outputPath, threshold, research, projectRoot } = args;
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, outputPath, model, threshold, research } = args;
|
||||
|
||||
const logWrapper = createLogWrapper(log);
|
||||
|
||||
// --- 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 {
|
||||
@@ -52,6 +51,7 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Use the provided paths
|
||||
const tasksPath = tasksJsonPath;
|
||||
const resolvedOutputPath = outputPath;
|
||||
|
||||
@@ -59,93 +59,78 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
||||
log.info(`Output report will be saved to: ${resolvedOutputPath}`);
|
||||
|
||||
if (research) {
|
||||
log.info('Using research role for complexity analysis');
|
||||
log.info('Using Perplexity AI for research-backed complexity analysis');
|
||||
}
|
||||
|
||||
// Prepare options for the core function - REMOVED mcpLog and session here
|
||||
const coreOptions = {
|
||||
file: tasksJsonPath,
|
||||
output: outputPath,
|
||||
// Create options object for analyzeTaskComplexity using provided paths
|
||||
const options = {
|
||||
file: tasksPath,
|
||||
output: resolvedOutputPath,
|
||||
model: model,
|
||||
threshold: threshold,
|
||||
research: research === true, // Ensure boolean
|
||||
projectRoot: projectRoot // Pass projectRoot here
|
||||
research: research === true
|
||||
};
|
||||
// --- End Initial Checks ---
|
||||
|
||||
// --- Silent Mode and Logger Wrapper ---
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode(); // Still enable silent mode as a backup
|
||||
enableSilentMode();
|
||||
}
|
||||
|
||||
let report;
|
||||
// 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
|
||||
};
|
||||
|
||||
try {
|
||||
// --- Call Core Function (Pass context separately) ---
|
||||
// Pass coreOptions as the first argument
|
||||
// Pass context object { session, mcpLog } as the second argument
|
||||
report = await analyzeTaskComplexity(
|
||||
coreOptions, // Pass options object
|
||||
{ session, mcpLog: logWrapper } // Pass context object
|
||||
// Removed the explicit 'json' format argument, assuming context handling is sufficient
|
||||
// If issues persist, we might need to add an explicit format param to analyzeTaskComplexity
|
||||
);
|
||||
// Call the core function with session and logWrapper as mcpLog
|
||||
await analyzeTaskComplexity(options, {
|
||||
session,
|
||||
mcpLog: logWrapper // Use the wrapper instead of passing log directly
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error in analyzeTaskComplexity core function: ${error.message}`
|
||||
);
|
||||
// Restore logging if we changed it
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
log.error(`Error in analyzeTaskComplexity: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_CORE_ERROR',
|
||||
message: `Error running core complexity analysis: ${error.message}`
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: `Error running complexity analysis: ${error.message}`
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Always restore normal logging in finally block if we enabled silent mode
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
// Always restore normal logging in finally block, but only if we enabled it
|
||||
if (!wasSilent) {
|
||||
disableSilentMode();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Result Handling (remains largely the same) ---
|
||||
// Verify the report file was created (core function writes it)
|
||||
// Verify the report file was created
|
||||
if (!fs.existsSync(resolvedOutputPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'ANALYZE_REPORT_MISSING', // Specific code
|
||||
message:
|
||||
'Analysis completed but no report file was created at the expected path.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Added a check to ensure report is defined before accessing its properties
|
||||
if (!report || typeof report !== 'object') {
|
||||
log.error(
|
||||
'Core analysis function returned an invalid or undefined response.'
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_CORE_RESPONSE',
|
||||
message: 'Core analysis function returned an invalid response.'
|
||||
code: 'ANALYZE_ERROR',
|
||||
message: 'Analysis completed but no report file was created'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Read the report file
|
||||
let report;
|
||||
try {
|
||||
// Ensure complexityAnalysis exists and is an array
|
||||
const analysisArray = Array.isArray(report.complexityAnalysis)
|
||||
? report.complexityAnalysis
|
||||
: [];
|
||||
report = JSON.parse(fs.readFileSync(resolvedOutputPath, 'utf8'));
|
||||
|
||||
// Count tasks by complexity (remains the same)
|
||||
// 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
|
||||
const highComplexityTasks = analysisArray.filter(
|
||||
(t) => t.complexityScore >= 8
|
||||
).length;
|
||||
@@ -159,40 +144,37 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Task complexity analysis complete. Report saved to ${outputPath}`, // Use outputPath from args
|
||||
reportPath: outputPath, // Use outputPath from args
|
||||
message: `Task complexity analysis complete. Report saved to ${resolvedOutputPath}`,
|
||||
reportPath: resolvedOutputPath,
|
||||
reportSummary: {
|
||||
taskCount: analysisArray.length,
|
||||
highComplexityTasks,
|
||||
mediumComplexityTasks,
|
||||
lowComplexityTasks
|
||||
},
|
||||
fullReport: report // Now includes the full report
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (parseError) {
|
||||
// Should not happen if core function returns object, but good safety check
|
||||
log.error(`Internal error processing report data: ${parseError.message}`);
|
||||
log.error(`Error parsing report file: ${parseError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'REPORT_PROCESS_ERROR',
|
||||
message: `Internal error processing complexity report: ${parseError.message}`
|
||||
code: 'REPORT_PARSE_ERROR',
|
||||
message: `Error parsing complexity report: ${parseError.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
// --- End Result Handling ---
|
||||
} catch (error) {
|
||||
// Catch errors from initial checks or path resolution
|
||||
// Make sure to restore normal logging if silent mode was enabled
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
if (isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
|
||||
|
||||
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECT_FUNCTION_SETUP_ERROR',
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.
|
||||
|
||||
@@ -5,86 +5,138 @@
|
||||
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import { getAnthropicClientForMCP } from '../utils/ai-client-utils.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* Expand all pending tasks with subtasks (Direct Function Wrapper)
|
||||
* Expand all pending tasks with subtasks
|
||||
* @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 research-backed subtask generation
|
||||
* @param {boolean} [args.research] - Enable Perplexity AI for 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 {string} [args.projectRoot] - Project root path.
|
||||
* @param {Object} log - Logger object from FastMCP
|
||||
* @param {Object} log - Logger object
|
||||
* @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; // Extract session
|
||||
// Destructure expected args, including projectRoot
|
||||
const { tasksJsonPath, num, research, prompt, force, projectRoot } = args;
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, num, research, prompt, force } = args;
|
||||
|
||||
// Create logger wrapper using the utility
|
||||
const mcpLog = createLogWrapper(log);
|
||||
|
||||
if (!tasksJsonPath) {
|
||||
log.error('expandAllTasksDirect called without tasksJsonPath');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
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, projectRoot })}`
|
||||
);
|
||||
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Parse parameters (ensure correct types)
|
||||
const numSubtasks = num ? parseInt(num, 10) : undefined;
|
||||
const useResearch = research === true;
|
||||
const additionalContext = prompt || '';
|
||||
const forceFlag = force === true;
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
log.error('expandAllTasksDirect called without tasksJsonPath');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'tasksJsonPath is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Call the core function, passing options and the context object { session, mcpLog, projectRoot }
|
||||
const result = await expandAllTasks(
|
||||
tasksJsonPath,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
forceFlag,
|
||||
{ session, mcpLog, projectRoot }
|
||||
);
|
||||
// Enable silent mode early to prevent any console output
|
||||
enableSilentMode();
|
||||
|
||||
// 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
|
||||
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}`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
} 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); }
|
||||
|
||||
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}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Direct function implementation for expanding a task into subtasks
|
||||
*/
|
||||
|
||||
import expandTask from '../../../../scripts/modules/task-manager/expand-task.js';
|
||||
import { expandTask } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
readJSON,
|
||||
writeJSON,
|
||||
@@ -11,9 +11,12 @@ 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.
|
||||
@@ -22,19 +25,17 @@ import { createLogWrapper } from '../../tools/utils.js';
|
||||
* @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 research role for subtask generation.
|
||||
* @param {boolean} [args.research] - Enable Perplexity AI for research-backed subtask generation.
|
||||
* @param {string} [args.prompt] - Additional context to guide subtask generation.
|
||||
* @param {boolean} [args.force] - Force expansion even if subtasks exist.
|
||||
* @param {string} [args.projectRoot] - Project root directory.
|
||||
* @param {Object} log - Logger object
|
||||
* @param {Object} context - Context object containing session
|
||||
* @param {Object} [context.session] - MCP Session object
|
||||
* @param {Object} context - Context object containing session and reportProgress
|
||||
* @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; // Extract session
|
||||
// Destructure expected args, including projectRoot
|
||||
const { tasksJsonPath, id, num, research, prompt, force, projectRoot } = args;
|
||||
const { session } = context;
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, id, num, research, prompt, force } = args;
|
||||
|
||||
// Log session root data for debugging
|
||||
log.info(
|
||||
@@ -84,9 +85,28 @@ 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}, Force: ${forceFlag}`
|
||||
`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}`
|
||||
);
|
||||
|
||||
// Read tasks data
|
||||
@@ -182,29 +202,23 @@ 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);
|
||||
|
||||
let wasSilent; // Declare wasSilent outside the try block
|
||||
// Process the request
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
wasSilent = isSilentMode(); // Assign inside the try block
|
||||
if (!wasSilent) enableSilentMode();
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core expandTask function with the wrapped logger and projectRoot
|
||||
const updatedTaskResult = await expandTask(
|
||||
// Call expandTask with session context to ensure AI client is properly initialized
|
||||
const result = await expandTask(
|
||||
tasksPath,
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
{ mcpLog, session, projectRoot },
|
||||
forceFlag
|
||||
{ mcpLog: log, session } // Only pass mcpLog and session, NOT reportProgress
|
||||
);
|
||||
|
||||
// Restore normal logging
|
||||
if (!wasSilent && isSilentMode()) disableSilentMode();
|
||||
disableSilentMode();
|
||||
|
||||
// Read the updated data
|
||||
const updatedData = readJSON(tasksPath);
|
||||
@@ -230,7 +244,7 @@ export async function expandTaskDirect(args, log, context = {}) {
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
if (!wasSilent && isSilentMode()) disableSilentMode();
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error expanding task: ${error.message}`);
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for generateTaskFiles with error handling.
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
disableSilentMode
|
||||
// isSilentMode // Not used directly here
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { getProjectRootFromSession } from '../../tools/utils.js'; // Adjust path if necessary
|
||||
import os from 'os'; // Import os module for home directory check
|
||||
|
||||
/**
|
||||
@@ -15,32 +16,60 @@ import os from 'os'; // Import os module for home directory check
|
||||
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
||||
*/
|
||||
export async function initializeProjectDirect(args, log, context = {}) {
|
||||
const { session } = context; // Keep session if core logic needs it
|
||||
const { session } = context;
|
||||
const homeDir = os.homedir();
|
||||
let targetDirectory = null;
|
||||
|
||||
log.info(
|
||||
`CONTEXT received in direct function: ${context ? JSON.stringify(Object.keys(context)) : 'MISSING or Falsy'}`
|
||||
);
|
||||
log.info(
|
||||
`SESSION extracted in direct function: ${session ? 'Exists' : 'MISSING or Falsy'}`
|
||||
);
|
||||
log.info(`Args received in direct function: ${JSON.stringify(args)}`);
|
||||
|
||||
// --- Determine Target Directory ---
|
||||
// TRUST the projectRoot passed from the tool layer via args
|
||||
// The HOF in the tool layer already normalized and validated it came from a reliable source (args or session)
|
||||
const targetDirectory = args.projectRoot;
|
||||
|
||||
// --- Validate the targetDirectory (basic sanity checks) ---
|
||||
// 1. Prioritize projectRoot passed directly in args
|
||||
// Ensure it's not null, '/', or the home directory
|
||||
if (
|
||||
!targetDirectory ||
|
||||
typeof targetDirectory !== 'string' || // Ensure it's a string
|
||||
targetDirectory === '/' ||
|
||||
targetDirectory === homeDir
|
||||
args.projectRoot &&
|
||||
args.projectRoot !== '/' &&
|
||||
args.projectRoot !== homeDir
|
||||
) {
|
||||
log.error(
|
||||
`Invalid target directory received from tool layer: '${targetDirectory}'`
|
||||
log.info(`Using projectRoot directly from args: ${args.projectRoot}`);
|
||||
targetDirectory = args.projectRoot;
|
||||
} else {
|
||||
// 2. If args.projectRoot is missing or invalid, THEN try session (as a fallback)
|
||||
log.warn(
|
||||
`args.projectRoot ('${args.projectRoot}') is missing or invalid. Attempting to derive from session.`
|
||||
);
|
||||
const sessionDerivedPath = getProjectRootFromSession(session, log);
|
||||
// Validate the session-derived path as well
|
||||
if (
|
||||
sessionDerivedPath &&
|
||||
sessionDerivedPath !== '/' &&
|
||||
sessionDerivedPath !== homeDir
|
||||
) {
|
||||
log.info(
|
||||
`Using project root derived from session: ${sessionDerivedPath}`
|
||||
);
|
||||
targetDirectory = sessionDerivedPath;
|
||||
} else {
|
||||
log.error(
|
||||
`Could not determine a valid project root. args.projectRoot='${args.projectRoot}', sessionDerivedPath='${sessionDerivedPath}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validate the final targetDirectory
|
||||
if (!targetDirectory) {
|
||||
// This error now covers cases where neither args.projectRoot nor session provided a valid path
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TARGET_DIRECTORY',
|
||||
message: `Cannot initialize project: Invalid target directory '${targetDirectory}' received. Please ensure a valid workspace/folder is open or specified.`,
|
||||
details: `Received args.projectRoot: ${args.projectRoot}` // Show what was received
|
||||
message: `Cannot initialize project: Could not determine a valid target directory. Please ensure a workspace/folder is open or specify projectRoot.`,
|
||||
details: `Attempted args.projectRoot: ${args.projectRoot}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
@@ -57,12 +86,11 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
||||
log.info(
|
||||
`Temporarily changing CWD to ${targetDirectory} for initialization.`
|
||||
);
|
||||
process.chdir(targetDirectory); // Change CWD to the HOF-provided root
|
||||
process.chdir(targetDirectory); // Change CWD to the *validated* targetDirectory
|
||||
|
||||
enableSilentMode();
|
||||
enableSilentMode(); // Enable silent mode BEFORE calling the core function
|
||||
try {
|
||||
// Construct options ONLY from the relevant flags in args
|
||||
// The core initializeProject operates in the current CWD, which we just set
|
||||
// Always force yes: true when called via MCP to avoid interactive prompts
|
||||
const options = {
|
||||
aliases: args.addAliases,
|
||||
skipInstall: args.skipInstall,
|
||||
@@ -72,11 +100,12 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
||||
log.info(`Initializing project with options: ${JSON.stringify(options)}`);
|
||||
const result = await initializeProject(options); // Call core logic
|
||||
|
||||
// Format success result for handleApiResult
|
||||
resultData = {
|
||||
message: 'Project initialized successfully.',
|
||||
next_step:
|
||||
'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files (tasks folder will be created when parse-prd is run). The parse-prd tool will require a prd.txt file as input (typically found in the project root directory, scripts/ directory). You can create a prd.txt file by asking the user about their idea, and then using the scripts/example_prd.txt file as a template to genrate a prd.txt file in scripts/. You may skip all of this if the user already has a prd.txt file. You can THEN use the parse-prd tool to create the tasks. So: step 1 after initialization is to create a prd.txt file in scripts/prd.txt or confirm the user already has one. Step 2 is to use the parse-prd tool to create the tasks. Do not bother looking for tasks after initialization, just use the parse-prd tool to create the tasks after creating a prd.txt from which to parse the tasks. You do NOT need to reinitialize the project to parse-prd.',
|
||||
...result
|
||||
...result // Include details returned by initializeProject
|
||||
};
|
||||
success = true;
|
||||
log.info(
|
||||
@@ -91,11 +120,12 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
||||
};
|
||||
success = false;
|
||||
} finally {
|
||||
disableSilentMode();
|
||||
disableSilentMode(); // ALWAYS disable silent mode in finally
|
||||
log.info(`Restoring original CWD: ${originalCwd}`);
|
||||
process.chdir(originalCwd);
|
||||
process.chdir(originalCwd); // Change back to original CWD
|
||||
}
|
||||
|
||||
// Return in format expected by handleApiResult
|
||||
if (success) {
|
||||
return { success: true, data: resultData, fromCache: false };
|
||||
} else {
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -71,34 +71,24 @@ export async function nextTaskDirect(args, log) {
|
||||
data: {
|
||||
message:
|
||||
'No eligible next task found. All tasks are either completed or have unsatisfied dependencies',
|
||||
nextTask: null
|
||||
nextTask: null,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a subtask
|
||||
const isSubtask =
|
||||
typeof nextTask.id === 'string' && nextTask.id.includes('.');
|
||||
|
||||
const taskOrSubtask = isSubtask ? 'subtask' : 'task';
|
||||
|
||||
const additionalAdvice = isSubtask
|
||||
? 'Subtasks can be updated with timestamped details as you implement them. This is useful for tracking progress, marking milestones and insights (of successful or successive falures in attempting to implement the subtask). Research can be used when updating the subtask to collect up-to-date information, and can be helpful to solve a repeating problem the agent is unable to solve. It is a good idea to get-task the parent task to collect the overall context of the task, and to get-task the subtask to collect the specific details of the subtask.'
|
||||
: 'Tasks can be updated to reflect a change in the direction of the task, or to reformulate the task per your prompt. Research can be used when updating the task to collect up-to-date information. It is best to update subtasks as you work on them, and to update the task for more high-level changes that may affect pending subtasks or the general direction of the task.';
|
||||
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Return the next task data with the full tasks array for reference
|
||||
log.info(
|
||||
`Successfully found next task ${nextTask.id}: ${nextTask.title}. Is subtask: ${isSubtask}`
|
||||
`Successfully found next task ${nextTask.id}: ${nextTask.title}`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
nextTask,
|
||||
isSubtask,
|
||||
nextSteps: `When ready to work on the ${taskOrSubtask}, use set-status to set the status to "in progress" ${additionalAdvice}`
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,173 +8,208 @@ import fs from 'fs';
|
||||
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getModelConfig
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for parsing PRD documents and generating tasks.
|
||||
*
|
||||
* @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options.
|
||||
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output 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;
|
||||
// Extract projectRoot from args
|
||||
const {
|
||||
input: inputArg,
|
||||
output: outputArg,
|
||||
numTasks: numTasksArg,
|
||||
force,
|
||||
append,
|
||||
projectRoot
|
||||
} = args;
|
||||
|
||||
// Create the standard logger wrapper
|
||||
const logWrapper = createLogWrapper(log);
|
||||
|
||||
// --- Input Validation and Path Resolution ---
|
||||
if (!projectRoot) {
|
||||
logWrapper.error('parsePRDDirect requires a projectRoot argument.');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'projectRoot is required.'
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!inputArg) {
|
||||
logWrapper.error('parsePRDDirect called without input path');
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' }
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input and output paths relative to projectRoot
|
||||
const inputPath = path.resolve(projectRoot, inputArg);
|
||||
const outputPath = outputArg
|
||||
? path.resolve(projectRoot, outputArg)
|
||||
: path.resolve(projectRoot, 'tasks', 'tasks.json'); // Default output path
|
||||
|
||||
// Check if input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
const errorMsg = `Input PRD file not found at resolved path: ${inputPath}`;
|
||||
logWrapper.error(errorMsg);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'FILE_NOT_FOUND', message: errorMsg }
|
||||
};
|
||||
}
|
||||
|
||||
const outputDir = path.dirname(outputPath);
|
||||
try {
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
logWrapper.info(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
} catch (dirError) {
|
||||
logWrapper.error(
|
||||
`Failed to create output directory ${outputDir}: ${dirError.message}`
|
||||
);
|
||||
// Return an error response immediately if dir creation fails
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECTORY_CREATION_ERROR',
|
||||
message: `Failed to create output directory: ${dirError.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let numTasks = getDefaultNumTasks(projectRoot);
|
||||
if (numTasksArg) {
|
||||
numTasks =
|
||||
typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg;
|
||||
if (isNaN(numTasks) || numTasks <= 0) {
|
||||
// Ensure positive number
|
||||
numTasks = getDefaultNumTasks(projectRoot); // Fallback to default if parsing fails or invalid
|
||||
logWrapper.warn(
|
||||
`Invalid numTasks value: ${numTasksArg}. Using default: ${numTasks}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const useForce = force === true;
|
||||
const useAppend = append === true;
|
||||
if (useAppend) {
|
||||
logWrapper.info('Append mode enabled.');
|
||||
if (useForce) {
|
||||
logWrapper.warn(
|
||||
'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logWrapper.info(
|
||||
`Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${useForce}, Append: ${useAppend}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode();
|
||||
}
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
|
||||
try {
|
||||
// Call the core parsePRD function
|
||||
const result = await parsePRD(
|
||||
inputPath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
{ session, mcpLog: logWrapper, projectRoot, useForce, useAppend },
|
||||
'json'
|
||||
);
|
||||
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// parsePRD returns { success: true, tasks: processedTasks } on success
|
||||
if (result && result.success && Array.isArray(result.tasks)) {
|
||||
logWrapper.success(
|
||||
`Successfully parsed PRD. Generated ${result.tasks.length} tasks.`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully parsed PRD and generated ${result.tasks.length} tasks.`,
|
||||
outputPath: outputPath,
|
||||
taskCount: result.tasks.length
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Handle case where core function didn't return expected success structure
|
||||
logWrapper.error(
|
||||
'Core parsePRD function did not return a successful structure.'
|
||||
);
|
||||
// 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: 'CORE_FUNCTION_ERROR',
|
||||
message:
|
||||
result?.message ||
|
||||
'Core function failed to parse PRD or returned unexpected result.'
|
||||
}
|
||||
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';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROJECT_ROOT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.input) {
|
||||
const errorMessage = 'Input file path is required for parsePRDDirect';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_INPUT_PATH', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.output) {
|
||||
const errorMessage = 'Output file path is required for parsePRDDirect';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_OUTPUT_PATH', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input path (expecting absolute path or path relative to project root)
|
||||
const projectRoot = args.projectRoot;
|
||||
const inputPath = path.isAbsolute(args.input)
|
||||
? args.input
|
||||
: path.resolve(projectRoot, args.input);
|
||||
|
||||
// Verify input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
const errorMessage = `Input file not found: ${inputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_FILE_NOT_FOUND',
|
||||
message: errorMessage,
|
||||
details: `Checked path: ${inputPath}\nProject root: ${projectRoot}\nInput argument: ${args.input}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve output path (expecting absolute path or path relative to project root)
|
||||
const outputPath = path.isAbsolute(args.output)
|
||||
? args.output
|
||||
: path.resolve(projectRoot, args.output);
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
log.info(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Parse number of tasks - handle both string and number values
|
||||
let numTasks = 10; // Default
|
||||
if (args.numTasks) {
|
||||
numTasks =
|
||||
typeof args.numTasks === 'string'
|
||||
? parseInt(args.numTasks, 10)
|
||||
: args.numTasks;
|
||||
if (isNaN(numTasks)) {
|
||||
numTasks = 10; // Fallback to default if parsing fails
|
||||
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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, 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
|
||||
};
|
||||
|
||||
// Get model config from session
|
||||
const modelConfig = getModelConfig(session);
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
try {
|
||||
// Make sure the output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
log.info(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Execute core parsePRD function with AI client
|
||||
await parsePRD(
|
||||
inputPath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
{
|
||||
mcpLog: logWrapper,
|
||||
session,
|
||||
append
|
||||
},
|
||||
aiClient,
|
||||
modelConfig
|
||||
);
|
||||
|
||||
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
|
||||
// to return it to the caller
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
const actionVerb = append ? 'appended' : 'generated';
|
||||
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
|
||||
|
||||
log.info(message);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message,
|
||||
taskCount: tasksData.tasks?.length || 0,
|
||||
outputPath,
|
||||
appended: append
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} else {
|
||||
const errorMessage = `Tasks file was not created at ${outputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
// Always restore normal logging
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
logWrapper.error(`Error executing core parsePRD: ${error.message}`);
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error parsing PRD: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PARSE_PRD_CORE_ERROR',
|
||||
code: 'PARSE_PRD_ERROR',
|
||||
message: error.message || 'Unknown error parsing PRD'
|
||||
}
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,100 +3,143 @@
|
||||
* Direct function implementation for showing task details
|
||||
*/
|
||||
|
||||
import { findTaskById, readJSON } from '../../../../scripts/modules/utils.js';
|
||||
import { findTaskById } from '../../../../scripts/modules/utils.js';
|
||||
import { readJSON } from '../../../../scripts/modules/utils.js';
|
||||
import { getCachedOrExecute } from '../../tools/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for getting task details.
|
||||
* Direct function wrapper for showing task details with error handling and caching.
|
||||
*
|
||||
* @param {Object} args - Command arguments.
|
||||
* @param {string} args.id - Task ID to show.
|
||||
* @param {string} [args.file] - Optional path to the tasks file (passed to findTasksJsonPath).
|
||||
* @param {string} [args.status] - Optional status to filter subtasks by.
|
||||
* @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool).
|
||||
* @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.
|
||||
* @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 show.
|
||||
* @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 session from context if needed later, otherwise ignore
|
||||
// const { session } = context;
|
||||
// Destructure projectRoot and other args. projectRoot is assumed normalized.
|
||||
const { id, file, status, projectRoot } = args;
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, id } = args;
|
||||
|
||||
log.info(
|
||||
`Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
|
||||
// --- Path Resolution using the passed (already normalized) projectRoot ---
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
// Use the projectRoot passed directly from args
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: projectRoot, file: file },
|
||||
log
|
||||
);
|
||||
log.info(`Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
if (!tasksJsonPath) {
|
||||
log.error('showTaskDirect called without tasksJsonPath');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASKS_FILE_NOT_FOUND',
|
||||
message: `Failed to find tasks.json: ${error.message}`
|
||||
}
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'tasksJsonPath is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
// --- End Path Resolution ---
|
||||
|
||||
// --- Rest of the function remains the same, using tasksJsonPath ---
|
||||
try {
|
||||
const tasksData = readJSON(tasksJsonPath);
|
||||
if (!tasksData || !tasksData.tasks) {
|
||||
// Validate task ID
|
||||
const taskId = id;
|
||||
if (!taskId) {
|
||||
log.error('Task ID is required');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_VALIDATION_ERROR',
|
||||
message: 'Task ID is required'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Generate cache key using the provided task path and ID
|
||||
const cacheKey = `showTask:${tasksJsonPath}:${taskId}`;
|
||||
|
||||
// Define the action function to be executed on cache miss
|
||||
const coreShowTaskAction = async () => {
|
||||
try {
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
|
||||
log.info(
|
||||
`Retrieving task details for ID: ${taskId} from ${tasksJsonPath}`
|
||||
);
|
||||
|
||||
// Read tasks data using the provided path
|
||||
const data = readJSON(tasksJsonPath);
|
||||
if (!data || !data.tasks) {
|
||||
disableSilentMode(); // Disable before returning
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_TASKS_FILE',
|
||||
message: `No valid tasks found in ${tasksJsonPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific task
|
||||
const task = findTaskById(data.tasks, taskId);
|
||||
|
||||
if (!task) {
|
||||
disableSilentMode(); // Disable before returning
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task with ID ${taskId} not found`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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 {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASKS_DATA', message: 'Invalid tasks data' }
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
allTasks: data.tasks
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
const { task, originalSubtaskCount } = findTaskById(
|
||||
tasksData.tasks,
|
||||
id,
|
||||
status
|
||||
);
|
||||
|
||||
if (!task) {
|
||||
log.error(`Error showing task: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task or subtask with ID ${id} not found`
|
||||
code: 'CORE_FUNCTION_ERROR',
|
||||
message: error.message || 'Failed to show task details'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
log.info(`Successfully retrieved task ${id}.`);
|
||||
|
||||
const returnData = { ...task };
|
||||
if (originalSubtaskCount !== null) {
|
||||
returnData._originalSubtaskCount = originalSubtaskCount;
|
||||
returnData._subtaskFilter = status;
|
||||
}
|
||||
|
||||
return { success: true, data: returnData };
|
||||
// Use the caching utility
|
||||
try {
|
||||
const result = await getCachedOrExecute({
|
||||
cacheKey,
|
||||
actionFn: coreShowTaskAction,
|
||||
log
|
||||
});
|
||||
log.info(`showTaskDirect completed. From cache: ${result.fromCache}`);
|
||||
return result; // Returns { success, data/error, fromCache }
|
||||
} catch (error) {
|
||||
log.error(`Error showing task ${id}: ${error.message}`);
|
||||
// Catch unexpected errors from getCachedOrExecute itself
|
||||
disableSilentMode();
|
||||
log.error(
|
||||
`Unexpected error during getCachedOrExecute for showTask: ${error.message}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TASK_OPERATION_ERROR',
|
||||
code: 'UNEXPECTED_ERROR',
|
||||
message: error.message
|
||||
}
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,40 +6,32 @@
|
||||
import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updateSubtaskById with error handling.
|
||||
*
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
|
||||
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
||||
* @param {string} args.id - Subtask ID in format "parent.sub".
|
||||
* @param {string} args.prompt - Information to append to the subtask.
|
||||
* @param {boolean} [args.research] - Whether to use research role.
|
||||
* @param {string} [args.projectRoot] - Project root path.
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch 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 updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
const { session } = context;
|
||||
// Destructure expected args, including projectRoot
|
||||
const { tasksJsonPath, id, prompt, research, projectRoot } = args;
|
||||
|
||||
const logWrapper = createLogWrapper(log);
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
const { tasksJsonPath, id, prompt, research } = args;
|
||||
|
||||
try {
|
||||
logWrapper.info(
|
||||
`Updating subtask by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
const errorMessage = 'tasksJsonPath is required but was not provided.';
|
||||
logWrapper.error(errorMessage);
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage },
|
||||
@@ -47,22 +39,22 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Basic validation for ID format (e.g., '5.2')
|
||||
if (!id || typeof id !== 'string' || !id.includes('.')) {
|
||||
// Check required parameters (id and prompt)
|
||||
if (!id) {
|
||||
const errorMessage =
|
||||
'Invalid subtask ID format. Must be in format "parentId.subtaskId" (e.g., "5.2").';
|
||||
logWrapper.error(errorMessage);
|
||||
'No subtask ID specified. Please provide a subtask ID to update.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_SUBTASK_ID', message: errorMessage },
|
||||
error: { code: 'MISSING_SUBTASK_ID', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!prompt) {
|
||||
const errorMessage =
|
||||
'No prompt specified. Please provide the information to append.';
|
||||
logWrapper.error(errorMessage);
|
||||
'No prompt specified. Please provide a prompt with information to add to the subtask.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
@@ -95,41 +87,79 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
|
||||
// Use the provided path
|
||||
const tasksPath = tasksJsonPath;
|
||||
|
||||
// Get research flag
|
||||
const useResearch = research === true;
|
||||
|
||||
log.info(
|
||||
`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}`
|
||||
);
|
||||
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode();
|
||||
// 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
|
||||
};
|
||||
|
||||
// Execute core updateSubtaskById function
|
||||
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
|
||||
const updatedSubtask = await updateSubtaskById(
|
||||
tasksPath,
|
||||
subtaskIdStr,
|
||||
prompt,
|
||||
useResearch,
|
||||
{ mcpLog: logWrapper, session, projectRoot },
|
||||
'json'
|
||||
{
|
||||
session,
|
||||
mcpLog: logWrapper
|
||||
}
|
||||
);
|
||||
|
||||
if (updatedSubtask === null) {
|
||||
const message = `Subtask ${id} or its parent task not found.`;
|
||||
logWrapper.error(message); // Log as error since it couldn't be found
|
||||
// Restore normal logging
|
||||
disableSilentMode();
|
||||
|
||||
// Handle the case where the subtask couldn't be updated (e.g., already marked as done)
|
||||
if (!updatedSubtask) {
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'SUBTASK_NOT_FOUND', message: message },
|
||||
error: {
|
||||
code: 'SUBTASK_UPDATE_FAILED',
|
||||
message:
|
||||
'Failed to update subtask. It may be marked as completed, or another error occurred.'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Subtask updated successfully
|
||||
const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`;
|
||||
logWrapper.success(successMessage);
|
||||
// Return the updated subtask information
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
@@ -140,33 +170,23 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
||||
tasksPath,
|
||||
useResearch
|
||||
},
|
||||
fromCache: false
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
logWrapper.error(`Error updating subtask by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_SUBTASK_CORE_ERROR',
|
||||
message: error.message || 'Unknown error updating subtask'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
throw error; // Rethrow to be caught by outer catch block
|
||||
}
|
||||
} catch (error) {
|
||||
logWrapper.error(
|
||||
`Setup error in updateSubtaskByIdDirect: ${error.message}`
|
||||
);
|
||||
if (isSilentMode()) disableSilentMode();
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating subtask by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECT_FUNCTION_SETUP_ERROR',
|
||||
message: error.message || 'Unknown setup error'
|
||||
code: 'UPDATE_SUBTASK_ERROR',
|
||||
message: error.message || 'Unknown error updating subtask'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
|
||||
@@ -6,40 +6,33 @@
|
||||
import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updateTaskById with error handling.
|
||||
*
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
|
||||
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
||||
* @param {string} args.id - Task ID (or subtask ID like "1.2").
|
||||
* @param {string} args.prompt - New information/context prompt.
|
||||
* @param {boolean} [args.research] - Whether to use research role.
|
||||
* @param {string} [args.projectRoot] - Project root path.
|
||||
* @param {Object} args - Command arguments containing id, prompt, useResearch 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 updateTaskByIdDirect(args, log, context = {}) {
|
||||
const { session } = context;
|
||||
// Destructure expected args, including projectRoot
|
||||
const { tasksJsonPath, id, prompt, research, projectRoot } = args;
|
||||
|
||||
const logWrapper = createLogWrapper(log);
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
// Destructure expected args, including the resolved tasksJsonPath
|
||||
const { tasksJsonPath, id, prompt, research } = args;
|
||||
|
||||
try {
|
||||
logWrapper.info(
|
||||
`Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
log.info(`Updating task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
const errorMessage = 'tasksJsonPath is required but was not provided.';
|
||||
logWrapper.error(errorMessage);
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage },
|
||||
@@ -51,7 +44,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
if (!id) {
|
||||
const errorMessage =
|
||||
'No task ID specified. Please provide a task ID to update.';
|
||||
logWrapper.error(errorMessage);
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_TASK_ID', message: errorMessage },
|
||||
@@ -62,7 +55,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
if (!prompt) {
|
||||
const errorMessage =
|
||||
'No prompt specified. Please provide a prompt with new information for the task update.';
|
||||
logWrapper.error(errorMessage);
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROMPT', message: errorMessage },
|
||||
@@ -81,7 +74,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
taskId = parseInt(id, 10);
|
||||
if (isNaN(taskId)) {
|
||||
const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
|
||||
logWrapper.error(errorMessage);
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'INVALID_TASK_ID', message: errorMessage },
|
||||
@@ -99,80 +92,94 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
||||
// Get research flag
|
||||
const useResearch = research === true;
|
||||
|
||||
logWrapper.info(
|
||||
// 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}`
|
||||
);
|
||||
|
||||
const wasSilent = isSilentMode();
|
||||
if (!wasSilent) {
|
||||
enableSilentMode();
|
||||
}
|
||||
|
||||
try {
|
||||
// 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
|
||||
};
|
||||
|
||||
// Execute core updateTaskById function with proper parameters
|
||||
const updatedTask = await updateTaskById(
|
||||
await updateTaskById(
|
||||
tasksPath,
|
||||
taskId,
|
||||
prompt,
|
||||
useResearch,
|
||||
{
|
||||
mcpLog: logWrapper,
|
||||
session,
|
||||
projectRoot
|
||||
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
|
||||
session
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
// Check if the core function indicated the task wasn't updated (e.g., status was 'done')
|
||||
if (updatedTask === null) {
|
||||
// Core function logs the reason, just return success with info
|
||||
const message = `Task ${taskId} was not updated (likely already completed).`;
|
||||
logWrapper.info(message);
|
||||
return {
|
||||
success: true,
|
||||
data: { message: message, taskId: taskId, updated: false },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Task was updated successfully
|
||||
const successMessage = `Successfully updated task with ID ${taskId} based on the prompt`;
|
||||
logWrapper.success(successMessage);
|
||||
// Since updateTaskById doesn't return a value but modifies the tasks file,
|
||||
// we'll return a success message
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: successMessage,
|
||||
taskId: taskId,
|
||||
tasksPath: tasksPath,
|
||||
useResearch: useResearch,
|
||||
updated: true,
|
||||
updatedTask: updatedTask
|
||||
message: `Successfully updated task with ID ${taskId} based on the prompt`,
|
||||
taskId,
|
||||
tasksPath: tasksPath, // Return the used path
|
||||
useResearch
|
||||
},
|
||||
fromCache: false
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} catch (error) {
|
||||
logWrapper.error(`Error updating task by ID: ${error.message}`);
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASK_CORE_ERROR',
|
||||
code: 'UPDATE_TASK_ERROR',
|
||||
message: error.message || 'Unknown error updating task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
logWrapper.error(`Setup error in updateTaskByIdDirect: ${error.message}`);
|
||||
if (isSilentMode()) disableSilentMode();
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating task by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECT_FUNCTION_SETUP_ERROR',
|
||||
message: error.message || 'Unknown setup error'
|
||||
code: 'UPDATE_TASK_ERROR',
|
||||
message: error.message || 'Unknown error updating task'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
|
||||
@@ -1,124 +1,187 @@
|
||||
/**
|
||||
* update-tasks.js
|
||||
* Direct function implementation for updating tasks based on new context
|
||||
* Direct function implementation for updating tasks based on new context/prompt
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { updateTasks } from '../../../../scripts/modules/task-manager.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP
|
||||
} from '../utils/ai-client-utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for updating tasks based on new context.
|
||||
* Direct function wrapper for updating tasks based on new context/prompt.
|
||||
*
|
||||
* @param {Object} args - Command arguments containing projectRoot, from, prompt, research options.
|
||||
* @param {Object} args - Command arguments containing fromId, prompt, useResearch 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;
|
||||
const { from, prompt, research, file: fileArg, projectRoot } = args;
|
||||
const { session } = context; // Only extract session, not reportProgress
|
||||
const { tasksJsonPath, from, prompt, research } = args;
|
||||
|
||||
// Create the standard logger wrapper
|
||||
const logWrapper = createLogWrapper(log);
|
||||
|
||||
// --- Input Validation ---
|
||||
if (!projectRoot) {
|
||||
logWrapper.error('updateTasksDirect requires a projectRoot argument.');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'projectRoot is required.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
logWrapper.error('updateTasksDirect called without from ID');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'Starting task ID (from) is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!prompt) {
|
||||
logWrapper.error('updateTasksDirect called without prompt');
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'Update prompt is required'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve tasks file path
|
||||
const tasksFile = fileArg
|
||||
? path.resolve(projectRoot, fileArg)
|
||||
: path.resolve(projectRoot, 'tasks', 'tasks.json');
|
||||
|
||||
logWrapper.info(
|
||||
`Updating tasks via direct function. From: ${from}, Research: ${research}, File: ${tasksFile}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
|
||||
enableSilentMode(); // Enable silent mode
|
||||
try {
|
||||
// Call the core updateTasks function
|
||||
const result = await updateTasks(
|
||||
tasksFile,
|
||||
from,
|
||||
prompt,
|
||||
research,
|
||||
{
|
||||
session,
|
||||
mcpLog: logWrapper,
|
||||
projectRoot
|
||||
},
|
||||
'json'
|
||||
);
|
||||
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// updateTasks returns { success: true, updatedTasks: [...] } on success
|
||||
if (result && result.success && Array.isArray(result.updatedTasks)) {
|
||||
logWrapper.success(
|
||||
`Successfully updated ${result.updatedTasks.length} tasks.`
|
||||
);
|
||||
// Check if tasksJsonPath was provided
|
||||
if (!tasksJsonPath) {
|
||||
const errorMessage = 'tasksJsonPath is required but was not provided.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully updated ${result.updatedTasks.length} tasks.`,
|
||||
tasksFile,
|
||||
updatedCount: result.updatedTasks.length
|
||||
}
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
} else {
|
||||
// Handle case where core function didn't return expected success structure
|
||||
logWrapper.error(
|
||||
'Core updateTasks function did not return a successful structure.'
|
||||
);
|
||||
}
|
||||
|
||||
// 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: 'CORE_FUNCTION_ERROR',
|
||||
message:
|
||||
result?.message ||
|
||||
'Core function failed to update tasks or returned unexpected result.'
|
||||
}
|
||||
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
|
||||
};
|
||||
|
||||
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) {
|
||||
logWrapper.error(`Error executing core updateTasks: ${error.message}`);
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_TASKS_CORE_ERROR',
|
||||
code: 'UPDATE_TASKS_ERROR',
|
||||
message: error.message || 'Unknown error updating tasks'
|
||||
}
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
disableSilentMode(); // Ensure silent mode is disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,20 @@ import { fixDependenciesDirect } from './direct-functions/fix-dependencies.js';
|
||||
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.js';
|
||||
import { modelsDirect } from './direct-functions/models.js';
|
||||
import { initializeProjectDirect } from './direct-functions/initialize-project-direct.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],
|
||||
@@ -58,9 +66,7 @@ export const directFunctions = new Map([
|
||||
['fixDependenciesDirect', fixDependenciesDirect],
|
||||
['complexityReportDirect', complexityReportDirect],
|
||||
['addDependencyDirect', addDependencyDirect],
|
||||
['removeTaskDirect', removeTaskDirect],
|
||||
['initializeProjectDirect', initializeProjectDirect],
|
||||
['modelsDirect', modelsDirect]
|
||||
['removeTaskDirect', removeTaskDirect]
|
||||
]);
|
||||
|
||||
// Re-export all direct function implementations
|
||||
@@ -88,6 +94,5 @@ export {
|
||||
complexityReportDirect,
|
||||
addDependencyDirect,
|
||||
removeTaskDirect,
|
||||
initializeProjectDirect,
|
||||
modelsDirect
|
||||
initializeProjectDirect
|
||||
};
|
||||
|
||||
213
mcp-server/src/core/utils/ai-client-utils.js
Normal file
213
mcp-server/src/core/utils/ai-client-utils.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
251
mcp-server/src/core/utils/async-manager.js
Normal file
251
mcp-server/src/core/utils/async-manager.js
Normal file
@@ -0,0 +1,251 @@
|
||||
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 };
|
||||
@@ -5,6 +5,7 @@ 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();
|
||||
@@ -34,6 +35,9 @@ 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);
|
||||
@@ -84,4 +88,7 @@ class TaskMasterMCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Export the manager from here as well, if needed elsewhere
|
||||
export { asyncOperationManager };
|
||||
|
||||
export default TaskMasterMCPServer;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 = {
|
||||
@@ -11,8 +10,10 @@ const LOG_LEVELS = {
|
||||
success: 4
|
||||
};
|
||||
|
||||
// Get log level from config manager or default to info
|
||||
const LOG_LEVEL = LOG_LEVELS[getLogLevel().toLowerCase()] ?? LOG_LEVELS.info;
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Logs a message with the specified level
|
||||
|
||||
@@ -7,8 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { addDependencyDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -36,16 +35,28 @@ export function registerAddDependencyTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
|
||||
);
|
||||
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -81,6 +92,6 @@ export function registerAddDependencyTool(server) {
|
||||
log.error(`Error in addDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { addSubtaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -60,15 +60,24 @@ export function registerAddSubtaskTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// Get project root from args or session
|
||||
const rootFolder =
|
||||
args.projectRoot || getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder) {
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
|
||||
);
|
||||
}
|
||||
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -104,6 +113,6 @@ export function registerAddSubtaskTool(server) {
|
||||
log.error(`Error in addSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
createContentResponse,
|
||||
getProjectRootFromSession,
|
||||
executeTaskMasterCommand,
|
||||
handleApiResult
|
||||
} from './utils.js';
|
||||
import { addTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -63,15 +65,26 @@ export function registerAddTaskTool(server) {
|
||||
.optional()
|
||||
.describe('Whether to use research capabilities for task creation')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -81,29 +94,27 @@ export function registerAddTaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct functionP
|
||||
// Call the direct function
|
||||
const result = await addTaskDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
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,
|
||||
projectRoot: args.projectRoot
|
||||
research: args.research
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
// Return the result
|
||||
return handleApiResult(result, log);
|
||||
} catch (error) {
|
||||
log.error(`Error in add-task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,128 +4,120 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import path from 'path';
|
||||
import fs from 'fs'; // Import fs for directory check/creation
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js
|
||||
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Register the analyze_project_complexity tool
|
||||
* Register the analyze tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerAnalyzeProjectComplexityTool(server) {
|
||||
export function registerAnalyzeTool(server) {
|
||||
server.addTool({
|
||||
name: 'analyze_project_complexity',
|
||||
description:
|
||||
'Analyze task complexity and generate expansion recommendations.',
|
||||
'Analyze task complexity and generate expansion recommendations',
|
||||
parameters: z.object({
|
||||
threshold: z.coerce // Use coerce for number conversion from string if needed
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(10)
|
||||
.optional()
|
||||
.default(5) // Default threshold
|
||||
.describe('Complexity score threshold (1-10) to recommend expansion.'),
|
||||
research: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Use Perplexity AI for research-backed analysis.'),
|
||||
output: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Output file path relative to project root (default: scripts/task-complexity-report.json).'
|
||||
'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)'
|
||||
),
|
||||
threshold: z.coerce
|
||||
.number()
|
||||
.min(1)
|
||||
.max(10)
|
||||
.optional()
|
||||
.describe(
|
||||
'Minimum complexity score to recommend expansion (1-10) (default: 5)'
|
||||
),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to the tasks file relative to project root (default: tasks/tasks.json).'
|
||||
'Absolute path to the tasks file (default: tasks/tasks.json)'
|
||||
),
|
||||
research: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Use Perplexity AI for research-backed complexity analysis'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const toolName = 'analyze_project_complexity'; // Define tool name for logging
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
|
||||
`Analyzing task complexity with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Get project root from args or session
|
||||
const rootFolder =
|
||||
args.projectRoot || getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder) {
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
|
||||
);
|
||||
}
|
||||
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json within project root '${args.projectRoot}': ${error.message}`
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const outputPath = args.output
|
||||
? path.resolve(args.projectRoot, args.output)
|
||||
: path.resolve(
|
||||
args.projectRoot,
|
||||
'scripts',
|
||||
'task-complexity-report.json'
|
||||
);
|
||||
? path.resolve(rootFolder, args.output)
|
||||
: path.resolve(rootFolder, 'scripts', 'task-complexity-report.json');
|
||||
|
||||
log.info(`${toolName}: Report output path: ${outputPath}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
try {
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
log.info(`${toolName}: Created output directory: ${outputDir}`);
|
||||
}
|
||||
} catch (dirError) {
|
||||
log.error(
|
||||
`${toolName}: Failed to create output directory ${outputDir}: ${dirError.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Failed to create output directory: ${dirError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Call Direct Function - Pass projectRoot in first arg object
|
||||
const result = await analyzeTaskComplexityDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
outputPath: outputPath,
|
||||
model: args.model,
|
||||
threshold: args.threshold,
|
||||
research: args.research,
|
||||
projectRoot: args.projectRoot
|
||||
research: args.research
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
// 4. Handle Result
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
if (result.success) {
|
||||
log.info(`Task complexity analysis complete: ${result.data.message}`);
|
||||
log.info(
|
||||
`Report summary: ${JSON.stringify(result.data.reportSummary)}`
|
||||
);
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to analyze task complexity: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error analyzing task complexity');
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Internal tool error (${toolName}): ${error.message}`
|
||||
);
|
||||
log.error(`Error in analyze tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { clearSubtasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -41,15 +41,26 @@ export function registerClearSubtasksTool(server) {
|
||||
message: "Either 'id' or 'all' parameter must be provided",
|
||||
path: ['id', 'all']
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -61,11 +72,14 @@ export function registerClearSubtasksTool(server) {
|
||||
|
||||
const result = await clearSubtasksDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
all: args.all
|
||||
},
|
||||
log
|
||||
// Remove context object as clearSubtasksDirect likely doesn't need session/reportProgress
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
@@ -79,6 +93,6 @@ export function registerClearSubtasksTool(server) {
|
||||
log.error(`Error in clearSubtasks tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { complexityReportDirect } from '../core/task-master-core.js';
|
||||
import path from 'path';
|
||||
@@ -31,24 +31,34 @@ export function registerComplexityReportTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Getting complexity report with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to the complexity report file
|
||||
// Default to scripts/task-complexity-report.json relative to root
|
||||
const reportPath = args.file
|
||||
? path.resolve(args.projectRoot, args.file)
|
||||
: path.resolve(
|
||||
args.projectRoot,
|
||||
'scripts',
|
||||
'task-complexity-report.json'
|
||||
);
|
||||
? path.resolve(rootFolder, args.file)
|
||||
: path.resolve(rootFolder, 'scripts', 'task-complexity-report.json');
|
||||
|
||||
const result = await complexityReportDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
reportPath: reportPath
|
||||
// No other args specific to this tool
|
||||
},
|
||||
log
|
||||
);
|
||||
@@ -74,6 +84,6 @@ export function registerComplexityReportTool(server) {
|
||||
`Failed to retrieve complexity report: ${error.message}`
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { expandAllTasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -19,27 +19,22 @@ import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
export function registerExpandAllTool(server) {
|
||||
server.addTool({
|
||||
name: 'expand_all',
|
||||
description:
|
||||
'Expand all pending tasks into subtasks based on complexity or defaults',
|
||||
description: 'Expand all pending tasks into subtasks',
|
||||
parameters: z.object({
|
||||
num: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Target number of subtasks per task (uses complexity/defaults otherwise)'
|
||||
),
|
||||
.describe('Number of subtasks to generate for each task'),
|
||||
research: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'Enable research-backed subtask generation (e.g., using Perplexity)'
|
||||
'Enable Perplexity AI for research-backed subtask generation'
|
||||
),
|
||||
prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Additional context to guide subtask generation for all tasks'
|
||||
),
|
||||
.describe('Additional context to guide subtask generation'),
|
||||
force: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -50,28 +45,34 @@ export function registerExpandAllTool(server) {
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
|
||||
'Absolute path to the tasks file (default: tasks/tasks.json)'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Absolute path to the project root directory (derived from session if possible)'
|
||||
)
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Tool expand_all execution started with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
log.info(`Expanding all tasks 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ 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(
|
||||
@@ -81,29 +82,31 @@ 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,
|
||||
force: args.force,
|
||||
projectRoot: args.projectRoot
|
||||
force: args.force
|
||||
},
|
||||
log,
|
||||
{ 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(
|
||||
`Unexpected error in expand_all tool execute: ${error.message}`
|
||||
);
|
||||
if (error.stack) {
|
||||
log.error(error.stack);
|
||||
}
|
||||
return createErrorResponse(
|
||||
`An unexpected error occurred: ${error.message}`
|
||||
);
|
||||
log.error(`Error in expand-all tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} 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
|
||||
@@ -26,36 +28,39 @@ export function registerExpandTaskTool(server) {
|
||||
research: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Use research role for generation'),
|
||||
.describe('Use Perplexity AI for research-backed generation'),
|
||||
prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Additional context for subtask generation'),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to the tasks file relative to project root (e.g., tasks/tasks.json)'
|
||||
),
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
force: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Force expansion even if subtasks exist')
|
||||
force: z.boolean().optional().describe('Force the expansion')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Starting expand-task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Project root resolved to: ${rootFolder}`);
|
||||
|
||||
// Resolve the path to tasks.json using the utility
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -65,25 +70,29 @@ export function registerExpandTaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call direct function with only session in the context, not reportProgress
|
||||
// Use the pattern recommended in the MCP guidelines
|
||||
const result = await expandTaskDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
num: args.num,
|
||||
research: args.research,
|
||||
prompt: args.prompt,
|
||||
force: args.force,
|
||||
projectRoot: args.projectRoot
|
||||
force: args.force // Need to add force to parameters
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
); // Only pass session, NOT reportProgress
|
||||
|
||||
// Return the result
|
||||
return handleApiResult(result, log, 'Error expanding task');
|
||||
} catch (error) {
|
||||
log.error(`Error in expand-task tool: ${error.message}`);
|
||||
log.error(`Error in expand task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { fixDependenciesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -26,15 +26,24 @@ export function registerFixDependenciesTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// Get project root from args or session
|
||||
const rootFolder =
|
||||
args.projectRoot || getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder) {
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
|
||||
);
|
||||
}
|
||||
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -62,6 +71,6 @@ export function registerFixDependenciesTool(server) {
|
||||
log.error(`Error in fixDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { generateTaskFilesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -32,15 +32,26 @@ export function registerGenerateTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -50,14 +61,17 @@ export function registerGenerateTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Determine output directory: use explicit arg or default to tasks.json directory
|
||||
const outputDir = args.output
|
||||
? path.resolve(args.projectRoot, args.output)
|
||||
? path.resolve(rootFolder, args.output) // Resolve relative to root if needed
|
||||
: path.dirname(tasksJsonPath);
|
||||
|
||||
const result = await generateTaskFilesDirect(
|
||||
{
|
||||
// Pass the explicitly resolved paths
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
outputDir: outputDir
|
||||
// No other args specific to this tool
|
||||
},
|
||||
log
|
||||
);
|
||||
@@ -75,6 +89,6 @@ export function registerGenerateTool(server) {
|
||||
log.error(`Error in generate tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { showTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -21,10 +21,8 @@ function processTaskResponse(data) {
|
||||
if (!data) return data;
|
||||
|
||||
// If we have the expected structure with task and allTasks
|
||||
if (typeof data === 'object' && data !== null && data.id && data.title) {
|
||||
// If the data itself looks like the task object, return it
|
||||
return data;
|
||||
} else if (data.task) {
|
||||
if (data.task) {
|
||||
// Return only the task object, removing the allTasks array
|
||||
return data.task;
|
||||
}
|
||||
|
||||
@@ -42,37 +40,46 @@ 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('Path to the tasks file relative to project root'),
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Absolute path to the project root directory (Optional, usually from session)'
|
||||
)
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log }) => {
|
||||
const { id, file, status, projectRoot } = args;
|
||||
execute: async (args, { log, session }) => {
|
||||
// Log the session right at the start of execute
|
||||
log.info(
|
||||
`Session object received in execute: ${JSON.stringify(session)}`
|
||||
); // Use JSON.stringify for better visibility
|
||||
|
||||
try {
|
||||
log.info(
|
||||
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
|
||||
);
|
||||
log.info(`Getting task details for ID: ${args.id}`);
|
||||
|
||||
// Resolve the path to tasks.json using the NORMALIZED projectRoot from args
|
||||
log.info(
|
||||
`Session object received in execute: ${JSON.stringify(session)}`
|
||||
); // Use JSON.stringify for better visibility
|
||||
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Attempting to use project root: ${rootFolder}`); // Log the final resolved root
|
||||
|
||||
log.info(`Root folder: ${rootFolder}`); // Log the final resolved root
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: projectRoot, file: file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
log.info(`Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
@@ -80,13 +87,14 @@ export function registerShowTaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct function, passing the normalized projectRoot
|
||||
log.info(`Attempting to use tasks file path: ${tasksJsonPath}`);
|
||||
|
||||
const result = await showTaskDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
id: id,
|
||||
status: status,
|
||||
projectRoot: projectRoot
|
||||
// Pass other relevant args
|
||||
id: args.id
|
||||
},
|
||||
log
|
||||
);
|
||||
@@ -99,7 +107,7 @@ export function registerShowTaskTool(server) {
|
||||
log.error(`Failed to get task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use our custom processor function
|
||||
// Use our custom processor function to remove allTasks from the response
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
@@ -107,9 +115,9 @@ export function registerShowTaskTool(server) {
|
||||
processTaskResponse
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);
|
||||
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); // Add stack trace
|
||||
return createErrorResponse(`Failed to get task: ${error.message}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { listTasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -42,19 +42,31 @@ export function registerListTasksTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
// Use the error message from findTasksJsonPath for better context
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
@@ -77,7 +89,7 @@ export function registerListTasksTool(server) {
|
||||
log.error(`Error getting tasks: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { registerExpandTaskTool } from './expand-task.js';
|
||||
import { registerAddTaskTool } from './add-task.js';
|
||||
import { registerAddSubtaskTool } from './add-subtask.js';
|
||||
import { registerRemoveSubtaskTool } from './remove-subtask.js';
|
||||
import { registerAnalyzeProjectComplexityTool } from './analyze.js';
|
||||
import { registerAnalyzeTool } from './analyze.js';
|
||||
import { registerClearSubtasksTool } from './clear-subtasks.js';
|
||||
import { registerExpandAllTool } from './expand-all.js';
|
||||
import { registerRemoveDependencyTool } from './remove-dependency.js';
|
||||
@@ -27,51 +27,39 @@ import { registerComplexityReportTool } from './complexity-report.js';
|
||||
import { registerAddDependencyTool } from './add-dependency.js';
|
||||
import { registerRemoveTaskTool } from './remove-task.js';
|
||||
import { registerInitializeProjectTool } from './initialize-project.js';
|
||||
import { registerModelsTool } from './models.js';
|
||||
import { asyncOperationManager } from '../core/utils/async-manager.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) {
|
||||
export function registerTaskMasterTools(server, asyncManager) {
|
||||
try {
|
||||
// Register each tool in a logical workflow order
|
||||
|
||||
// Group 1: Initialization & Setup
|
||||
registerInitializeProjectTool(server);
|
||||
registerModelsTool(server);
|
||||
registerParsePRDTool(server);
|
||||
|
||||
// Group 2: Task Listing & Viewing
|
||||
// Register each tool
|
||||
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);
|
||||
registerParsePRDTool(server);
|
||||
registerUpdateTool(server);
|
||||
registerUpdateTaskTool(server);
|
||||
registerUpdateSubtaskTool(server);
|
||||
registerRemoveTaskTool(server);
|
||||
registerRemoveSubtaskTool(server);
|
||||
registerClearSubtasksTool(server);
|
||||
|
||||
// Group 5: Task Analysis & Expansion
|
||||
registerAnalyzeProjectComplexityTool(server);
|
||||
registerGenerateTool(server);
|
||||
registerShowTaskTool(server);
|
||||
registerNextTaskTool(server);
|
||||
registerExpandTaskTool(server);
|
||||
registerAddTaskTool(server, asyncManager);
|
||||
registerAddSubtaskTool(server);
|
||||
registerRemoveSubtaskTool(server);
|
||||
registerAnalyzeTool(server);
|
||||
registerClearSubtasksTool(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;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
createContentResponse,
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
handleApiResult
|
||||
} from './utils.js';
|
||||
import { initializeProjectDirect } from '../core/task-master-core.js';
|
||||
|
||||
@@ -37,10 +37,19 @@ export function registerInitializeProjectTool(server) {
|
||||
'The root directory for the project. ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY. IF NOT SET, THE TOOL WILL NOT WORK.'
|
||||
)
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, context) => {
|
||||
execute: async (args, context) => {
|
||||
const { log } = context;
|
||||
const session = context.session;
|
||||
|
||||
log.info(
|
||||
'>>> Full Context Received by Tool:',
|
||||
JSON.stringify(context, null, 2)
|
||||
);
|
||||
log.info(`Context received in tool function: ${context}`);
|
||||
log.info(
|
||||
`Session received in tool function: ${session ? session : 'undefined'}`
|
||||
);
|
||||
|
||||
try {
|
||||
log.info(
|
||||
`Executing initialize_project tool with args: ${JSON.stringify(args)}`
|
||||
@@ -54,6 +63,6 @@ export function registerInitializeProjectTool(server) {
|
||||
log.error(errorMessage, error);
|
||||
return createErrorResponse(errorMessage, { details: error.stack });
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* models.js
|
||||
* MCP tool for managing AI model configurations
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} 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. Input/output costs values are in dollars (3 is $3.00).'
|
||||
),
|
||||
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: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Starting models tool with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
const result = await modelsDirect(
|
||||
{ ...args, projectRoot: args.projectRoot },
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log);
|
||||
} catch (error) {
|
||||
log.error(`Error in models tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { nextTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -27,15 +27,26 @@ export function registerNextTaskTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Finding next task with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -47,7 +58,9 @@ export function registerNextTaskTool(server) {
|
||||
|
||||
const result = await nextTaskDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath
|
||||
// No other args specific to this tool
|
||||
},
|
||||
log
|
||||
);
|
||||
@@ -67,6 +80,6 @@ export function registerNextTaskTool(server) {
|
||||
log.error(`Error in nextTask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import path from 'path';
|
||||
import {
|
||||
getProjectRootFromSession,
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
createErrorResponse
|
||||
} from './utils.js';
|
||||
import { parsePRDDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
resolveProjectPaths,
|
||||
findPRDDocumentPath,
|
||||
resolveTasksOutputPath
|
||||
} from '../core/utils/path-utils.js';
|
||||
|
||||
/**
|
||||
* Register the parse_prd tool
|
||||
* Register the parsePRD tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerParsePRDTool(server) {
|
||||
@@ -42,50 +46,72 @@ export function registerParsePRDTool(server) {
|
||||
force: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Overwrite existing output file without prompting.'),
|
||||
.describe('Allow overwriting an existing tasks.json file.'),
|
||||
append: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Append generated tasks to existing file.'),
|
||||
.describe(
|
||||
'Append new tasks to existing tasks.json instead of overwriting'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
.describe('The directory of the project. Must be absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const toolName = 'parse_prd';
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
|
||||
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get project root from args or session
|
||||
const rootFolder =
|
||||
args.projectRoot || getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder) {
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve input (PRD) and output (tasks.json) paths using the utility
|
||||
const { projectRoot, prdPath, tasksJsonPath } = resolveProjectPaths(
|
||||
rootFolder,
|
||||
args,
|
||||
log
|
||||
);
|
||||
|
||||
// Call Direct Function - Pass relevant args including projectRoot
|
||||
// Check if PRD path was found (resolveProjectPaths returns null if not found and not provided)
|
||||
if (!prdPath) {
|
||||
return createErrorResponse(
|
||||
'No PRD document found or provided. Please ensure a PRD file exists (e.g., PRD.md) or provide a valid input file path.'
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct function with fully resolved paths
|
||||
const result = await parsePRDDirect(
|
||||
{
|
||||
input: args.input,
|
||||
output: args.output,
|
||||
projectRoot: projectRoot,
|
||||
input: prdPath,
|
||||
output: tasksJsonPath,
|
||||
numTasks: args.numTasks,
|
||||
force: args.force,
|
||||
append: args.append,
|
||||
projectRoot: args.projectRoot
|
||||
append: args.append
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
if (result.success) {
|
||||
log.info(`Successfully parsed PRD: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error parsing PRD');
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Internal tool error (${toolName}): ${error.message}`
|
||||
);
|
||||
log.error(`Error in parse-prd tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { removeDependencyDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -33,17 +33,28 @@ export function registerRemoveDependencyTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -55,7 +66,9 @@ export function registerRemoveDependencyTool(server) {
|
||||
|
||||
const result = await removeDependencyDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
dependsOn: args.dependsOn
|
||||
},
|
||||
@@ -73,6 +86,6 @@ export function registerRemoveDependencyTool(server) {
|
||||
log.error(`Error in removeDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { removeSubtaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -46,15 +46,26 @@ export function registerRemoveSubtaskTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -66,7 +77,9 @@ export function registerRemoveSubtaskTool(server) {
|
||||
|
||||
const result = await removeSubtaskDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
convert: args.convert,
|
||||
skipGenerate: args.skipGenerate
|
||||
@@ -85,6 +98,6 @@ export function registerRemoveSubtaskTool(server) {
|
||||
log.error(`Error in removeSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { removeTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -24,7 +24,7 @@ export function registerRemoveTaskTool(server) {
|
||||
id: z
|
||||
.string()
|
||||
.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."
|
||||
"ID(s) of the task(s) or subtask(s) to remove (e.g., '5' or '5.2' or '5,6,7')"
|
||||
),
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
projectRoot: z
|
||||
@@ -35,15 +35,28 @@ export function registerRemoveTaskTool(server) {
|
||||
.optional()
|
||||
.describe('Whether to skip confirmation prompt (default: false)')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Removing task(s) with ID(s): ${args.id}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Using project root: ${rootFolder}`);
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -55,6 +68,7 @@ export function registerRemoveTaskTool(server) {
|
||||
|
||||
log.info(`Using tasks file path: ${tasksJsonPath}`);
|
||||
|
||||
// Assume client has already handled confirmation if needed
|
||||
const result = await removeTaskDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
@@ -74,6 +88,6 @@ export function registerRemoveTaskTool(server) {
|
||||
log.error(`Error in remove-task tool: ${error.message}`);
|
||||
return createErrorResponse(`Failed to remove task: ${error.message}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { setTaskStatusDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -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 to update multiple tasks/subtasks at once."
|
||||
"Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated for multiple updates."
|
||||
),
|
||||
status: z
|
||||
.string()
|
||||
@@ -36,15 +36,26 @@ export function registerSetTaskStatusTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -54,15 +65,19 @@ export function registerSetTaskStatusTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct function with the resolved path
|
||||
const result = await setTaskStatusDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
status: args.status
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
// Log the result
|
||||
if (result.success) {
|
||||
log.info(
|
||||
`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`
|
||||
@@ -73,6 +88,7 @@ export function registerSetTaskStatusTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Format and return the result
|
||||
return handleApiResult(result, log, 'Error setting task status');
|
||||
} catch (error) {
|
||||
log.error(`Error in setTaskStatus tool: ${error.message}`);
|
||||
@@ -80,6 +96,6 @@ export function registerSetTaskStatusTool(server) {
|
||||
`Error setting task status: ${error.message}`
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { updateSubtaskByIdDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -37,19 +37,30 @@ export function registerUpdateSubtaskTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const toolName = 'update_subtask';
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Updating subtask 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
@@ -57,11 +68,12 @@ export function registerUpdateSubtaskTool(server) {
|
||||
|
||||
const result = await updateSubtaskByIdDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
prompt: args.prompt,
|
||||
research: args.research,
|
||||
projectRoot: args.projectRoot
|
||||
research: args.research
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
@@ -77,13 +89,9 @@ export function registerUpdateSubtaskTool(server) {
|
||||
|
||||
return handleApiResult(result, log, 'Error updating subtask');
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Internal tool error (${toolName}): ${error.message}`
|
||||
);
|
||||
log.error(`Error in update_subtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { updateTaskByIdDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -23,7 +23,7 @@ export function registerUpdateTaskTool(server) {
|
||||
'Updates a single task by ID with new information or context provided in the prompt.',
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string() // ID can be number or string like "1.2"
|
||||
.string()
|
||||
.describe(
|
||||
"ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool."
|
||||
),
|
||||
@@ -39,53 +39,61 @@ export function registerUpdateTaskTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const toolName = 'update_task';
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
log.info(`Updating task 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
|
||||
} catch (error) {
|
||||
log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Call Direct Function - Include projectRoot
|
||||
const result = await updateTaskByIdDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
prompt: args.prompt,
|
||||
research: args.research,
|
||||
projectRoot: args.projectRoot
|
||||
research: args.research
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
// 4. Handle Result
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
if (result.success) {
|
||||
log.info(`Successfully updated task with ID ${args.id}`);
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to update task: ${result.error?.message || 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error updating task');
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Internal tool error (${toolName}): ${error.message}`
|
||||
);
|
||||
log.error(`Error in update_task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { updateTasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -34,61 +34,66 @@ export function registerUpdateTool(server) {
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Use Perplexity AI for research-backed updates'),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Path to the tasks file relative to project root'),
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The directory of the project. (Optional, usually from session)'
|
||||
)
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
const toolName = 'update';
|
||||
const { from, prompt, research, file, projectRoot } = args;
|
||||
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Executing ${toolName} tool with normalized root: ${projectRoot}`
|
||||
);
|
||||
log.info(`Updating tasks 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the path to tasks.json
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath({ projectRoot, file }, log);
|
||||
log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json within project root '${projectRoot}': ${error.message}`
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await updateTasksDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
from: from,
|
||||
prompt: prompt,
|
||||
research: research,
|
||||
projectRoot: projectRoot
|
||||
from: args.from,
|
||||
prompt: args.prompt,
|
||||
research: args.research
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
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'}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error updating tasks');
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Internal tool error (${toolName}): ${error.message}`
|
||||
);
|
||||
log.error(`Error in update tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,10 +83,10 @@ function getProjectRoot(projectRootRaw, log) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and normalizes the project root path from the MCP session object.
|
||||
* @param {Object} session - The MCP session object.
|
||||
* @param {Object} log - The MCP logger object.
|
||||
* @returns {string|null} - The normalized absolute project root path or null if not found/invalid.
|
||||
* Extracts the project root path from the FastMCP session object.
|
||||
* @param {Object} session - The FastMCP session object.
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {string|null} - The absolute path to the project root, or null if not found.
|
||||
*/
|
||||
function getProjectRootFromSession(session, log) {
|
||||
try {
|
||||
@@ -107,87 +107,68 @@ function getProjectRootFromSession(session, log) {
|
||||
})}`
|
||||
);
|
||||
|
||||
let rawRootPath = null;
|
||||
let decodedPath = null;
|
||||
let finalPath = null;
|
||||
|
||||
// Check primary location
|
||||
if (session?.roots?.[0]?.uri) {
|
||||
rawRootPath = session.roots[0].uri;
|
||||
log.info(`Found raw root URI in session.roots[0].uri: ${rawRootPath}`);
|
||||
}
|
||||
// Check alternate location
|
||||
else if (session?.roots?.roots?.[0]?.uri) {
|
||||
rawRootPath = session.roots.roots[0].uri;
|
||||
log.info(
|
||||
`Found raw root URI in session.roots.roots[0].uri: ${rawRootPath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (rawRootPath) {
|
||||
// Decode URI and strip file:// protocol
|
||||
decodedPath = rawRootPath.startsWith('file://')
|
||||
? decodeURIComponent(rawRootPath.slice(7))
|
||||
: rawRootPath; // Assume non-file URI is already decoded? Or decode anyway? Let's decode.
|
||||
if (!rawRootPath.startsWith('file://')) {
|
||||
decodedPath = decodeURIComponent(rawRootPath); // Decode even if no file://
|
||||
}
|
||||
|
||||
// Handle potential Windows drive prefix after stripping protocol (e.g., /C:/...)
|
||||
if (
|
||||
decodedPath.startsWith('/') &&
|
||||
/[A-Za-z]:/.test(decodedPath.substring(1, 3))
|
||||
) {
|
||||
decodedPath = decodedPath.substring(1); // Remove leading slash if it's like /C:/...
|
||||
}
|
||||
|
||||
log.info(`Decoded path: ${decodedPath}`);
|
||||
|
||||
// Normalize slashes and resolve
|
||||
const normalizedSlashes = decodedPath.replace(/\\/g, '/');
|
||||
finalPath = path.resolve(normalizedSlashes); // Resolve to absolute path for current OS
|
||||
|
||||
log.info(`Normalized and resolved session path: ${finalPath}`);
|
||||
return finalPath;
|
||||
}
|
||||
|
||||
// Fallback Logic (remains the same)
|
||||
log.warn('No project root URI found in session. Attempting fallbacks...');
|
||||
// ALWAYS ensure we return a valid path for project root
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Fallback 1: Use server path deduction (Cursor IDE)
|
||||
const serverPath = process.argv[1];
|
||||
// If we have a session with roots array
|
||||
if (session?.roots?.[0]?.uri) {
|
||||
const rootUri = session.roots[0].uri;
|
||||
log.info(`Found rootUri in session.roots[0].uri: ${rootUri}`);
|
||||
const rootPath = rootUri.startsWith('file://')
|
||||
? decodeURIComponent(rootUri.slice(7))
|
||||
: rootUri;
|
||||
log.info(`Decoded rootPath: ${rootPath}`);
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
// If we have a session with roots.roots array (different structure)
|
||||
if (session?.roots?.roots?.[0]?.uri) {
|
||||
const rootUri = session.roots.roots[0].uri;
|
||||
log.info(`Found rootUri in session.roots.roots[0].uri: ${rootUri}`);
|
||||
const rootPath = rootUri.startsWith('file://')
|
||||
? decodeURIComponent(rootUri.slice(7))
|
||||
: rootUri;
|
||||
log.info(`Decoded rootPath: ${rootPath}`);
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
// Get the server's location and try to find project root -- this is a fallback necessary in Cursor IDE
|
||||
const serverPath = process.argv[1]; // This should be the path to server.js, which is in mcp-server/
|
||||
if (serverPath && serverPath.includes('mcp-server')) {
|
||||
// Find the mcp-server directory first
|
||||
const mcpServerIndex = serverPath.indexOf('mcp-server');
|
||||
if (mcpServerIndex !== -1) {
|
||||
const projectRoot = path.dirname(
|
||||
serverPath.substring(0, mcpServerIndex)
|
||||
); // Go up one level
|
||||
// Get the path up to mcp-server, which should be the project root
|
||||
const projectRoot = serverPath.substring(0, mcpServerIndex - 1); // -1 to remove trailing slash
|
||||
|
||||
// Verify this looks like our project root by checking for key files/directories
|
||||
if (
|
||||
fs.existsSync(path.join(projectRoot, '.cursor')) ||
|
||||
fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
|
||||
fs.existsSync(path.join(projectRoot, 'package.json'))
|
||||
) {
|
||||
log.info(
|
||||
`Using project root derived from server path: ${projectRoot}`
|
||||
);
|
||||
return projectRoot; // Already absolute
|
||||
log.info(`Found project root from server path: ${projectRoot}`);
|
||||
return projectRoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 2: Use CWD
|
||||
// ALWAYS ensure we return a valid path as a last resort
|
||||
log.info(`Using current working directory as ultimate fallback: ${cwd}`);
|
||||
return cwd; // Already absolute
|
||||
} catch (e) {
|
||||
log.error(`Error in getProjectRootFromSession: ${e.message}`);
|
||||
// Attempt final fallback to CWD on error
|
||||
const cwd = process.cwd();
|
||||
log.warn(
|
||||
`Returning CWD (${cwd}) due to error during session root processing.`
|
||||
);
|
||||
return cwd;
|
||||
} catch (e) {
|
||||
// If we have a server path, use it as a basis for project root
|
||||
const serverPath = process.argv[1];
|
||||
if (serverPath && serverPath.includes('mcp-server')) {
|
||||
const mcpServerIndex = serverPath.indexOf('mcp-server');
|
||||
return mcpServerIndex !== -1
|
||||
? serverPath.substring(0, mcpServerIndex - 1)
|
||||
: process.cwd();
|
||||
}
|
||||
|
||||
// Only use cwd if it's not "/"
|
||||
const cwd = process.cwd();
|
||||
return cwd !== '/' ? cwd : '/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +443,7 @@ function createContentResponse(content) {
|
||||
* @param {string} errorMessage - Error message to include in response
|
||||
* @returns {Object} - Error content response object in FastMCP format
|
||||
*/
|
||||
function createErrorResponse(errorMessage) {
|
||||
export function createErrorResponse(errorMessage) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -474,167 +455,6 @@ 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)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and normalizes a project root path from various formats.
|
||||
* Handles URI encoding, Windows paths, and file protocols.
|
||||
* @param {string | undefined | null} rawPath - The raw project root path.
|
||||
* @param {object} [log] - Optional logger object.
|
||||
* @returns {string | null} Normalized absolute path or null if input is invalid/empty.
|
||||
*/
|
||||
function normalizeProjectRoot(rawPath, log) {
|
||||
if (!rawPath) return null;
|
||||
try {
|
||||
let pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
|
||||
if (!pathString) return null;
|
||||
|
||||
// 1. Decode URI Encoding
|
||||
// Use try-catch for decoding as malformed URIs can throw
|
||||
try {
|
||||
pathString = decodeURIComponent(pathString);
|
||||
} catch (decodeError) {
|
||||
if (log)
|
||||
log.warn(
|
||||
`Could not decode URI component for path "${rawPath}": ${decodeError.message}. Proceeding with raw string.`
|
||||
);
|
||||
// Proceed with the original string if decoding fails
|
||||
pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
|
||||
}
|
||||
|
||||
// 2. Strip file:// prefix (handle 2 or 3 slashes)
|
||||
if (pathString.startsWith('file:///')) {
|
||||
pathString = pathString.slice(7); // Slice 7 for file:///, may leave leading / on Windows
|
||||
} else if (pathString.startsWith('file://')) {
|
||||
pathString = pathString.slice(7); // Slice 7 for file://
|
||||
}
|
||||
|
||||
// 3. Handle potential Windows leading slash after stripping prefix (e.g., /C:/...)
|
||||
// This checks if it starts with / followed by a drive letter C: D: etc.
|
||||
if (
|
||||
pathString.startsWith('/') &&
|
||||
/[A-Za-z]:/.test(pathString.substring(1, 3))
|
||||
) {
|
||||
pathString = pathString.substring(1); // Remove the leading slash
|
||||
}
|
||||
|
||||
// 4. Normalize backslashes to forward slashes
|
||||
pathString = pathString.replace(/\\/g, '/');
|
||||
|
||||
// 5. Resolve to absolute path using server's OS convention
|
||||
const resolvedPath = path.resolve(pathString);
|
||||
return resolvedPath;
|
||||
} catch (error) {
|
||||
if (log) {
|
||||
log.error(
|
||||
`Error normalizing project root path "${rawPath}": ${error.message}`
|
||||
);
|
||||
}
|
||||
return null; // Return null on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the raw project root path from the session (without normalization).
|
||||
* Used as a fallback within the HOF.
|
||||
* @param {Object} session - The MCP session object.
|
||||
* @param {Object} log - The MCP logger object.
|
||||
* @returns {string|null} The raw path string or null.
|
||||
*/
|
||||
function getRawProjectRootFromSession(session, log) {
|
||||
try {
|
||||
// Check primary location
|
||||
if (session?.roots?.[0]?.uri) {
|
||||
return session.roots[0].uri;
|
||||
}
|
||||
// Check alternate location
|
||||
else if (session?.roots?.roots?.[0]?.uri) {
|
||||
return session.roots.roots[0].uri;
|
||||
}
|
||||
return null; // Not found in expected session locations
|
||||
} catch (e) {
|
||||
log.error(`Error accessing session roots: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order function to wrap MCP tool execute methods.
|
||||
* Ensures args.projectRoot is present and normalized before execution.
|
||||
* @param {Function} executeFn - The original async execute(args, context) function.
|
||||
* @returns {Function} The wrapped async execute function.
|
||||
*/
|
||||
function withNormalizedProjectRoot(executeFn) {
|
||||
return async (args, context) => {
|
||||
const { log, session } = context;
|
||||
let normalizedRoot = null;
|
||||
let rootSource = 'unknown';
|
||||
|
||||
try {
|
||||
// Determine raw root: prioritize args, then session
|
||||
let rawRoot = args.projectRoot;
|
||||
if (!rawRoot) {
|
||||
rawRoot = getRawProjectRootFromSession(session, log);
|
||||
rootSource = 'session';
|
||||
} else {
|
||||
rootSource = 'args';
|
||||
}
|
||||
|
||||
if (!rawRoot) {
|
||||
log.error('Could not determine project root from args or session.');
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide projectRoot argument or ensure session contains root info.'
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize the determined raw root
|
||||
normalizedRoot = normalizeProjectRoot(rawRoot, log);
|
||||
|
||||
if (!normalizedRoot) {
|
||||
log.error(
|
||||
`Failed to normalize project root obtained from ${rootSource}: ${rawRoot}`
|
||||
);
|
||||
return createErrorResponse(
|
||||
`Invalid project root provided or derived from ${rootSource}: ${rawRoot}`
|
||||
);
|
||||
}
|
||||
|
||||
// Inject the normalized root back into args
|
||||
const updatedArgs = { ...args, projectRoot: normalizedRoot };
|
||||
|
||||
// Execute the original function with normalized root in args
|
||||
return await executeFn(updatedArgs, context);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
|
||||
);
|
||||
// Add stack trace if available and debug enabled
|
||||
if (error.stack && log.debug) {
|
||||
log.debug(error.stack);
|
||||
}
|
||||
// Return a generic error or re-throw depending on desired behavior
|
||||
return createErrorResponse(`Operation failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure all functions are exported
|
||||
export {
|
||||
getProjectRoot,
|
||||
@@ -643,10 +463,5 @@ export {
|
||||
executeTaskMasterCommand,
|
||||
getCachedOrExecute,
|
||||
processMCPResponseData,
|
||||
createContentResponse,
|
||||
createErrorResponse,
|
||||
createLogWrapper,
|
||||
normalizeProjectRoot,
|
||||
getRawProjectRootFromSession,
|
||||
withNormalizedProjectRoot
|
||||
createContentResponse
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
getProjectRootFromSession
|
||||
} from './utils.js';
|
||||
import { validateDependenciesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||
@@ -27,15 +27,24 @@ export function registerValidateDependenciesTool(server) {
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// Get project root from args or session
|
||||
const rootFolder =
|
||||
args.projectRoot || getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder) {
|
||||
return createErrorResponse(
|
||||
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
|
||||
);
|
||||
}
|
||||
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksJsonPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
{ projectRoot: rootFolder, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -65,6 +74,6 @@ export function registerValidateDependenciesTool(server) {
|
||||
log.error(`Error in validateDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user