diff --git a/.taskmasterconfig b/.taskmasterconfig index a38f2bd8..9b86628e 100644 --- a/.taskmasterconfig +++ b/.taskmasterconfig @@ -1,31 +1,31 @@ { - "models": { - "main": { - "provider": "anthropic", - "modelId": "claude-3-7-sonnet-20250219", - "maxTokens": 100000, - "temperature": 0.2 - }, - "research": { - "provider": "perplexity", - "modelId": "sonar-pro", - "maxTokens": 8700, - "temperature": 0.1 - }, - "fallback": { - "provider": "anthropic", - "modelId": "claude-3-5-sonnet-20241022", - "maxTokens": 120000, - "temperature": 0.2 - } - }, - "global": { - "logLevel": "info", - "debug": false, - "defaultSubtasks": 5, - "defaultPriority": "medium", - "projectName": "Taskmaster", - "ollamaBaseUrl": "http://localhost:11434/api", - "azureOpenaiBaseUrl": "https://your-endpoint.openai.azure.com/" - } -} + "models": { + "main": { + "provider": "openai", + "modelId": "gpt-4o", + "maxTokens": 100000, + "temperature": 0.2 + }, + "research": { + "provider": "perplexity", + "modelId": "sonar-pro", + "maxTokens": 8700, + "temperature": 0.1 + }, + "fallback": { + "provider": "anthropic", + "modelId": "claude-3-5-sonnet-20241022", + "maxTokens": 120000, + "temperature": 0.2 + } + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Taskmaster", + "ollamaBaseUrl": "http://localhost:11434/api", + "azureOpenaiBaseUrl": "https://your-endpoint.openai.azure.com/" + } +} \ No newline at end of file diff --git a/mcp-server/src/core/direct-functions/update-task-by-id.js b/mcp-server/src/core/direct-functions/update-task-by-id.js index 059fa5ff..fd979be9 100644 --- a/mcp-server/src/core/direct-functions/update-task-by-id.js +++ b/mcp-server/src/core/direct-functions/update-task-by-id.js @@ -6,30 +6,40 @@ import { updateTaskById } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, - disableSilentMode + disableSilentMode, + isSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for updateTaskById with error handling. * - * @param {Object} args - Command arguments containing id, prompt, useResearch and tasksJsonPath. + * @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} log - Logger object. * @param {Object} context - Context object containing session data. * @returns {Promise} - Result object with success status and data/error information. */ export async function updateTaskByIdDirect(args, log, context = {}) { - const { session } = context; // Only extract session, not reportProgress - // Destructure expected args, including the resolved tasksJsonPath - const { tasksJsonPath, id, prompt, research } = args; + const { session } = context; + // Destructure expected args, including projectRoot + const { tasksJsonPath, id, prompt, research, projectRoot } = args; + + const logWrapper = createLogWrapper(log); try { - log.info(`Updating task with args: ${JSON.stringify(args)}`); + logWrapper.info( + `Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}` + ); // Check if tasksJsonPath was provided if (!tasksJsonPath) { const errorMessage = 'tasksJsonPath is required but was not provided.'; - log.error(errorMessage); + logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_ARGUMENT', message: errorMessage }, @@ -41,7 +51,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) { if (!id) { const errorMessage = 'No task ID specified. Please provide a task ID to update.'; - log.error(errorMessage); + logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_TASK_ID', message: errorMessage }, @@ -52,7 +62,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.'; - log.error(errorMessage); + logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_PROMPT', message: errorMessage }, @@ -71,7 +81,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").`; - log.error(errorMessage); + logWrapper.error(errorMessage); return { success: false, error: { code: 'INVALID_TASK_ID', message: errorMessage }, @@ -89,66 +99,80 @@ export async function updateTaskByIdDirect(args, log, context = {}) { // Get research flag const useResearch = research === true; - log.info( + logWrapper.info( `Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}` ); - try { - // Enable silent mode to prevent console logs from interfering with JSON response + const wasSilent = isSilentMode(); + if (!wasSilent) { enableSilentMode(); + } - // Create the logger wrapper using the utility function - const mcpLog = createLogWrapper(log); - + try { // Execute core updateTaskById function with proper parameters - await updateTaskById( + const updatedTask = await updateTaskById( tasksPath, taskId, prompt, useResearch, { - mcpLog, // Pass the wrapped logger - session + mcpLog: logWrapper, + session, + projectRoot }, 'json' ); - // Since updateTaskById doesn't return a value but modifies the tasks file, - // we'll return a success message + // 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); return { success: true, data: { - message: `Successfully updated task with ID ${taskId} based on the prompt`, - taskId, - tasksPath: tasksPath, // Return the used path - useResearch + message: successMessage, + taskId: taskId, + tasksPath: tasksPath, + useResearch: useResearch, + updated: true, + updatedTask: updatedTask }, - fromCache: false // This operation always modifies state and should never be cached + fromCache: false }; } catch (error) { - log.error(`Error updating task by ID: ${error.message}`); + logWrapper.error(`Error updating task by ID: ${error.message}`); return { success: false, error: { - code: 'UPDATE_TASK_ERROR', + code: 'UPDATE_TASK_CORE_ERROR', message: error.message || 'Unknown error updating task' }, fromCache: false }; } finally { - // Make sure to restore normal logging even if there's an error - disableSilentMode(); + if (!wasSilent && isSilentMode()) { + disableSilentMode(); + } } } catch (error) { - // Ensure silent mode is disabled - disableSilentMode(); - - log.error(`Error updating task by ID: ${error.message}`); + logWrapper.error(`Setup error in updateTaskByIdDirect: ${error.message}`); + if (isSilentMode()) disableSilentMode(); return { success: false, error: { - code: 'UPDATE_TASK_ERROR', - message: error.message || 'Unknown error updating task' + code: 'DIRECT_FUNCTION_SETUP_ERROR', + message: error.message || 'Unknown setup error' }, fromCache: false }; diff --git a/mcp-server/src/tools/update-task.js b/mcp-server/src/tools/update-task.js index 89dc4ca8..d5eb96c9 100644 --- a/mcp-server/src/tools/update-task.js +++ b/mcp-server/src/tools/update-task.js @@ -4,6 +4,7 @@ */ import { z } from 'zod'; +import path from 'path'; // Import path import { handleApiResult, createErrorResponse, @@ -23,7 +24,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() + .string() // ID can be number or string like "1.2" .describe( "ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool." ), @@ -40,59 +41,65 @@ export function registerUpdateTaskTool(server) { .describe('The directory of the project. Must be an absolute path.') }), execute: async (args, { log, session }) => { + const toolName = 'update_task'; try { - log.info(`Updating task with args: ${JSON.stringify(args)}`); + log.info( + `Executing ${toolName} tool with args: ${JSON.stringify(args)}` + ); - // Get project root from args or session - const rootFolder = - args.projectRoot || getProjectRootFromSession(session, log); - - // Ensure project root was determined - if (!rootFolder) { + // 1. Get Project Root + const rootFolder = args.projectRoot; + if (!rootFolder || !path.isAbsolute(rootFolder)) { + log.error( + `${toolName}: projectRoot is required and must be absolute.` + ); return createErrorResponse( - 'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.' + 'projectRoot is required and must be absolute.' ); } + log.info(`${toolName}: Project root: ${rootFolder}`); - // Resolve the path to tasks.json + // 2. Resolve Tasks Path let tasksJsonPath; try { tasksJsonPath = findTasksJsonPath( - { projectRoot: rootFolder, file: args.file }, + { projectRoot: rootFolder, file: args.file }, // Pass root and optional relative file log ); + log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`); } catch (error) { - log.error(`Error finding tasks.json: ${error.message}`); + log.error(`${toolName}: Error finding tasks.json: ${error.message}`); return createErrorResponse( - `Failed to find tasks.json: ${error.message}` + `Failed to find tasks.json within project root '${rootFolder}': ${error.message}` ); } + // 3. Call Direct Function - Include projectRoot const result = await updateTaskByIdDirect( { - // Pass the explicitly resolved path - tasksJsonPath: tasksJsonPath, - // Pass other relevant args + tasksJsonPath: tasksJsonPath, // Pass resolved path id: args.id, prompt: args.prompt, - research: args.research + research: args.research, + projectRoot: rootFolder // <<< Pass projectRoot HERE }, log, - { session } + { session } // Pass context with session ); - if (result.success) { - log.info(`Successfully updated task with ID ${args.id}`); - } else { - log.error( - `Failed to update task: ${result.error?.message || 'Unknown error'}` - ); - } - + // 4. Handle Result + log.info( + `${toolName}: Direct function result: success=${result.success}` + ); + // Pass the actual data from the result (contains updated task or message) return handleApiResult(result, log, 'Error updating task'); } catch (error) { - log.error(`Error in update_task tool: ${error.message}`); - return createErrorResponse(error.message); + log.error( + `Critical error in ${toolName} tool execute: ${error.message}` + ); + return createErrorResponse( + `Internal tool error (${toolName}): ${error.message}` + ); } } }); diff --git a/scripts/modules/task-manager/update-task-by-id.js b/scripts/modules/task-manager/update-task-by-id.js index ec4e3f6c..b2bdb107 100644 --- a/scripts/modules/task-manager/update-task-by-id.js +++ b/scripts/modules/task-manager/update-task-by-id.js @@ -70,29 +70,80 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) { let cleanedResponse = text.trim(); const originalResponseForDebug = cleanedResponse; + let parseMethodUsed = 'raw'; // Keep track of which method worked - // Extract from Markdown code block first - const codeBlockMatch = cleanedResponse.match( - /```(?:json)?\s*([\s\S]*?)\s*```/ - ); - if (codeBlockMatch) { - cleanedResponse = codeBlockMatch[1].trim(); - report('info', 'Extracted JSON content from Markdown code block.'); - } else { - // If no code block, find first '{' and last '}' for the object - const firstBrace = cleanedResponse.indexOf('{'); - const lastBrace = cleanedResponse.lastIndexOf('}'); - if (firstBrace !== -1 && lastBrace > firstBrace) { - cleanedResponse = cleanedResponse.substring(firstBrace, lastBrace + 1); - report('info', 'Extracted content between first { and last }.'); - } else { - report( - 'warn', - 'Response does not appear to contain a JSON object structure. Parsing raw response.' - ); + // --- NEW Step 1: Try extracting between {} first --- + const firstBraceIndex = cleanedResponse.indexOf('{'); + const lastBraceIndex = cleanedResponse.lastIndexOf('}'); + let potentialJsonFromBraces = null; + + if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) { + potentialJsonFromBraces = cleanedResponse.substring( + firstBraceIndex, + lastBraceIndex + 1 + ); + if (potentialJsonFromBraces.length <= 2) { + potentialJsonFromBraces = null; // Ignore empty braces {} } } + // If {} extraction yielded something, try parsing it immediately + if (potentialJsonFromBraces) { + try { + const testParse = JSON.parse(potentialJsonFromBraces); + // It worked! Use this as the primary cleaned response. + cleanedResponse = potentialJsonFromBraces; + parseMethodUsed = 'braces'; + report( + 'info', + 'Successfully parsed JSON content extracted between first { and last }.' + ); + } catch (e) { + report( + 'info', + 'Content between {} looked promising but failed initial parse. Proceeding to other methods.' + ); + // Reset cleanedResponse to original if brace parsing failed + cleanedResponse = originalResponseForDebug; + } + } + + // --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction --- + if (parseMethodUsed === 'raw') { + const codeBlockMatch = cleanedResponse.match( + /```(?:json|javascript)?\s*([\s\S]*?)\s*```/i + ); + if (codeBlockMatch) { + cleanedResponse = codeBlockMatch[1].trim(); + parseMethodUsed = 'codeblock'; + report('info', 'Extracted JSON content from Markdown code block.'); + } else { + // --- Step 3: If code block failed, try stripping prefixes --- + const commonPrefixes = [ + 'json\n', + 'javascript\n' + // ... other prefixes ... + ]; + let prefixFound = false; + for (const prefix of commonPrefixes) { + if (cleanedResponse.toLowerCase().startsWith(prefix)) { + cleanedResponse = cleanedResponse.substring(prefix.length).trim(); + parseMethodUsed = 'prefix'; + report('info', `Stripped prefix: "${prefix.trim()}"`); + prefixFound = true; + break; + } + } + if (!prefixFound) { + report( + 'warn', + 'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.' + ); + } + } + } + + // --- Step 4: Attempt final parse --- let parsedTask; try { parsedTask = JSON.parse(cleanedResponse); @@ -168,7 +219,7 @@ async function updateTaskById( context = {}, outputFormat = 'text' ) { - const { session, mcpLog } = context; + const { session, mcpLog, projectRoot } = context; const logFn = mcpLog || consoleLog; const isMCP = !!mcpLog; @@ -343,7 +394,8 @@ The changes described in the prompt should be thoughtfully applied to make the t prompt: userPrompt, systemPrompt: systemPrompt, role, - session + session, + projectRoot }); report('success', 'Successfully received text response from AI service'); // --- End AI Service Call --- diff --git a/scripts/modules/task-manager/update-tasks.js b/scripts/modules/task-manager/update-tasks.js index acd3f0e1..9046a97f 100644 --- a/scripts/modules/task-manager/update-tasks.js +++ b/scripts/modules/task-manager/update-tasks.js @@ -43,13 +43,12 @@ const updatedTaskArraySchema = z.array(updatedTaskSchema); * Parses an array of task objects from AI's text response. * @param {string} text - Response text from AI. * @param {number} expectedCount - Expected number of tasks. - * @param {Function | Object} logFn - The logging function (consoleLog) or MCP log object. + * @param {Function | Object} logFn - The logging function or MCP log object. * @param {boolean} isMCP - Flag indicating if logFn is MCP logger. * @returns {Array} Parsed and validated tasks array. * @throws {Error} If parsing or validation fails. */ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { - // Helper for consistent logging inside parser const report = (level, ...args) => { if (isMCP) { if (typeof logFn[level] === 'function') logFn[level](...args); @@ -70,32 +69,70 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { let cleanedResponse = text.trim(); const originalResponseForDebug = cleanedResponse; - // Extract from Markdown code block first + // Step 1: Attempt to extract from Markdown code block first const codeBlockMatch = cleanedResponse.match( - /```(?:json)?\s*([\s\S]*?)\s*```/ + /```(?:json|javascript)?\s*([\s\S]*?)\s*```/i // Made case-insensitive, allow js ); if (codeBlockMatch) { cleanedResponse = codeBlockMatch[1].trim(); - report('info', 'Extracted JSON content from Markdown code block.'); + report('info', 'Extracted content from Markdown code block.'); } else { - // If no code block, find first '[' and last ']' for the array + // Step 2 (if no code block): Attempt to strip common language identifiers/intro text + // List common prefixes AI might add before JSON + const commonPrefixes = [ + 'json\n', + 'javascript\n', + 'python\n', // Language identifiers + 'here are the updated tasks:', + 'here is the updated json:', // Common intro phrases + 'updated tasks:', + 'updated json:', + 'response:', + 'output:' + ]; + let prefixFound = false; + for (const prefix of commonPrefixes) { + if (cleanedResponse.toLowerCase().startsWith(prefix)) { + cleanedResponse = cleanedResponse.substring(prefix.length).trim(); + report('info', `Stripped prefix: "${prefix.trim()}"`); + prefixFound = true; + break; // Stop after finding the first matching prefix + } + } + + // Step 3 (if no code block and no prefix stripped, or after stripping): Find first '[' and last ']' + // This helps if there's still leading/trailing text around the array const firstBracket = cleanedResponse.indexOf('['); const lastBracket = cleanedResponse.lastIndexOf(']'); if (firstBracket !== -1 && lastBracket > firstBracket) { - cleanedResponse = cleanedResponse.substring( + const extractedArray = cleanedResponse.substring( firstBracket, lastBracket + 1 ); - report('info', 'Extracted content between first [ and last ].'); - } else { + // Basic check to see if the extraction looks like JSON + if (extractedArray.length > 2) { + // More than just '[]' + cleanedResponse = extractedArray; // Use the extracted array content + if (!codeBlockMatch && !prefixFound) { + // Only log if we didn't already log extraction/stripping + report('info', 'Extracted content between first [ and last ].'); + } + } else if (!codeBlockMatch && !prefixFound) { + report( + 'warn', + 'Found brackets "[]" but content seems empty or invalid. Proceeding with original cleaned response.' + ); + } + } else if (!codeBlockMatch && !prefixFound) { + // Only warn if no other extraction method worked report( 'warn', - 'Response does not appear to contain a JSON array structure. Parsing raw response.' + 'Response does not appear to contain a JSON code block, known prefix, or clear array structure ([...]). Attempting to parse raw response.' ); } } - // Attempt to parse the array + // Step 4: Attempt to parse the (hopefully) cleaned JSON array let parsedTasks; try { parsedTasks = JSON.parse(cleanedResponse); @@ -114,7 +151,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { ); } - // Validate Array structure + // Step 5: Validate Array structure if (!Array.isArray(parsedTasks)) { report( 'error', @@ -135,7 +172,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { ); } - // Validate each task object using Zod + // Step 6: Validate each task object using Zod const validationResult = updatedTaskArraySchema.safeParse(parsedTasks); if (!validationResult.success) { report('error', 'Parsed task array failed Zod validation.'); diff --git a/scripts/modules/utils.js b/scripts/modules/utils.js index 9303ccf9..64432f6f 100644 --- a/scripts/modules/utils.js +++ b/scripts/modules/utils.js @@ -510,8 +510,6 @@ function detectCamelCaseFlags(args) { // Export all utility functions and configuration export { - // CONFIG, <-- Already Removed - // getConfig <-- Removing now LOG_LEVELS, log, readJSON, @@ -532,5 +530,4 @@ export { resolveEnvVariable, getTaskManager, findProjectRoot - // getConfig <-- Removed };