Compare commits

..

5 Commits

Author SHA1 Message Date
Ralph Khreish
344f40c699 fix: implement support for MCP 2025-04-19 10:45:00 +02:00
Ralph Khreish
8840d2fb3b chore: fix formatting issues 2025-04-19 00:09:22 +02:00
Kresna Sucandra
3eca720f36 feat: Enhance remove-task command to handle multiple comma-separated task IDs 2025-04-19 00:08:06 +02:00
Ralph Khreish
d99fa00980 feat: improve task-master init (#248)
* chore: fix weird bug where package.json is not upgrading its version based on current package version

* feat: improve `tm init`
2025-04-17 19:32:30 +02:00
Ralph Khreish
b2ccd60526 feat: add new bin task-master-ai same name as package to allow npx -y task-master-ai to work (#253) 2025-04-17 19:30:30 +02:00
10 changed files with 235 additions and 117 deletions

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': patch
---
Fix remove-task command to handle multiple comma-separated task IDs

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': minor
---
Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``

View File

@@ -146,7 +146,7 @@ To enable enhanced task management capabilities directly within Cursor using the
4. Configure with the following details: 4. Configure with the following details:
- Name: "Task Master" - Name: "Task Master"
- Type: "Command" - Type: "Command"
- Command: "npx -y task-master-mcp" - Command: "npx -y task-master-ai"
5. Save the settings 5. Save the settings
Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience.

View File

@@ -20,20 +20,14 @@ A task management system for AI-driven development with Claude, designed to work
MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor. MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor.
1. **Install the package** 1. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```bash
npm i -g task-master-ai
```
2. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```json ```json
{ {
"mcpServers": { "mcpServers": {
"taskmaster-ai": { "taskmaster-ai": {
"command": "npx", "command": "npx",
"args": ["-y", "task-master-mcp"], "args": ["-y", "task-master-ai"],
"env": { "env": {
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",

View File

@@ -10,20 +10,14 @@ There are two ways to set up Task Master: using MCP (recommended) or via npm ins
MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor. MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor.
1. **Install the package** 1. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```bash
npm i -g task-master-ai
```
2. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```json ```json
{ {
"mcpServers": { "mcpServers": {
"taskmaster-ai": { "taskmaster-ai": {
"command": "npx", "command": "npx",
"args": ["-y", "task-master-mcp"], "args": ["-y", "task-master-ai"],
"env": { "env": {
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",

View File

@@ -3,18 +3,23 @@
* Direct function implementation for removing a task * 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 { import {
enableSilentMode, enableSilentMode,
disableSilentMode disableSilentMode,
readJSON
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
/** /**
* Direct function wrapper for removeTask with error handling. * Direct function wrapper for removeTask with error handling.
* Supports removing multiple tasks at once with comma-separated IDs.
* *
* @param {Object} args - Command arguments * @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @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 * @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false } * @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
*/ */
@@ -36,8 +41,7 @@ export async function removeTaskDirect(args, log) {
} }
// Validate task ID parameter // Validate task ID parameter
const taskId = id; if (!id) {
if (!taskId) {
log.error('Task ID is required'); log.error('Task ID is required');
return { return {
success: false, 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 // Split task IDs if comma-separated
log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`); const taskIdArray = id.split(',').map((taskId) => taskId.trim());
try { log.info(
// Enable silent mode to prevent console logs from interfering with JSON response `Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
enableSilentMode(); );
// Call the core removeTask function using the provided path // Validate all task IDs exist before proceeding
const result = await removeTask(tasksJsonPath, taskId); const data = readJSON(tasksJsonPath);
if (!data || !data.tasks) {
// 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}`);
return { return {
success: false, success: false,
error: { error: {
code: error.code || 'REMOVE_TASK_ERROR', code: 'INVALID_TASKS_FILE',
message: error.message || 'Failed to remove task' message: `No valid tasks found in ${tasksJsonPath}`
}, },
fromCache: false 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) { } catch (error) {
// Ensure silent mode is disabled even if an outer error occurs // Ensure silent mode is disabled even if an outer error occurs
disableSilentMode(); disableSilentMode();

View File

@@ -23,7 +23,9 @@ export function registerRemoveTaskTool(server) {
parameters: z.object({ parameters: z.object({
id: z id: z
.string() .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'), file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z projectRoot: z
.string() .string()
@@ -35,7 +37,7 @@ export function registerRemoveTaskTool(server) {
}), }),
execute: async (args, { log, session }) => { execute: async (args, { log, session }) => {
try { 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 // Get project root from args or session
const rootFolder = const rootFolder =

1
package-lock.json generated
View File

@@ -31,6 +31,7 @@
}, },
"bin": { "bin": {
"task-master": "bin/task-master.js", "task-master": "bin/task-master.js",
"task-master-ai": "mcp-server/server.js",
"task-master-mcp": "mcp-server/server.js" "task-master-mcp": "mcp-server/server.js"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -6,7 +6,8 @@
"type": "module", "type": "module",
"bin": { "bin": {
"task-master": "bin/task-master.js", "task-master": "bin/task-master.js",
"task-master-mcp": "mcp-server/server.js" "task-master-mcp": "mcp-server/server.js",
"task-master-ai": "mcp-server/server.js"
}, },
"scripts": { "scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest", "test": "node --experimental-vm-modules node_modules/.bin/jest",

View File

@@ -1374,18 +1374,18 @@ function registerCommands(programInstance) {
// remove-task command // remove-task command
programInstance programInstance
.command('remove-task') .command('remove-task')
.description('Remove a task or subtask permanently') .description('Remove one or more tasks or subtasks permanently')
.option( .option(
'-i, --id <id>', '-i, --id <id>',
'ID of the task or subtask to remove (e.g., "5" or "5.2")' 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5" or "5.2" or "5,6,7")'
) )
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-y, --yes', 'Skip confirmation prompt', false) .option('-y, --yes', 'Skip confirmation prompt', false)
.action(async (options) => { .action(async (options) => {
const tasksPath = options.file; const tasksPath = options.file;
const taskId = options.id; const taskIds = options.id;
if (!taskId) { if (!taskIds) {
console.error(chalk.red('Error: Task ID is required')); console.error(chalk.red('Error: Task ID is required'));
console.error( console.error(
chalk.yellow('Usage: task-master remove-task --id=<taskId>') chalk.yellow('Usage: task-master remove-task --id=<taskId>')
@@ -1394,7 +1394,7 @@ function registerCommands(programInstance) {
} }
try { try {
// Check if the task exists // Check if the tasks file exists and is valid
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
console.error( console.error(
@@ -1403,75 +1403,89 @@ function registerCommands(programInstance) {
process.exit(1); process.exit(1);
} }
if (!taskExists(data.tasks, taskId)) { // Split task IDs if comma-separated
console.error(chalk.red(`Error: Task with ID ${taskId} not found`)); const taskIdArray = taskIds.split(',').map((id) => id.trim());
// Validate all task IDs exist before proceeding
const invalidTasks = taskIdArray.filter(
(id) => !taskExists(data.tasks, id)
);
if (invalidTasks.length > 0) {
console.error(
chalk.red(
`Error: The following tasks were not found: ${invalidTasks.join(', ')}`
)
);
process.exit(1); process.exit(1);
} }
// Load task for display
const task = findTaskById(data.tasks, taskId);
// Skip confirmation if --yes flag is provided // Skip confirmation if --yes flag is provided
if (!options.yes) { if (!options.yes) {
// Display task information // Display tasks to be removed
console.log(); console.log();
console.log( console.log(
chalk.red.bold( chalk.red.bold(
'⚠️ WARNING: This will permanently delete the following task:' '⚠️ WARNING: This will permanently delete the following tasks:'
) )
); );
console.log(); console.log();
if (typeof taskId === 'string' && taskId.includes('.')) { for (const taskId of taskIdArray) {
// It's a subtask const task = findTaskById(data.tasks, taskId);
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
console.log(
chalk.gray(
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
)
);
} else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
// Show if it has subtasks if (typeof taskId === 'string' && taskId.includes('.')) {
if (task.subtasks && task.subtasks.length > 0) { // It's a subtask
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
console.log( console.log(
chalk.yellow( chalk.gray(
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!` `Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
) )
); );
} } else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
// Show if other tasks depend on it // Show if it has subtasks
const dependentTasks = data.tasks.filter( if (task.subtasks && task.subtasks.length > 0) {
(t) => console.log(
t.dependencies && t.dependencies.includes(parseInt(taskId, 10)) chalk.yellow(
); `⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
)
);
}
if (dependentTasks.length > 0) { // Show if other tasks depend on it
console.log( const dependentTasks = data.tasks.filter(
chalk.yellow( (t) =>
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!` t.dependencies &&
) t.dependencies.includes(parseInt(taskId, 10))
); );
console.log(chalk.yellow('These dependencies will be removed:'));
dependentTasks.forEach((t) => { if (dependentTasks.length > 0) {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`)); console.log(
}); chalk.yellow(
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
)
);
console.log(
chalk.yellow('These dependencies will be removed:')
);
dependentTasks.forEach((t) => {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
});
}
} }
console.log();
} }
console.log();
// Prompt for confirmation // Prompt for confirmation
const { confirm } = await inquirer.prompt([ const { confirm } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'confirm', name: 'confirm',
message: chalk.red.bold( message: chalk.red.bold(
'Are you sure you want to permanently delete this task?' `Are you sure you want to permanently delete ${taskIdArray.length > 1 ? 'these tasks' : 'this task'}?`
), ),
default: false default: false
} }
@@ -1483,31 +1497,72 @@ function registerCommands(programInstance) {
} }
} }
const indicator = startLoadingIndicator('Removing task...'); const indicator = startLoadingIndicator('Removing tasks...');
// Remove the task // Remove each task
const result = await removeTask(tasksPath, taskId); const results = [];
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksPath, taskId);
results.push({ taskId, success: true, ...result });
} catch (error) {
results.push({ taskId, success: false, error: error.message });
}
}
stopLoadingIndicator(indicator); stopLoadingIndicator(indicator);
// Display success message with appropriate color based on task or subtask // Display results
if (typeof taskId === 'string' && taskId.includes('.')) { const successfulRemovals = results.filter((r) => r.success);
// It was a subtask const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length > 0) {
console.log( console.log(
boxen( boxen(
chalk.green(`Subtask ${taskId} has been successfully removed`), chalk.green(
{ padding: 1, borderColor: 'green', borderStyle: 'round' } `Successfully removed ${successfulRemovals.length} task${successfulRemovals.length > 1 ? 's' : ''}`
) +
'\n\n' +
successfulRemovals
.map((r) =>
chalk.white(
`${r.taskId.includes('.') ? 'Subtask' : 'Task'} ${r.taskId}`
)
)
.join('\n'),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
) )
); );
} else { }
// It was a main task
if (failedRemovals.length > 0) {
console.log( console.log(
boxen(chalk.green(`Task ${taskId} has been successfully removed`), { boxen(
padding: 1, chalk.red(
borderColor: 'green', `Failed to remove ${failedRemovals.length} task${failedRemovals.length > 1 ? 's' : ''}`
borderStyle: 'round' ) +
}) '\n\n' +
failedRemovals
.map((r) => chalk.white(`${r.taskId}: ${r.error}`))
.join('\n'),
{
padding: 1,
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1 }
}
)
); );
// Exit with error if any removals failed
if (successfulRemovals.length === 0) {
process.exit(1);
}
} }
} catch (error) { } catch (error) {
console.error( console.error(