Recovers lost files and commits work from the past 5-6 days. Holy shit that was a close call.

This commit is contained in:
Eyal Toledano
2025-04-07 19:55:03 -04:00
parent b3e7ebefd9
commit b7580e038d
42 changed files with 5180 additions and 1988 deletions

View File

@@ -6,6 +6,8 @@
import { addTask } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
import { _buildAddTaskPrompt, parseTaskJsonResponse, _handleAnthropicStream } from '../../../../scripts/modules/ai-services.js';
/**
* Direct function wrapper for adding a new task with error handling.
@@ -16,10 +18,12 @@ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules
* @param {string} [args.priority='medium'] - Task priority (high, medium, low)
* @param {string} [args.file] - Path to the tasks file
* @param {string} [args.projectRoot] - Project root directory
* @param {boolean} [args.research] - Whether to use research capabilities for task creation
* @param {Object} log - Logger object
* @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) {
export async function addTaskDirect(args, log, context = {}) {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
@@ -30,6 +34,7 @@ export async function addTaskDirect(args, log) {
// Check required parameters
if (!args.prompt) {
log.error('Missing required parameter: prompt');
disableSilentMode();
return {
success: false,
error: {
@@ -48,13 +53,100 @@ export async function addTaskDirect(args, log) {
log.info(`Adding new task with prompt: "${prompt}", dependencies: [${dependencies.join(', ')}], priority: ${priority}`);
// Extract context parameters for advanced functionality
// Commenting out reportProgress extraction
// const { reportProgress, session } = context;
const { session } = context; // Keep session
// 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
},
{
// reportProgress: context.reportProgress, // Commented out to prevent Cursor stroking out
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,
dependencies,
priority,
{ mcpLog: log },
{
// reportProgress, // Commented out
mcpLog: log,
session,
taskDataFromAI // Pass the parsed AI result
},
'json'
);

View File

@@ -4,7 +4,7 @@
import { analyzeTaskComplexity } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { enableSilentMode, disableSilentMode, isSilentMode, readJSON } from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import path from 'path';
@@ -18,9 +18,12 @@ import path from 'path';
* @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
* @param {string} [args.projectRoot] - Project root directory
* @param {Object} log - Logger object
* @param {Object} [context={}] - Context object containing session data
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function analyzeTaskComplexityDirect(args, log) {
export async function analyzeTaskComplexityDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
@@ -33,6 +36,13 @@ export async function analyzeTaskComplexityDirect(args, log) {
outputPath = path.join(args.projectRoot, outputPath);
}
log.info(`Analyzing task complexity from: ${tasksPath}`);
log.info(`Output report will be saved to: ${outputPath}`);
if (args.research) {
log.info('Using Perplexity AI for research-backed complexity analysis');
}
// Create options object for analyzeTaskComplexity
const options = {
file: tasksPath,
@@ -42,21 +52,42 @@ export async function analyzeTaskComplexityDirect(args, log) {
research: args.research === true
};
log.info(`Analyzing task complexity from: ${tasksPath}`);
log.info(`Output report will be saved to: ${outputPath}`);
if (options.research) {
log.info('Using Perplexity AI for research-backed complexity analysis');
// Enable silent mode to prevent console logs from interfering with JSON response
const wasSilent = isSilentMode();
if (!wasSilent) {
enableSilentMode();
}
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create a logWrapper that matches the expected mcpLog interface as specified in utilities.mdc
const logWrapper = {
info: (message, ...args) => log.info(message, ...args),
warn: (message, ...args) => log.warn(message, ...args),
error: (message, ...args) => log.error(message, ...args),
debug: (message, ...args) => log.debug && log.debug(message, ...args),
success: (message, ...args) => log.info(message, ...args) // Map success to info
};
// Call the core function
await analyzeTaskComplexity(options);
// Restore normal logging
disableSilentMode();
try {
// Call the core function with session and logWrapper as mcpLog
await analyzeTaskComplexity(options, {
session,
mcpLog: logWrapper // Use the wrapper instead of passing log directly
});
} catch (error) {
log.error(`Error in analyzeTaskComplexity: ${error.message}`);
return {
success: false,
error: {
code: 'ANALYZE_ERROR',
message: `Error running complexity analysis: ${error.message}`
}
};
} finally {
// Always restore normal logging in finally block, but only if we enabled it
if (!wasSilent) {
disableSilentMode();
}
}
// Verify the report file was created
if (!fs.existsSync(outputPath)) {
@@ -70,24 +101,48 @@ export async function analyzeTaskComplexityDirect(args, log) {
}
// Read the report file
const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
return {
success: true,
data: {
message: `Task complexity analysis complete. Report saved to ${outputPath}`,
reportPath: outputPath,
reportSummary: {
taskCount: report.length,
highComplexityTasks: report.filter(t => t.complexityScore >= 8).length,
mediumComplexityTasks: report.filter(t => t.complexityScore >= 5 && t.complexityScore < 8).length,
lowComplexityTasks: report.filter(t => t.complexityScore < 5).length,
let report;
try {
report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
// 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;
const mediumComplexityTasks = analysisArray.filter(t => t.complexityScore >= 5 && t.complexityScore < 8).length;
const lowComplexityTasks = analysisArray.filter(t => t.complexityScore < 5).length;
return {
success: true,
data: {
message: `Task complexity analysis complete. Report saved to ${outputPath}`,
reportPath: outputPath,
reportSummary: {
taskCount: analysisArray.length,
highComplexityTasks,
mediumComplexityTasks,
lowComplexityTasks
}
}
}
};
};
} catch (parseError) {
log.error(`Error parsing report file: ${parseError.message}`);
return {
success: false,
error: {
code: 'REPORT_PARSE_ERROR',
message: `Error parsing complexity report: ${parseError.message}`
}
};
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
return {

View File

@@ -3,8 +3,11 @@
*/
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { getAnthropicClientForMCP } from '../utils/ai-client-utils.js';
import path from 'path';
import fs from 'fs';
/**
* Expand all pending tasks with subtasks
@@ -16,43 +19,71 @@ import { findTasksJsonPath } from '../utils/path-utils.js';
* @param {string} [args.file] - Path to the tasks file
* @param {string} [args.projectRoot] - Project root directory
* @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) {
export async function expandAllTasksDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
// Find the tasks.json path
const tasksPath = findTasksJsonPath(args, log);
// Parse parameters
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
const useResearch = args.research === true;
const additionalContext = args.prompt || '';
const forceFlag = args.force === true;
log.info(`Expanding all tasks with ${numSubtasks || 'default'} subtasks each...`);
if (useResearch) {
log.info('Using Perplexity AI for research-backed subtask generation');
}
if (additionalContext) {
log.info(`Additional context: "${additionalContext}"`);
}
if (forceFlag) {
log.info('Force regeneration of subtasks is enabled');
}
// Enable silent mode early to prevent any console output
enableSilentMode();
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Find the tasks.json path
const tasksPath = findTasksJsonPath(args, log);
// Call the core function
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
// Parse parameters
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
const useResearch = args.research === true;
const additionalContext = args.prompt || '';
const forceFlag = args.force === true;
// Restore normal logging
disableSilentMode();
log.info(`Expanding all tasks with ${numSubtasks || 'default'} subtasks each...`);
// The expandAllTasks function doesn't have a return value, so we'll create our own success response
if (useResearch) {
log.info('Using Perplexity AI for research-backed subtask generation');
// Initialize AI client for research-backed expansion
try {
await getAnthropicClientForMCP(session, log);
} catch (error) {
// Ensure silent mode is disabled before returning error
disableSilentMode();
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
}
};
}
}
if (additionalContext) {
log.info(`Additional context: "${additionalContext}"`);
}
if (forceFlag) {
log.info('Force regeneration of subtasks is enabled');
}
// Call the core function with session context for AI operations
// and outputFormat as 'json' to prevent UI elements
const result = await expandAllTasks(
tasksPath,
numSubtasks,
useResearch,
additionalContext,
forceFlag,
{ mcpLog: log, session },
'json' // Use JSON output format to prevent UI elements
);
// The expandAllTasks function now returns a result object
return {
success: true,
data: {
@@ -61,18 +92,21 @@ export async function expandAllTasksDirect(args, log) {
numSubtasks: numSubtasks,
research: useResearch,
prompt: additionalContext,
force: forceFlag
force: forceFlag,
tasksExpanded: result.expandedCount,
totalEligibleTasks: result.tasksToExpand
}
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
} finally {
// Restore normal logging in finally block to ensure it runs even if there's an error
disableSilentMode();
throw error; // Rethrow to be caught by outer catch block
}
} catch (error) {
// Ensure silent mode is disabled
disableSilentMode();
// Ensure silent mode is disabled if an error occurs
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error in expandAllTasksDirect: ${error.message}`);
return {

View File

@@ -4,8 +4,9 @@
*/
import { expandTask } from '../../../../scripts/modules/task-manager.js';
import { readJSON, writeJSON, enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { readJSON, writeJSON, enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
import path from 'path';
import fs from 'fs';
@@ -14,25 +15,54 @@ import fs from 'fs';
*
* @param {Object} args - Command arguments
* @param {Object} log - Logger 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) {
export async function expandTaskDirect(args, log, context = {}) {
const { session } = context;
// Log session root data for debugging
log.info(`Session data in expandTaskDirect: ${JSON.stringify({
hasSession: !!session,
sessionKeys: session ? Object.keys(session) : [],
roots: session?.roots,
rootsStr: JSON.stringify(session?.roots)
})}`);
let tasksPath;
try {
// Find the tasks path first
tasksPath = findTasksJsonPath(args, log);
// If a direct file path is provided, use it directly
if (args.file && fs.existsSync(args.file)) {
log.info(`[expandTaskDirect] Using explicitly provided tasks file: ${args.file}`);
tasksPath = args.file;
} else {
// Find the tasks path through standard logic
log.info(`[expandTaskDirect] No direct file path provided or file not found at ${args.file}, searching using findTasksJsonPath`);
tasksPath = findTasksJsonPath(args, log);
}
} catch (error) {
log.error(`Tasks file not found: ${error.message}`);
log.error(`[expandTaskDirect] Error during tasksPath determination: ${error.message}`);
// Include session roots information in error
const sessionRootsInfo = session ?
`\nSession.roots: ${JSON.stringify(session.roots)}\n` +
`Current Working Directory: ${process.cwd()}\n` +
`Args.projectRoot: ${args.projectRoot}\n` +
`Args.file: ${args.file}\n` :
'\nSession object not available';
return {
success: false,
error: {
code: 'FILE_NOT_FOUND_ERROR',
message: error.message
message: `Error determining tasksPath: ${error.message}${sessionRootsInfo}`
},
fromCache: false
};
}
log.info(`[expandTaskDirect] Determined tasksPath: ${tasksPath}`);
// Validate task ID
const taskId = args.id ? parseInt(args.id, 10) : null;
if (!taskId) {
@@ -51,26 +81,50 @@ export async function expandTaskDirect(args, log) {
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;
const useResearch = args.research === true;
const additionalContext = args.prompt || '';
const force = args.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(`Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${force}`);
log.info(`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}`);
// Read tasks data
log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
const data = readJSON(tasksPath);
log.info(`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`);
if (!data || !data.tasks) {
return {
success: false,
error: {
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksPath}`
},
log.error(`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`);
return {
success: false,
error: {
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
},
fromCache: false
};
}
// Find the specific task
log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
const task = data.tasks.find(t => t.id === taskId);
log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);
if (!task) {
return {
@@ -98,6 +152,20 @@ export async function expandTaskDirect(args, log) {
// Check for existing subtasks
const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
// If the task already has subtasks, just return it (matching core behavior)
if (hasExistingSubtasks) {
log.info(`Task ${taskId} already has ${task.subtasks.length} subtasks`);
return {
success: true,
data: {
task,
subtasksAdded: 0,
hasExistingSubtasks
},
fromCache: false
};
}
// Keep a copy of the task before modification
const originalTask = JSON.parse(JSON.stringify(task));
@@ -121,8 +189,15 @@ export async function expandTaskDirect(args, log) {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call expandTask
const result = await expandTask(taskId, numSubtasks, useResearch, additionalContext);
// Call expandTask with session context to ensure AI client is properly initialized
const result = await expandTask(
tasksPath,
taskId,
numSubtasks,
useResearch,
additionalContext,
{ mcpLog: log, session } // Only pass mcpLog and session, NOT reportProgress
);
// Restore normal logging
disableSilentMode();

View File

@@ -8,19 +8,39 @@ import fs from 'fs';
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
*
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output options.
* @param {Object} 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) {
export async function parsePRDDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Check required parameters
// Initialize AI client for PRD parsing
let aiClient;
try {
aiClient = getAnthropicClientForMCP(session, log);
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
// Parameter validation and path resolution
if (!args.input) {
const errorMessage = 'No input file specified. Please provide an input PRD document path.';
log.error(errorMessage);
@@ -67,38 +87,54 @@ export async function parsePRDDirect(args, log) {
log.info(`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`);
// 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();
// Execute core parsePRD function (which is not async but we'll await it to maintain consistency)
await parsePRD(inputPath, outputPath, numTasks);
// Restore normal logging
disableSilentMode();
// 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'));
log.info(`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`);
try {
// Execute core parsePRD function with AI client
await parsePRD(inputPath, outputPath, numTasks, {
mcpLog: logWrapper,
session
}, aiClient, modelConfig);
return {
success: true,
data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0,
outputPath
},
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
};
// 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'));
log.info(`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`);
return {
success: true,
data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0,
outputPath
},
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) {
// Make sure to restore normal logging even if there's an error

View File

@@ -5,7 +5,7 @@
import { setTaskStatus } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for setTaskStatus with error handling.
@@ -58,26 +58,22 @@ export async function setTaskStatusDirect(args, log) {
}
// Execute core setTaskStatus function
// We need to handle the arguments correctly - this function expects tasksPath, taskIdInput, newStatus
const taskId = args.id;
const newStatus = args.status;
log.info(`Setting task ${taskId} status to "${newStatus}"`);
// Call the core function
// Call the core function with proper silent mode handling
let result;
enableSilentMode(); // Enable silent mode before calling core function
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
await setTaskStatus(tasksPath, taskId, newStatus);
// Restore normal logging
disableSilentMode();
// Call the core function
await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log });
log.info(`Successfully set task ${taskId} status to ${newStatus}`);
// Return success data
return {
result = {
success: true,
data: {
message: `Successfully updated task ${taskId} status to "${newStatus}"`,
@@ -88,17 +84,24 @@ export async function setTaskStatusDirect(args, log) {
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error setting task status: ${error.message}`);
return {
result = {
success: false,
error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' },
fromCache: false
};
} finally {
// ALWAYS restore normal logging in finally block
disableSilentMode();
}
return result;
} catch (error) {
// Ensure silent mode is disabled if there was an uncaught error in the outer try block
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error setting task status: ${error.message}`);
return {
success: false,

View File

@@ -6,15 +6,19 @@
import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-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 and file path 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 updateSubtaskByIdDirect(args, log) {
export async function updateSubtaskByIdDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
@@ -41,8 +45,19 @@ export async function updateSubtaskByIdDirect(args, log) {
// Validate subtask ID format
const subtaskId = args.id;
if (typeof subtaskId !== 'string' || !subtaskId.includes('.')) {
const errorMessage = `Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`;
if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') {
const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`;
log.error(errorMessage);
return {
success: false,
error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage },
fromCache: false
};
}
const subtaskIdStr = String(subtaskId);
if (!subtaskIdStr.includes('.')) {
const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`;
log.error(errorMessage);
return {
success: false,
@@ -67,14 +82,46 @@ export async function updateSubtaskByIdDirect(args, log) {
// Get research flag
const useResearch = args.research === true;
log.info(`Updating subtask with ID ${subtaskId} with prompt "${args.prompt}" and research: ${useResearch}`);
log.info(`Updating subtask with ID ${subtaskIdStr} with prompt "${args.prompt}" and research: ${useResearch}`);
// Initialize the appropriate AI client based on research flag
try {
if (useResearch) {
// Initialize Perplexity client
await getPerplexityClientForMCP(session);
} else {
// Initialize Anthropic client
await getAnthropicClientForMCP(session);
}
} catch (error) {
log.error(`AI client initialization error: ${error.message}`);
return {
success: false,
error: { code: 'AI_CLIENT_ERROR', message: error.message || 'Failed to initialize AI client' },
fromCache: false
};
}
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create a logger wrapper object to handle logging without breaking the mcpLog[level] calls
// This ensures outputFormat is set to 'json' while still supporting proper logging
const logWrapper = {
info: (message) => log.info(message),
warn: (message) => log.warn(message),
error: (message) => log.error(message),
debug: (message) => log.debug && log.debug(message),
success: (message) => log.info(message) // Map success to info if needed
};
// Execute core updateSubtaskById function
const updatedSubtask = await updateSubtaskById(tasksPath, subtaskId, args.prompt, useResearch);
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
const updatedSubtask = await updateSubtaskById(tasksPath, subtaskIdStr, args.prompt, useResearch, {
session,
mcpLog: logWrapper
});
// Restore normal logging
disableSilentMode();
@@ -95,9 +142,9 @@ export async function updateSubtaskByIdDirect(args, log) {
return {
success: true,
data: {
message: `Successfully updated subtask with ID ${subtaskId}`,
subtaskId,
parentId: subtaskId.split('.')[0],
message: `Successfully updated subtask with ID ${subtaskIdStr}`,
subtaskId: subtaskIdStr,
parentId: subtaskIdStr.split('.')[0],
subtask: updatedSubtask,
tasksPath,
useResearch

View File

@@ -6,15 +6,22 @@
import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/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 and file path 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 updateTaskByIdDirect(args, log) {
export async function updateTaskByIdDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Updating task with args: ${JSON.stringify(args)}`);
@@ -78,31 +85,81 @@ export async function updateTaskByIdDirect(args, log) {
// Get research flag
const useResearch = args.research === true;
// Initialize appropriate AI client based on research flag
let aiClient;
try {
if (useResearch) {
log.info('Using Perplexity AI for research-backed task update');
aiClient = await getPerplexityClientForMCP(session, log);
} else {
log.info('Using Claude AI for task update');
aiClient = getAnthropicClientForMCP(session, log);
}
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
log.info(`Updating task with ID ${taskId} with prompt "${args.prompt}" and research: ${useResearch}`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Execute core updateTaskById function
await updateTaskById(tasksPath, taskId, args.prompt, useResearch);
// Restore normal logging
disableSilentMode();
// Since updateTaskById doesn't return a value but modifies the tasks file,
// we'll return a success message
return {
success: true,
data: {
message: `Successfully updated task with ID ${taskId} based on the prompt`,
taskId,
tasksPath,
useResearch
},
fromCache: false // This operation always modifies state and should never be cached
};
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
await updateTaskById(
tasksPath,
taskId,
args.prompt,
useResearch,
{
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
session
},
'json'
);
// Since updateTaskById doesn't return a value but modifies the tasks file,
// we'll return a success message
return {
success: true,
data: {
message: `Successfully updated task with ID ${taskId} based on the prompt`,
taskId,
tasksPath,
useResearch
},
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
log.error(`Error updating task by ID: ${error.message}`);
return {
success: false,
error: { code: 'UPDATE_TASK_ERROR', message: error.message || 'Unknown error updating task' },
fromCache: false
};
} finally {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Error updating task by ID: ${error.message}`);

View File

@@ -6,18 +6,40 @@
import { updateTasks } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
/**
* Direct function wrapper for updating tasks based on new context/prompt.
*
* @param {Object} args - Command arguments containing fromId, prompt, useResearch and file path 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 updateTasksDirect(args, log) {
export async function updateTasksDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
// Check for the common mistake of using 'id' instead of 'from'
if (args.id !== undefined && args.from === undefined) {
const errorMessage = "You specified 'id' parameter but 'update' requires 'from' parameter. Use 'from' for this tool or use 'update_task' tool if you want to update a single task.";
log.error(errorMessage);
return {
success: false,
error: {
code: 'PARAMETER_MISMATCH',
message: errorMessage,
suggestion: "Use 'from' parameter instead of 'id', or use the 'update_task' tool for single task updates"
},
fromCache: false
};
}
// Check required parameters
if (!args.from) {
const errorMessage = 'No from ID specified. Please provide a task ID to start updating from.';
@@ -72,17 +94,45 @@ export async function updateTasksDirect(args, log) {
// Get research flag
const useResearch = args.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 "${args.prompt}" and research: ${useResearch}`);
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Execute core updateTasks function
await updateTasks(tasksPath, fromId, args.prompt, useResearch);
// Restore normal logging
disableSilentMode();
// Execute core updateTasks function, passing the AI client and session
await updateTasks(
tasksPath,
fromId,
args.prompt,
useResearch,
{
mcpLog: log,
session
}
);
// Since updateTasks doesn't return a value but modifies the tasks file,
// we'll return a success message
@@ -97,9 +147,15 @@ export async function updateTasksDirect(args, log) {
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();
throw error; // Rethrow to be caught by outer catch block
}
} catch (error) {
// Ensure silent mode is disabled

View File

@@ -32,6 +32,15 @@ import { removeTaskDirect } from './direct-functions/remove-task.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],

View File

@@ -179,7 +179,11 @@ function findTasksJsonInDirectory(dirPath, explicitFilePath, log) {
// Find the first existing path
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
log.info(`Checking if exists: ${p}`);
const exists = fs.existsSync(p);
log.info(`Path ${p} exists: ${exists}`);
if (exists) {
log.info(`Found tasks file at: ${p}`);
// Store the project root for future use
lastFoundProjectRoot = dirPath;

View File

@@ -69,9 +69,10 @@ class TaskMasterMCPServer {
await this.init();
}
// Start the FastMCP server
// Start the FastMCP server with increased timeout
await this.server.start({
transportType: "stdio",
timeout: 120000 // 2 minutes timeout (in milliseconds)
});
return this;

View File

@@ -1,4 +1,5 @@
import chalk from "chalk";
import { isSilentMode } from "../../scripts/modules/utils.js";
// Define log levels
const LOG_LEVELS = {
@@ -20,6 +21,11 @@ const LOG_LEVEL = process.env.LOG_LEVEL
* @param {...any} args - Arguments to log
*/
function log(level, ...args) {
// Skip logging if silent mode is enabled
if (isSilentMode()) {
return;
}
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray("[DEBUG]"),

View File

@@ -5,61 +5,53 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse,
createContentResponse,
getProjectRootFromSession
getProjectRootFromSession,
executeTaskMasterCommand,
handleApiResult
} from "./utils.js";
import { addTaskDirect } from "../core/task-master-core.js";
/**
* Register the add-task tool with the MCP server
* Register the addTask tool with the MCP server
* @param {Object} server - FastMCP server instance
* @param {AsyncOperationManager} asyncManager - The async operation manager instance.
*/
export function registerAddTaskTool(server, asyncManager) {
export function registerAddTaskTool(server) {
server.addTool({
name: "add_task",
description: "Starts adding a new task using AI in the background.",
description: "Add a new task using AI",
parameters: z.object({
prompt: z.string().describe("Description of the task to add"),
dependencies: z.string().optional().describe("Comma-separated list of task IDs this task depends on"),
priority: z.string().optional().describe("Task priority (high, medium, low)"),
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
projectRoot: z.string().optional().describe("Root directory of the project"),
research: z.boolean().optional().describe("Whether to use research capabilities for task creation")
}),
execute: async (args, context) => {
const { log, reportProgress, session } = context;
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`MCP add_task request received with prompt: \"${args.prompt}\"`);
log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
if (!args.prompt) {
return createErrorResponse("Prompt is required for add_task.", "VALIDATION_ERROR");
}
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const directArgs = {
projectRoot: rootFolder,
...args
};
const operationId = asyncManager.addOperation(addTaskDirect, directArgs, context);
log.info(`Started background operation for add_task. Operation ID: ${operationId}`);
return createContentResponse({
message: "Add task operation started successfully.",
operationId: operationId
});
// Call the direct function
const result = await addTaskDirect({
...args,
projectRoot: rootFolder
}, log, { reportProgress, session });
// Return the result
return handleApiResult(result, log);
} catch (error) {
log.error(`Error initiating add_task operation: ${error.message}`, { stack: error.stack });
return createErrorResponse(`Failed to start add task operation: ${error.message}`, "ADD_TASK_INIT_ERROR");
log.error(`Error in add-task tool: ${error.message}`);
return createErrorResponse(error.message);
}
}
});

View File

@@ -27,10 +27,9 @@ export function registerAnalyzeTool(server) {
research: z.boolean().optional().describe("Use Perplexity AI for research-backed complexity analysis"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,9 +41,7 @@ export function registerAnalyzeTool(server) {
const result = await analyzeTaskComplexityDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`);

View File

@@ -20,17 +20,16 @@ export function registerExpandAllTool(server) {
name: "expand_all",
description: "Expand all pending tasks into subtasks",
parameters: z.object({
num: z.union([z.number(), z.string()]).optional().describe("Number of subtasks to generate for each task"),
num: z.string().optional().describe("Number of subtasks to generate for each task"),
research: z.boolean().optional().describe("Enable Perplexity AI for research-backed subtask generation"),
prompt: z.string().optional().describe("Additional context to guide subtask generation"),
force: z.boolean().optional().describe("Force regeneration of subtasks for tasks that already have them"),
file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,9 +41,7 @@ export function registerExpandAllTool(server) {
const result = await expandAllTasksDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully expanded all tasks: ${result.data.message}`);

View File

@@ -10,6 +10,8 @@ import {
getProjectRootFromSession
} from "./utils.js";
import { expandTaskDirect } from "../core/task-master-core.js";
import fs from "fs";
import path from "path";
/**
* Register the expand-task tool with the MCP server
@@ -21,10 +23,9 @@ export function registerExpandTaskTool(server) {
description: "Expand a task into subtasks for detailed implementation",
parameters: z.object({
id: z.string().describe("ID of task to expand"),
num: z.union([z.number(), z.string()]).optional().describe("Number of subtasks to generate"),
num: z.union([z.string(), z.number()]).optional().describe("Number of subtasks to generate"),
research: z.boolean().optional().describe("Use Perplexity AI for research-backed generation"),
prompt: z.string().optional().describe("Additional context for subtask generation"),
force: z.boolean().optional().describe("Force regeneration even for tasks that already have subtasks"),
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z
.string()
@@ -33,11 +34,11 @@ export function registerExpandTaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`Expanding task with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
log.info(`Starting expand-task with args: ${JSON.stringify(args)}`);
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
@@ -45,19 +46,27 @@ export function registerExpandTaskTool(server) {
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await expandTaskDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
log.info(`Project root resolved to: ${rootFolder}`);
// await reportProgress({ progress: 100 });
// Check for tasks.json in the standard locations
const tasksJsonPath = path.join(rootFolder, 'tasks', 'tasks.json');
if (result.success) {
log.info(`Successfully expanded task with ID ${args.id}`);
if (fs.existsSync(tasksJsonPath)) {
log.info(`Found tasks.json at ${tasksJsonPath}`);
// Add the file parameter directly to args
args.file = tasksJsonPath;
} else {
log.error(`Failed to expand task: ${result.error?.message || 'Unknown error'}`);
log.warn(`Could not find tasks.json at ${tasksJsonPath}`);
}
// Call direct function with only session in the context, not reportProgress
// Use the pattern recommended in the MCP guidelines
const result = await expandTaskDirect({
...args,
projectRoot: rootFolder
}, 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}`);

View File

@@ -28,7 +28,6 @@ import { registerAddDependencyTool } from "./add-dependency.js";
import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { asyncOperationManager } from '../core/utils/async-manager.js';
import { registerGetOperationStatusTool } from './get-operation-status.js';
/**
* Register all Task Master tools with the MCP server
@@ -61,7 +60,6 @@ export function registerTaskMasterTools(server, asyncManager) {
registerAddDependencyTool(server);
registerRemoveTaskTool(server);
registerInitializeProjectTool(server);
registerGetOperationStatusTool(server, asyncManager);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;

View File

@@ -31,7 +31,7 @@ export function registerParsePRDTool(server) {
"Root directory of the project (default: automatically detected from session or CWD)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
@@ -45,9 +45,7 @@ export function registerParsePRDTool(server) {
const result = await parsePRDDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully parsed PRD: ${result.data.message}`);

View File

@@ -34,11 +34,11 @@ export function registerSetTaskStatusTool(server) {
"Root directory of the project (default: automatically detected)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
// await reportProgress({ progress: 0 });
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
@@ -46,19 +46,20 @@ export function registerSetTaskStatusTool(server) {
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
// Call the direct function with the project root
const result = await setTaskStatusDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
...args,
projectRoot: rootFolder
}, log);
// Log the result
if (result.success) {
log.info(`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`);
} else {
log.error(`Failed to update task status: ${result.error?.message || 'Unknown error'}`);
}
// Format and return the result
return handleApiResult(result, log, 'Error setting task status');
} catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`);

View File

@@ -31,10 +31,9 @@ export function registerUpdateSubtaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateSubtaskTool(server) {
const result = await updateSubtaskByIdDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated subtask with ID ${args.id}`);

View File

@@ -20,7 +20,7 @@ export function registerUpdateTaskTool(server) {
name: "update_task",
description: "Updates a single task by ID with new information or context provided in the prompt.",
parameters: z.object({
id: z.union([z.number(), z.string()]).describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
id: z.string().describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
prompt: z.string().describe("New information or context to incorporate into the task"),
research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"),
file: z.string().optional().describe("Path to the tasks file"),
@@ -31,10 +31,9 @@ export function registerUpdateTaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating task with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateTaskTool(server) {
const result = await updateTaskByIdDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated task with ID ${args.id}`);

View File

@@ -18,9 +18,9 @@ import { updateTasksDirect } from "../core/task-master-core.js";
export function registerUpdateTool(server) {
server.addTool({
name: "update",
description: "Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt.",
description: "Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task.",
parameters: z.object({
from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating (inclusive)"),
from: z.string().describe("Task ID from which to start updating (inclusive). IMPORTANT: This tool uses 'from', not 'id'"),
prompt: z.string().describe("Explanation of changes or new context to apply"),
research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"),
file: z.string().optional().describe("Path to the tasks file"),
@@ -31,10 +31,9 @@ export function registerUpdateTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateTool(server) {
const result = await updateTasksDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated tasks from ID ${args.from}: ${result.data.message}`);

View File

@@ -75,21 +75,43 @@ function getProjectRoot(projectRootRaw, log) {
*/
function getProjectRootFromSession(session, log) {
try {
// Add detailed logging of session structure
log.info(`Session object: ${JSON.stringify({
hasSession: !!session,
hasRoots: !!session?.roots,
rootsType: typeof session?.roots,
isRootsArray: Array.isArray(session?.roots),
rootsLength: session?.roots?.length,
firstRoot: session?.roots?.[0],
hasRootsRoots: !!session?.roots?.roots,
rootsRootsType: typeof session?.roots?.roots,
isRootsRootsArray: Array.isArray(session?.roots?.roots),
rootsRootsLength: session?.roots?.roots?.length,
firstRootsRoot: session?.roots?.roots?.[0]
})}`);
// ALWAYS ensure we return a valid path for project root
const cwd = process.cwd();
// 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;
}
@@ -106,24 +128,15 @@ function getProjectRootFromSession(session, log) {
if (fs.existsSync(path.join(projectRoot, '.cursor')) ||
fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
fs.existsSync(path.join(projectRoot, 'package.json'))) {
log.info(`Found project root from server path: ${projectRoot}`);
return projectRoot;
}
}
}
// If we get here, we'll try process.cwd() but only if it's not "/"
const cwd = process.cwd();
if (cwd !== '/') {
return cwd;
}
// Last resort: try to derive from the server path we found earlier
if (serverPath) {
const mcpServerIndex = serverPath.indexOf('mcp-server');
return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : cwd;
}
throw new Error('Could not determine project root');
// ALWAYS ensure we return a valid path as a last resort
log.info(`Using current working directory as ultimate fallback: ${cwd}`);
return cwd;
} catch (e) {
// If we have a server path, use it as a basis for project root
const serverPath = process.argv[1];
@@ -171,18 +184,20 @@ function handleApiResult(result, log, errorPrefix = 'API error', processFunction
}
/**
* Execute a Task Master CLI command using child_process
* @param {string} command - The command to execute
* @param {Object} log - The logger object from FastMCP
* Executes a task-master CLI command synchronously.
* @param {string} command - The command to execute (e.g., 'add-task')
* @param {Object} log - Logger instance
* @param {Array} args - Arguments for the command
* @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
* @param {Object|null} customEnv - Optional object containing environment variables to pass to the child process
* @returns {Object} - The result of the command execution
*/
function executeTaskMasterCommand(
command,
log,
args = [],
projectRootRaw = null
projectRootRaw = null,
customEnv = null // Changed from session to customEnv
) {
try {
// Normalize project root internally using the getProjectRoot utility
@@ -201,8 +216,13 @@ function executeTaskMasterCommand(
const spawnOptions = {
encoding: "utf8",
cwd: cwd,
// Merge process.env with customEnv, giving precedence to customEnv
env: { ...process.env, ...(customEnv || {}) }
};
// Log the environment being passed (optional, for debugging)
// log.info(`Spawn options env: ${JSON.stringify(spawnOptions.env)}`);
// Execute the command using the global task-master CLI or local script
// Try the global CLI first
let result = spawnSync("task-master", fullArgs, spawnOptions);
@@ -210,6 +230,7 @@ function executeTaskMasterCommand(
// If global CLI is not available, try fallback to the local script
if (result.error && result.error.code === "ENOENT") {
log.info("Global task-master not found, falling back to local script");
// Pass the same spawnOptions (including env) to the fallback
result = spawnSync("node", ["scripts/dev.js", ...fullArgs], spawnOptions);
}