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