Merge branch 'next' of https://github.com/eyaltoledano/claude-task-master into joedanz/flexible-brand-rules

# Conflicts:
#	.cursor/rules/dev_workflow.mdc
#	mcp-server/src/tools/index.js
#	scripts/init.js
This commit is contained in:
Joe Danziger
2025-06-15 13:25:59 -04:00
263 changed files with 25128 additions and 21840 deletions

View File

@@ -0,0 +1,198 @@
/**
* add-tag.js
* Direct function implementation for creating a new tag
*/
import {
createTag,
createTagFromBranch
} from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for creating a new tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the new tag to create
* @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag
* @param {string} [args.copyFromTag] - Specific tag to copy tasks from
* @param {boolean} [args.fromBranch=false] - Create tag name from current git branch
* @param {string} [args.description] - Optional description for the tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function addTagDirect(args, log, context = {}) {
// Destructure expected args
const {
tasksJsonPath,
name,
copyFromCurrent = false,
copyFromTag,
fromBranch = false,
description,
projectRoot
} = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('addTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Handle --from-branch option
if (fromBranch) {
log.info('Creating tag from current git branch');
// Import git utilities
const gitUtils = await import(
'../../../../scripts/modules/utils/git-utils.js'
);
// Check if we're in a git repository
if (!(await gitUtils.isGitRepository(projectRoot))) {
log.error('Not in a git repository');
disableSilentMode();
return {
success: false,
error: {
code: 'NOT_GIT_REPO',
message: 'Not in a git repository. Cannot use fromBranch option.'
}
};
}
// Get current git branch
const currentBranch = await gitUtils.getCurrentBranch(projectRoot);
if (!currentBranch) {
log.error('Could not determine current git branch');
disableSilentMode();
return {
success: false,
error: {
code: 'NO_CURRENT_BRANCH',
message: 'Could not determine current git branch.'
}
};
}
// Prepare options for branch-based tag creation
const branchOptions = {
copyFromCurrent,
copyFromTag,
description:
description || `Tag created from git branch "${currentBranch}"`
};
// Call the createTagFromBranch function
const result = await createTagFromBranch(
tasksJsonPath,
currentBranch,
branchOptions,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
branchName: result.branchName,
tagName: result.tagName,
created: result.created,
mappingUpdated: result.mappingUpdated,
message: `Successfully created tag "${result.tagName}" from git branch "${result.branchName}"`
}
};
} else {
// Check required parameters for regular tag creation
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Creating new tag: ${name}`);
// Prepare options
const options = {
copyFromCurrent,
copyFromTag,
description
};
// Call the createTag function
const result = await createTag(
tasksJsonPath,
name,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.tagName,
created: result.created,
tasksCopied: result.tasksCopied,
sourceTag: result.sourceTag,
description: result.description,
message: `Successfully created tag "${result.tagName}"`
}
};
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'ADD_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -95,6 +95,7 @@ export async function addTaskDirect(args, log, context = {}) {
let manualTaskData = null;
let newTaskId;
let telemetryData;
let tagInfo;
if (isManualCreation) {
// Create manual task data object
@@ -129,6 +130,7 @@ export async function addTaskDirect(args, log, context = {}) {
);
newTaskId = result.newTaskId;
telemetryData = result.telemetryData;
tagInfo = result.tagInfo;
} else {
// AI-driven task creation
log.info(
@@ -154,6 +156,7 @@ export async function addTaskDirect(args, log, context = {}) {
);
newTaskId = result.newTaskId;
telemetryData = result.telemetryData;
tagInfo = result.tagInfo;
}
// Restore normal logging
@@ -164,7 +167,8 @@ export async function addTaskDirect(args, log, context = {}) {
data: {
taskId: newTaskId,
message: `Successfully added new task #${newTaskId}`,
telemetryData: telemetryData
telemetryData: telemetryData,
tagInfo: tagInfo
}
};
} catch (error) {

View File

@@ -196,7 +196,8 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
lowComplexityTasks
},
fullReport: coreResult.report,
telemetryData: coreResult.telemetryData
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (parseError) {

View File

@@ -5,9 +5,11 @@
import { clearSubtasks } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
disableSilentMode,
readJSON
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import path from 'path';
/**
* Clear subtasks from specified tasks
@@ -15,12 +17,13 @@ import fs from 'fs';
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} [args.id] - Task IDs (comma-separated) to clear subtasks from
* @param {boolean} [args.all] - Clear subtasks from all tasks
* @param {string} [args.tag] - Tag context to operate on (defaults to current active tag)
* @param {Object} log - Logger object
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function clearSubtasksDirect(args, log) {
// Destructure expected args
const { tasksJsonPath, id, all } = args;
const { tasksJsonPath, id, all, tag, projectRoot } = args;
try {
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
@@ -64,52 +67,70 @@ export async function clearSubtasksDirect(args, log) {
let taskIds;
// Use readJSON which handles silent migration and tag resolution
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
return {
success: false,
error: {
code: 'INPUT_VALIDATION_ERROR',
message: `No tasks found in tasks file: ${tasksPath}`
}
};
}
const currentTag = data.tag || 'master';
const tasks = data.tasks;
// If all is specified, get all task IDs
if (all) {
log.info('Clearing subtasks from all tasks');
const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
if (!data || !data.tasks || data.tasks.length === 0) {
log.info(`Clearing subtasks from all tasks in tag '${currentTag}'`);
if (tasks.length === 0) {
return {
success: false,
error: {
code: 'INPUT_VALIDATION_ERROR',
message: 'No valid tasks found in the tasks file'
message: `No tasks found in tag context '${currentTag}'`
}
};
}
taskIds = data.tasks.map((t) => t.id).join(',');
taskIds = tasks.map((t) => t.id).join(',');
} else {
// Use the provided task IDs
taskIds = id;
}
log.info(`Clearing subtasks from tasks: ${taskIds}`);
log.info(`Clearing subtasks from tasks: ${taskIds} in tag '${currentTag}'`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
clearSubtasks(tasksPath, taskIds);
clearSubtasks(tasksPath, taskIds, { projectRoot, tag: currentTag });
// Restore normal logging
disableSilentMode();
// Read the updated data to provide a summary
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
const updatedData = readJSON(tasksPath, projectRoot, currentTag);
const taskIdArray = taskIds.split(',').map((id) => parseInt(id.trim(), 10));
// Build a summary of what was done
const clearedTasksCount = taskIdArray.length;
const updatedTasks = updatedData.tasks || [];
const taskSummary = taskIdArray.map((id) => {
const task = updatedData.tasks.find((t) => t.id === id);
const task = updatedTasks.find((t) => t.id === id);
return task ? { id, title: task.title } : { id, title: 'Task not found' };
});
return {
success: true,
data: {
message: `Successfully cleared subtasks from ${clearedTasksCount} task(s)`,
tasksCleared: taskSummary
message: `Successfully cleared subtasks from ${clearedTasksCount} task(s) in tag '${currentTag}'`,
tasksCleared: taskSummary,
tag: currentTag
}
};
} catch (error) {

View File

@@ -0,0 +1,125 @@
/**
* copy-tag.js
* Direct function implementation for copying a tag
*/
import { copyTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for copying a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.sourceName - Name of the source tag to copy from
* @param {string} args.targetName - Name of the new tag to create
* @param {string} [args.description] - Optional description for the new tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function copyTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, sourceName, targetName, description, projectRoot } =
args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('copyTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!sourceName || typeof sourceName !== 'string') {
log.error('Missing required parameter: sourceName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Source tag name is required and must be a string'
}
};
}
if (!targetName || typeof targetName !== 'string') {
log.error('Missing required parameter: targetName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Target tag name is required and must be a string'
}
};
}
log.info(`Copying tag from "${sourceName}" to "${targetName}"`);
// Prepare options
const options = {
description
};
// Call the copyTag function
const result = await copyTag(
tasksJsonPath,
sourceName,
targetName,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
sourceName: result.sourceName,
targetName: result.targetName,
copied: result.copied,
tasksCopied: result.tasksCopied,
description: result.description,
message: `Successfully copied tag from "${result.sourceName}" to "${result.targetName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in copyTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'COPY_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,159 @@
/**
* create-tag-from-branch.js
* Direct function implementation for creating tags from git branches
*/
import { createTagFromBranch } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
getCurrentBranch,
isGitRepository
} from '../../../../scripts/modules/utils/git-utils.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for creating tags from git branches with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Path to the tasks.json file (resolved by tool)
* @param {string} [args.branchName] - Git branch name (optional, uses current branch if not provided)
* @param {boolean} [args.copyFromCurrent] - Copy tasks from current tag
* @param {string} [args.copyFromTag] - Copy tasks from specific tag
* @param {string} [args.description] - Custom description for the tag
* @param {boolean} [args.autoSwitch] - Automatically switch to the new tag
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function createTagFromBranchDirect(args, log, context = {}) {
// Destructure expected args
const {
tasksJsonPath,
branchName,
copyFromCurrent,
copyFromTag,
description,
autoSwitch,
projectRoot
} = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('createTagFromBranchDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check if projectRoot was provided
if (!projectRoot) {
log.error('createTagFromBranchDirect called without projectRoot');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'projectRoot is required'
}
};
}
// Check if we're in a git repository
if (!(await isGitRepository(projectRoot))) {
log.error('Not in a git repository');
disableSilentMode();
return {
success: false,
error: {
code: 'NOT_GIT_REPOSITORY',
message: 'Not in a git repository. Cannot create tag from branch.'
}
};
}
// Determine branch name
let targetBranch = branchName;
if (!targetBranch) {
targetBranch = await getCurrentBranch(projectRoot);
if (!targetBranch) {
log.error('Could not determine current git branch');
disableSilentMode();
return {
success: false,
error: {
code: 'NO_CURRENT_BRANCH',
message: 'Could not determine current git branch'
}
};
}
}
log.info(`Creating tag from git branch: ${targetBranch}`);
// Prepare options
const options = {
copyFromCurrent: copyFromCurrent || false,
copyFromTag,
description:
description || `Tag created from git branch "${targetBranch}"`,
autoSwitch: autoSwitch || false
};
// Call the createTagFromBranch function
const result = await createTagFromBranch(
tasksJsonPath,
targetBranch,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
branchName: result.branchName,
tagName: result.tagName,
created: result.created,
mappingUpdated: result.mappingUpdated,
autoSwitched: result.autoSwitched,
message: `Successfully created tag "${result.tagName}" from branch "${result.branchName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in createTagFromBranchDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'CREATE_TAG_FROM_BRANCH_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,110 @@
/**
* delete-tag.js
* Direct function implementation for deleting a tag
*/
import { deleteTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for deleting a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the tag to delete
* @param {boolean} [args.yes=false] - Skip confirmation prompts
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function deleteTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, name, yes = false, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('deleteTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Deleting tag: ${name}`);
// Prepare options
const options = {
yes // For MCP, we always skip confirmation prompts
};
// Call the deleteTag function
const result = await deleteTag(
tasksJsonPath,
name,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.tagName,
deleted: result.deleted,
tasksDeleted: result.tasksDeleted,
wasCurrentTag: result.wasCurrentTag,
switchedToMaster: result.switchedToMaster,
message: `Successfully deleted tag "${result.tagName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in deleteTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'DELETE_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -89,7 +89,7 @@ export async function expandTaskDirect(args, log, context = {}) {
// Read tasks data
log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
log.info(
`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`
);
@@ -164,10 +164,6 @@ export async function expandTaskDirect(args, log, context = {}) {
// Tracking subtasks count before expansion
const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;
// Create a backup of the tasks.json file
const backupPath = path.join(path.dirname(tasksPath), 'tasks.json.bak');
fs.copyFileSync(tasksPath, backupPath);
// Directly modify the data instead of calling the CLI function
if (!task.subtasks) {
task.subtasks = [];
@@ -207,7 +203,7 @@ export async function expandTaskDirect(args, log, context = {}) {
if (!wasSilent && isSilentMode()) disableSilentMode();
// Read the updated data
const updatedData = readJSON(tasksPath);
const updatedData = readJSON(tasksPath, projectRoot);
const updatedTask = updatedData.tasks.find((t) => t.id === taskId);
// Calculate how many subtasks were added
@@ -225,7 +221,8 @@ export async function expandTaskDirect(args, log, context = {}) {
task: coreResult.task,
subtasksAdded,
hasExistingSubtasks,
telemetryData: coreResult.telemetryData
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (error) {

View File

@@ -0,0 +1,132 @@
/**
* list-tags.js
* Direct function implementation for listing all tags
*/
import { tags } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for listing all tags with error handling.
*
* @param {Object} args - Command arguments
* @param {boolean} [args.showMetadata=false] - Whether to include metadata in the output
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function listTagsDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, showMetadata = false, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('listTagsDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
log.info('Listing all tags');
// Prepare options
const options = {
showMetadata
};
// Call the tags function
const result = await tags(
tasksJsonPath,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Transform the result to remove full task data and provide summary info
const tagsSummary = result.tags.map((tag) => {
const tasks = tag.tasks || [];
// Calculate status breakdown
const statusBreakdown = tasks.reduce((acc, task) => {
const status = task.status || 'pending';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
// Calculate subtask counts
const subtaskCounts = tasks.reduce(
(acc, task) => {
if (task.subtasks && task.subtasks.length > 0) {
acc.totalSubtasks += task.subtasks.length;
task.subtasks.forEach((subtask) => {
const subStatus = subtask.status || 'pending';
acc.subtasksByStatus[subStatus] =
(acc.subtasksByStatus[subStatus] || 0) + 1;
});
}
return acc;
},
{ totalSubtasks: 0, subtasksByStatus: {} }
);
return {
name: tag.name,
isCurrent: tag.isCurrent,
taskCount: tasks.length,
completedTasks: tag.completedTasks,
statusBreakdown,
subtaskCounts,
created: tag.created,
description: tag.description
};
});
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tags: tagsSummary,
currentTag: result.currentTag,
totalTags: result.totalTags,
message: `Found ${result.totalTags} tag(s)`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in listTagsDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'LIST_TAGS_ERROR',
message: error.message
}
};
}
}

View File

@@ -16,9 +16,10 @@ import {
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string } }.
*/
export async function listTasksDirect(args, log) {
export async function listTasksDirect(args, log, context = {}) {
// Destructure the explicit tasksJsonPath from args
const { tasksJsonPath, reportPath, status, withSubtasks } = args;
const { tasksJsonPath, reportPath, status, withSubtasks, projectRoot } = args;
const { session } = context;
if (!tasksJsonPath) {
log.error('listTasksDirect called without tasksJsonPath');
@@ -50,7 +51,9 @@ export async function listTasksDirect(args, log) {
statusFilter,
reportPath,
withSubtasksFilter,
'json'
'json',
null, // tag
{ projectRoot, session } // context
);
if (!resultData || !resultData.tasks) {

View File

@@ -13,10 +13,11 @@ import {
* Move a task or subtask to a new position
* @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file
* @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2')
* @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3')
* @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
* @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3' or '7,8,9')
* @param {string} args.file - Alternative path to the tasks.json file
* @param {string} args.projectRoot - Project root directory
* @param {boolean} args.generateFiles - Whether to regenerate task files after moving (default: true)
* @param {Object} log - Logger object
* @returns {Promise<{success: boolean, data?: Object, error?: Object}>}
*/
@@ -64,12 +65,17 @@ export async function moveTaskDirect(args, log, context = {}) {
// Enable silent mode to prevent console output during MCP operation
enableSilentMode();
// Call the core moveTask function, always generate files
// Call the core moveTask function with file generation control
const generateFiles = args.generateFiles !== false; // Default to true
const result = await moveTask(
tasksPath,
args.sourceId,
args.destinationId,
true
generateFiles,
{
projectRoot: args.projectRoot,
tag: args.tag
}
);
// Restore console output
@@ -78,7 +84,7 @@ export async function moveTaskDirect(args, log, context = {}) {
return {
success: true,
data: {
movedTask: result.movedTask,
...result,
message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}`
}
};

View File

@@ -21,9 +21,10 @@ import {
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Next task result { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function nextTaskDirect(args, log) {
export async function nextTaskDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, reportPath } = args;
const { tasksJsonPath, reportPath, projectRoot } = args;
const { session } = context;
if (!tasksJsonPath) {
log.error('nextTaskDirect called without tasksJsonPath');
@@ -45,7 +46,7 @@ export async function nextTaskDirect(args, log) {
log.info(`Finding next task from ${tasksJsonPath}`);
// Read tasks data using the provided path
const data = readJSON(tasksJsonPath);
const data = readJSON(tasksJsonPath, projectRoot);
if (!data || !data.tasks) {
disableSilentMode(); // Disable before return
return {

View File

@@ -170,7 +170,8 @@ export async function parsePRDDirect(args, log, context = {}) {
data: {
message: successMsg,
outputPath: result.tasksPath,
telemetryData: result.telemetryData
telemetryData: result.telemetryData,
tagInfo: result.tagInfo
}
};
} else {

View File

@@ -23,9 +23,10 @@ import {
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function removeTaskDirect(args, log) {
export async function removeTaskDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, id } = args;
const { tasksJsonPath, id, projectRoot } = args;
const { session } = context;
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
@@ -59,7 +60,7 @@ export async function removeTaskDirect(args, log) {
);
// Validate all task IDs exist before proceeding
const data = readJSON(tasksJsonPath);
const data = readJSON(tasksJsonPath, projectRoot);
if (!data || !data.tasks) {
return {
success: false,

View File

@@ -0,0 +1,118 @@
/**
* rename-tag.js
* Direct function implementation for renaming a tag
*/
import { renameTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for renaming a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.oldName - Current name of the tag to rename
* @param {string} args.newName - New name for the tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function renameTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, oldName, newName, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('renameTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!oldName || typeof oldName !== 'string') {
log.error('Missing required parameter: oldName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Old tag name is required and must be a string'
}
};
}
if (!newName || typeof newName !== 'string') {
log.error('Missing required parameter: newName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'New tag name is required and must be a string'
}
};
}
log.info(`Renaming tag from "${oldName}" to "${newName}"`);
// Call the renameTag function
const result = await renameTag(
tasksJsonPath,
oldName,
newName,
{}, // options (empty for now)
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
oldName: result.oldName,
newName: result.newName,
renamed: result.renamed,
taskCount: result.taskCount,
wasCurrentTag: result.wasCurrentTag,
message: `Successfully renamed tag from "${result.oldName}" to "${result.newName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in renameTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'RENAME_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,249 @@
/**
* research.js
* Direct function implementation for AI-powered research queries
*/
import path from 'path';
import { performResearch } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for performing AI-powered research with project context.
*
* @param {Object} args - Command arguments
* @param {string} args.query - Research query/prompt (required)
* @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
* @param {string} [args.filePaths] - Comma-separated list of file paths for context
* @param {string} [args.customContext] - Additional custom context text
* @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
* @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
* @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
* @param {boolean} [args.saveToFile=false] - Save research results to .taskmaster/docs/research/ directory
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function researchDirect(args, log, context = {}) {
// Destructure expected args
const {
query,
taskIds,
filePaths,
customContext,
includeProjectTree = false,
detailLevel = 'medium',
saveTo,
saveToFile = false,
projectRoot
} = args;
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check required parameters
if (!query || typeof query !== 'string' || query.trim().length === 0) {
log.error('Missing or invalid required parameter: query');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message:
'The query parameter is required and must be a non-empty string'
}
};
}
// Parse comma-separated task IDs if provided
const parsedTaskIds = taskIds
? taskIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
: [];
// Parse comma-separated file paths if provided
const parsedFilePaths = filePaths
? filePaths
.split(',')
.map((path) => path.trim())
.filter((path) => path.length > 0)
: [];
// Validate detail level
const validDetailLevels = ['low', 'medium', 'high'];
if (!validDetailLevels.includes(detailLevel)) {
log.error(`Invalid detail level: ${detailLevel}`);
disableSilentMode();
return {
success: false,
error: {
code: 'INVALID_PARAMETER',
message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
}
};
}
log.info(
`Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
`taskIds: [${parsedTaskIds.join(', ')}], ` +
`filePaths: [${parsedFilePaths.join(', ')}], ` +
`detailLevel: ${detailLevel}, ` +
`includeProjectTree: ${includeProjectTree}, ` +
`projectRoot: ${projectRoot}`
);
// Prepare options for the research function
const researchOptions = {
taskIds: parsedTaskIds,
filePaths: parsedFilePaths,
customContext: customContext || '',
includeProjectTree,
detailLevel,
projectRoot,
saveToFile
};
// Prepare context for the research function
const researchContext = {
session,
mcpLog,
commandName: 'research',
outputType: 'mcp'
};
// Call the performResearch function
const result = await performResearch(
query.trim(),
researchOptions,
researchContext,
'json', // outputFormat - use 'json' to suppress CLI UI
false // allowFollowUp - disable for MCP calls
);
// Auto-save to task/subtask if requested
if (saveTo) {
try {
const isSubtask = saveTo.includes('.');
// Format research content for saving
const researchContent = `## Research Query: ${query.trim()}
**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
### Results
${result.result}`;
if (isSubtask) {
// Save to subtask
const { updateSubtaskById } = await import(
'../../../../scripts/modules/task-manager/update-subtask-by-id.js'
);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateSubtaskById(
tasksPath,
saveTo,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot
},
'json'
);
log.info(`Research saved to subtask ${saveTo}`);
} else {
// Save to task
const updateTaskById = (
await import(
'../../../../scripts/modules/task-manager/update-task-by-id.js'
)
).default;
const taskIdNum = parseInt(saveTo, 10);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateTaskById(
tasksPath,
taskIdNum,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot
},
'json',
true // appendMode = true
);
log.info(`Research saved to task ${saveTo}`);
}
} catch (saveError) {
log.warn(`Error saving research to task/subtask: ${saveError.message}`);
}
}
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
query: result.query,
result: result.result,
contextSize: result.contextSize,
contextTokens: result.contextTokens,
tokenBreakdown: result.tokenBreakdown,
systemPromptTokens: result.systemPromptTokens,
userPromptTokens: result.userPromptTokens,
totalInputTokens: result.totalInputTokens,
detailLevel: result.detailLevel,
telemetryData: result.telemetryData,
tagInfo: result.tagInfo,
savedFilePath: result.savedFilePath
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in researchDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'RESEARCH_ERROR',
message: error.message
}
};
}
}

View File

@@ -13,13 +13,15 @@ import { nextTaskDirect } from './next-task.js';
/**
* Direct function wrapper for setTaskStatus with error handling.
*
* @param {Object} args - Command arguments containing id, status and tasksJsonPath.
* @param {Object} args - Command arguments containing id, status, tasksJsonPath, and projectRoot.
* @param {Object} log - Logger object.
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function setTaskStatusDirect(args, log) {
// Destructure expected args, including the resolved tasksJsonPath
const { tasksJsonPath, id, status, complexityReportPath } = args;
export async function setTaskStatusDirect(args, log, context = {}) {
// Destructure expected args, including the resolved tasksJsonPath and projectRoot
const { tasksJsonPath, id, status, complexityReportPath, projectRoot } = args;
const { session } = context;
try {
log.info(`Setting task status with args: ${JSON.stringify(args)}`);
@@ -67,7 +69,11 @@ export async function setTaskStatusDirect(args, log) {
enableSilentMode(); // Enable silent mode before calling core function
try {
// Call the core function
await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log });
await setTaskStatus(tasksPath, taskId, newStatus, {
mcpLog: log,
projectRoot,
session
});
log.info(`Successfully set task ${taskId} status to ${newStatus}`);
@@ -89,9 +95,11 @@ export async function setTaskStatusDirect(args, log) {
const nextResult = await nextTaskDirect(
{
tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath
reportPath: complexityReportPath,
projectRoot: projectRoot
},
log
log,
{ session }
);
if (nextResult.success) {

View File

@@ -24,8 +24,7 @@ import { findTasksPath } from '../utils/path-utils.js';
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function showTaskDirect(args, log) {
// Destructure session from context if needed later, otherwise ignore
// const { session } = context;
// This function doesn't need session context since it only reads data
// Destructure projectRoot and other args. projectRoot is assumed normalized.
const { id, file, reportPath, status, projectRoot } = args;
@@ -56,7 +55,7 @@ export async function showTaskDirect(args, log) {
// --- Rest of the function remains the same, using tasksJsonPath ---
try {
const tasksData = readJSON(tasksJsonPath);
const tasksData = readJSON(tasksJsonPath, projectRoot);
if (!tasksData || !tasksData.tasks) {
return {
success: false,
@@ -66,32 +65,91 @@ export async function showTaskDirect(args, log) {
const complexityReport = readComplexityReport(reportPath);
const { task, originalSubtaskCount } = findTaskById(
tasksData.tasks,
id,
complexityReport,
status
);
// Parse comma-separated IDs
const taskIds = id
.split(',')
.map((taskId) => taskId.trim())
.filter((taskId) => taskId.length > 0);
if (!task) {
if (taskIds.length === 0) {
return {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task or subtask with ID ${id} not found`
code: 'INVALID_TASK_ID',
message: 'No valid task IDs provided'
}
};
}
log.info(`Successfully retrieved task ${id}.`);
// Handle single task ID (existing behavior)
if (taskIds.length === 1) {
const { task, originalSubtaskCount } = findTaskById(
tasksData.tasks,
taskIds[0],
complexityReport,
status
);
const returnData = { ...task };
if (originalSubtaskCount !== null) {
returnData._originalSubtaskCount = originalSubtaskCount;
returnData._subtaskFilter = status;
if (!task) {
return {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task or subtask with ID ${taskIds[0]} not found`
}
};
}
log.info(`Successfully retrieved task ${taskIds[0]}.`);
const returnData = { ...task };
if (originalSubtaskCount !== null) {
returnData._originalSubtaskCount = originalSubtaskCount;
returnData._subtaskFilter = status;
}
return { success: true, data: returnData };
}
return { success: true, data: returnData };
// Handle multiple task IDs
const foundTasks = [];
const notFoundIds = [];
taskIds.forEach((taskId) => {
const { task, originalSubtaskCount } = findTaskById(
tasksData.tasks,
taskId,
complexityReport,
status
);
if (task) {
const taskData = { ...task };
if (originalSubtaskCount !== null) {
taskData._originalSubtaskCount = originalSubtaskCount;
taskData._subtaskFilter = status;
}
foundTasks.push(taskData);
} else {
notFoundIds.push(taskId);
}
});
log.info(
`Successfully retrieved ${foundTasks.length} of ${taskIds.length} requested tasks.`
);
// Return multiple tasks with metadata
return {
success: true,
data: {
tasks: foundTasks,
requestedIds: taskIds,
foundCount: foundTasks.length,
notFoundIds: notFoundIds,
isMultiple: true
}
};
} catch (error) {
log.error(`Error showing task ${id}: ${error.message}`);
return {

View File

@@ -139,7 +139,8 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
subtask: coreResult.updatedSubtask,
tasksPath,
useResearch,
telemetryData: coreResult.telemetryData
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (error) {

View File

@@ -19,6 +19,7 @@ import { createLogWrapper } from '../../tools/utils.js';
* @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 {boolean} [args.append] - Whether to append timestamped information instead of full update.
* @param {string} [args.projectRoot] - Project root path.
* @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data.
@@ -27,7 +28,7 @@ import { createLogWrapper } from '../../tools/utils.js';
export async function updateTaskByIdDirect(args, log, context = {}) {
const { session } = context;
// Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research, projectRoot } = args;
const { tasksJsonPath, id, prompt, research, append, projectRoot } = args;
const logWrapper = createLogWrapper(log);
@@ -76,7 +77,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
} else {
// Parse as integer for main task IDs
taskId = parseInt(id, 10);
if (isNaN(taskId)) {
if (Number.isNaN(taskId)) {
const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
logWrapper.error(errorMessage);
return {
@@ -118,7 +119,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
commandName: 'update-task',
outputType: 'mcp'
},
'json'
'json',
append || false
);
// Check if the core function returned null or an object without success
@@ -132,7 +134,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
message: message,
taskId: taskId,
updated: false,
telemetryData: coreResult?.telemetryData
telemetryData: coreResult?.telemetryData,
tagInfo: coreResult?.tagInfo
}
};
}
@@ -149,7 +152,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
useResearch: useResearch,
updated: true,
updatedTask: coreResult.updatedTask,
telemetryData: coreResult.telemetryData
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (error) {

View File

@@ -90,7 +90,8 @@ export async function updateTasksDirect(args, log, context = {}) {
message: `Successfully updated ${result.updatedTasks.length} tasks.`,
tasksPath: tasksJsonPath,
updatedCount: result.updatedTasks.length,
telemetryData: result.telemetryData
telemetryData: result.telemetryData,
tagInfo: result.tagInfo
}
};
} else {

View File

@@ -0,0 +1,103 @@
/**
* use-tag.js
* Direct function implementation for switching to a tag
*/
import { useTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for switching to a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the tag to switch to
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function useTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, name, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('useTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Switching to tag: ${name}`);
// Call the useTag function
const result = await useTag(
tasksJsonPath,
name,
{}, // options (empty for now)
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.currentTag,
switched: result.switched,
previousTag: result.previousTag,
taskCount: result.taskCount,
message: `Successfully switched to tag "${result.currentTag}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in useTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'USE_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -31,6 +31,13 @@ import { removeTaskDirect } from './direct-functions/remove-task.js';
import { initializeProjectDirect } from './direct-functions/initialize-project.js';
import { modelsDirect } from './direct-functions/models.js';
import { moveTaskDirect } from './direct-functions/move-task.js';
import { researchDirect } from './direct-functions/research.js';
import { addTagDirect } from './direct-functions/add-tag.js';
import { deleteTagDirect } from './direct-functions/delete-tag.js';
import { listTagsDirect } from './direct-functions/list-tags.js';
import { useTagDirect } from './direct-functions/use-tag.js';
import { renameTagDirect } from './direct-functions/rename-tag.js';
import { copyTagDirect } from './direct-functions/copy-tag.js';
// Re-export utility functions
export { findTasksPath } from './utils/path-utils.js';
@@ -62,7 +69,14 @@ export const directFunctions = new Map([
['removeTaskDirect', removeTaskDirect],
['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect],
['moveTaskDirect', moveTaskDirect]
['moveTaskDirect', moveTaskDirect],
['researchDirect', researchDirect],
['addTagDirect', addTagDirect],
['deleteTagDirect', deleteTagDirect],
['listTagsDirect', listTagsDirect],
['useTagDirect', useTagDirect],
['renameTagDirect', renameTagDirect],
['copyTagDirect', copyTagDirect]
]);
// Re-export all direct function implementations
@@ -92,5 +106,12 @@ export {
removeTaskDirect,
initializeProjectDirect,
modelsDirect,
moveTaskDirect
moveTaskDirect,
researchDirect,
addTagDirect,
deleteTagDirect,
listTagsDirect,
useTagDirect,
renameTagDirect,
copyTagDirect
};

View File

@@ -75,7 +75,13 @@ export function registerAddDependencyTool(server) {
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error adding dependency');
return handleApiResult(
result,
log,
'Error adding dependency',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in addDependency tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -88,9 +88,11 @@ export function registerAddSubtaskTool(server) {
details: args.details,
status: args.status,
dependencies: args.dependencies,
skipGenerate: args.skipGenerate
skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -99,7 +101,13 @@ export function registerAddSubtaskTool(server) {
log.error(`Failed to add subtask: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error adding subtask');
return handleApiResult(
result,
log,
'Error adding subtask',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in addSubtask tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -0,0 +1,99 @@
/**
* tools/add-tag.js
* Tool to create a new tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { addTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the addTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerAddTagTool(server) {
server.addTool({
name: 'add_tag',
description: 'Create a new tag for organizing tasks in different contexts',
parameters: z.object({
name: z.string().describe('Name of the new tag to create'),
copyFromCurrent: z
.boolean()
.optional()
.describe(
'Whether to copy tasks from the current tag (default: false)'
),
copyFromTag: z
.string()
.optional()
.describe('Specific tag to copy tasks from'),
fromBranch: z
.boolean()
.optional()
.describe(
'Create tag name from current git branch (ignores name parameter)'
),
description: z
.string()
.optional()
.describe('Optional description for the tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting add-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await addTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
copyFromCurrent: args.copyFromCurrent,
copyFromTag: args.copyFromTag,
fromBranch: args.fromBranch,
description: args.description,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error creating tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in add-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -99,7 +99,13 @@ export function registerAddTaskTool(server) {
{ session }
);
return handleApiResult(result, log);
return handleApiResult(
result,
log,
'Error adding task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in add-task tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -135,7 +135,13 @@ export function registerAnalyzeProjectComplexityTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(result, log, 'Error analyzing task complexity');
return handleApiResult(
result,
log,
'Error analyzing task complexity',
undefined,
args.projectRoot
);
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -35,7 +35,8 @@ export function registerClearSubtasksTool(server) {
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
})
.refine((data) => data.id || data.all, {
message: "Either 'id' or 'all' parameter must be provided",
@@ -63,9 +64,12 @@ export function registerClearSubtasksTool(server) {
{
tasksJsonPath: tasksJsonPath,
id: args.id,
all: args.all
all: args.all,
projectRoot: args.projectRoot,
tag: args.tag || 'master'
},
log
log,
{ session }
);
if (result.success) {
@@ -74,7 +78,13 @@ export function registerClearSubtasksTool(server) {
log.error(`Failed to clear subtasks: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error clearing subtasks');
return handleApiResult(
result,
log,
'Error clearing subtasks',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in clearSubtasks tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -69,7 +69,9 @@ export function registerComplexityReportTool(server) {
return handleApiResult(
result,
log,
'Error retrieving complexity report'
'Error retrieving complexity report',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in complexity-report tool: ${error.message}`);

View File

@@ -0,0 +1,83 @@
/**
* tools/copy-tag.js
* Tool to copy an existing tag to a new tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { copyTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the copyTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerCopyTagTool(server) {
server.addTool({
name: 'copy_tag',
description:
'Copy an existing tag to create a new tag with all tasks and metadata',
parameters: z.object({
sourceName: z.string().describe('Name of the source tag to copy from'),
targetName: z.string().describe('Name of the new tag to create'),
description: z
.string()
.optional()
.describe('Optional description for the new tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting copy-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await copyTagDirect(
{
tasksJsonPath: tasksJsonPath,
sourceName: args.sourceName,
targetName: args.targetName,
description: args.description,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error copying tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in copy-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -0,0 +1,80 @@
/**
* tools/delete-tag.js
* Tool to delete an existing tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { deleteTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the deleteTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerDeleteTagTool(server) {
server.addTool({
name: 'delete_tag',
description: 'Delete an existing tag and all its tasks',
parameters: z.object({
name: z.string().describe('Name of the tag to delete'),
yes: z
.boolean()
.optional()
.describe('Skip confirmation prompts (default: true for MCP)'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting delete-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function (always skip confirmation for MCP)
const result = await deleteTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
yes: args.yes !== undefined ? args.yes : true, // Default to true for MCP
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error deleting tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in delete-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -92,7 +92,13 @@ export function registerExpandAllTool(server) {
{ session }
);
return handleApiResult(result, log, 'Error expanding all tasks');
return handleApiResult(
result,
log,
'Error expanding all tasks',
undefined,
args.projectRoot
);
} catch (error) {
log.error(
`Unexpected error in expand_all tool execute: ${error.message}`

View File

@@ -79,7 +79,13 @@ export function registerExpandTaskTool(server) {
{ session }
);
return handleApiResult(result, log, 'Error expanding task');
return handleApiResult(
result,
log,
'Error expanding task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in expand-task tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -57,7 +57,13 @@ export function registerFixDependenciesTool(server) {
log.error(`Failed to fix dependencies: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error fixing dependencies');
return handleApiResult(
result,
log,
'Error fixing dependencies',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in fixDependencies tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -57,9 +57,11 @@ export function registerGenerateTool(server) {
const result = await generateTaskFilesDirect(
{
tasksJsonPath: tasksJsonPath,
outputDir: outputDir
outputDir: outputDir,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -70,7 +72,13 @@ export function registerGenerateTool(server) {
);
}
return handleApiResult(result, log, 'Error generating task files');
return handleApiResult(
result,
log,
'Error generating task files',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in generate tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -44,7 +44,11 @@ export function registerShowTaskTool(server) {
name: 'get_task',
description: 'Get detailed information about a specific task',
parameters: z.object({
id: z.string().describe('Task ID to get'),
id: z
.string()
.describe(
'Task ID(s) to get (can be comma-separated for multiple tasks)'
),
status: z
.string()
.optional()
@@ -61,12 +65,11 @@ export function registerShowTaskTool(server) {
),
projectRoot: z
.string()
.optional()
.describe(
'Absolute path to the project root directory (Optional, usually from session)'
)
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
const { id, file, status, projectRoot } = args;
try {
@@ -112,7 +115,8 @@ export function registerShowTaskTool(server) {
status: status,
projectRoot: projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -126,7 +130,8 @@ export function registerShowTaskTool(server) {
result,
log,
'Error retrieving task details',
processTaskResponse
processTaskResponse,
projectRoot
);
} catch (error) {
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);

View File

@@ -28,7 +28,9 @@ export function registerListTasksTool(server) {
status: z
.string()
.optional()
.describe("Filter tasks by status (e.g., 'pending', 'done')"),
.describe(
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
),
withSubtasks: z
.boolean()
.optional()
@@ -81,15 +83,23 @@ export function registerListTasksTool(server) {
tasksJsonPath: tasksJsonPath,
status: args.status,
withSubtasks: args.withSubtasks,
reportPath: complexityReportPath
reportPath: complexityReportPath,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
log.info(
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
);
return handleApiResult(result, log, 'Error getting tasks');
return handleApiResult(
result,
log,
'Error getting tasks',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error getting tasks: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -29,6 +29,13 @@ import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.js';
import { registerMoveTaskTool } from './move-task.js';
import { registerAddTagTool } from './add-tag.js';
import { registerDeleteTagTool } from './delete-tag.js';
import { registerListTagsTool } from './list-tags.js';
import { registerUseTagTool } from './use-tag.js';
import { registerRenameTagTool } from './rename-tag.js';
import { registerCopyTagTool } from './copy-tag.js';
import { registerResearchTool } from './research.js';
import { registerRulesTool } from './rules.js';
/**
@@ -45,17 +52,22 @@ export function registerTaskMasterTools(server) {
registerRulesTool(server);
registerParsePRDTool(server);
// Group 2: Task Listing & Viewing
// Group 2: Task Analysis & Expansion
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 3: Task Listing & Viewing
registerListTasksTool(server);
registerShowTaskTool(server);
registerNextTaskTool(server);
registerComplexityReportTool(server);
// Group 3: Task Status & Management
// Group 4: Task Status & Management
registerSetTaskStatusTool(server);
registerGenerateTool(server);
// Group 4: Task Creation & Modification
// Group 5: Task Creation & Modification
registerAddTaskTool(server);
registerAddSubtaskTool(server);
registerUpdateTool(server);
@@ -66,16 +78,22 @@ export function registerTaskMasterTools(server) {
registerClearSubtasksTool(server);
registerMoveTaskTool(server);
// Group 5: Task Analysis & Expansion
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 6: Dependency Management
registerAddDependencyTool(server);
registerRemoveDependencyTool(server);
registerValidateDependenciesTool(server);
registerFixDependenciesTool(server);
// Group 7: Tag Management
registerListTagsTool(server);
registerAddTagTool(server);
registerDeleteTagTool(server);
registerUseTagTool(server);
registerRenameTagTool(server);
registerCopyTagTool(server);
// Group 8: Research Features
registerResearchTool(server);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;

View File

@@ -55,7 +55,13 @@ export function registerInitializeProjectTool(server) {
const result = await initializeProjectDirect(args, log, { session });
return handleApiResult(result, log, 'Initialization failed');
return handleApiResult(
result,
log,
'Initialization failed',
undefined,
args.projectRoot
);
} catch (error) {
const errorMessage = `Project initialization tool failed: ${error.message || 'Unknown error'}`;
log.error(errorMessage, error);

View File

@@ -0,0 +1,78 @@
/**
* tools/list-tags.js
* Tool to list all available tags
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { listTagsDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the listTags tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerListTagsTool(server) {
server.addTool({
name: 'list_tags',
description: 'List all available tags with task counts and metadata',
parameters: z.object({
showMetadata: z
.boolean()
.optional()
.describe('Whether to include metadata in the output (default: false)'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting list-tags with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await listTagsDirect(
{
tasksJsonPath: tasksJsonPath,
showMetadata: args.showMetadata,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error listing tags',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in list-tags tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -68,7 +68,13 @@ export function registerModelsTool(server) {
{ session }
);
return handleApiResult(result, log);
return handleApiResult(
result,
log,
'Error managing models',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in models tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -102,7 +102,10 @@ export function registerMoveTaskTool(server) {
message: `Successfully moved ${results.length} tasks`
}
},
log
log,
'Error moving multiple tasks',
undefined,
args.projectRoot
);
} else {
// Moving a single task
@@ -117,7 +120,10 @@ export function registerMoveTaskTool(server) {
log,
{ session }
),
log
log,
'Error moving task',
undefined,
args.projectRoot
);
}
} catch (error) {

View File

@@ -64,13 +64,21 @@ export function registerNextTaskTool(server) {
const result = await nextTaskDirect(
{
tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath
reportPath: complexityReportPath,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
log.info(`Next task result: ${result.success ? 'found' : 'none'}`);
return handleApiResult(result, log, 'Error finding next task');
return handleApiResult(
result,
log,
'Error finding next task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error finding next task: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -64,7 +64,13 @@ export function registerParsePRDTool(server) {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
const result = await parsePRDDirect(args, log, { session });
return handleApiResult(result, log);
return handleApiResult(
result,
log,
'Error parsing PRD',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in parse_prd: ${error.message}`);
return createErrorResponse(`Failed to parse PRD: ${error.message}`);

View File

@@ -68,7 +68,13 @@ export function registerRemoveDependencyTool(server) {
log.error(`Failed to remove dependency: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error removing dependency');
return handleApiResult(
result,
log,
'Error removing dependency',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -46,7 +46,7 @@ export function registerRemoveSubtaskTool(server) {
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
@@ -69,9 +69,11 @@ export function registerRemoveSubtaskTool(server) {
tasksJsonPath: tasksJsonPath,
id: args.id,
convert: args.convert,
skipGenerate: args.skipGenerate
skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -80,7 +82,13 @@ export function registerRemoveSubtaskTool(server) {
log.error(`Failed to remove subtask: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error removing subtask');
return handleApiResult(
result,
log,
'Error removing subtask',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in removeSubtask tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -35,7 +35,7 @@ export function registerRemoveTaskTool(server) {
.optional()
.describe('Whether to skip confirmation prompt (default: false)')
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Removing task(s) with ID(s): ${args.id}`);
@@ -58,9 +58,11 @@ export function registerRemoveTaskTool(server) {
const result = await removeTaskDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id
id: args.id,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -69,7 +71,13 @@ export function registerRemoveTaskTool(server) {
log.error(`Failed to remove task: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error removing task');
return handleApiResult(
result,
log,
'Error removing task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in remove-task tool: ${error.message}`);
return createErrorResponse(`Failed to remove task: ${error.message}`);

View File

@@ -0,0 +1,77 @@
/**
* tools/rename-tag.js
* Tool to rename an existing tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { renameTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the renameTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerRenameTagTool(server) {
server.addTool({
name: 'rename_tag',
description: 'Rename an existing tag',
parameters: z.object({
oldName: z.string().describe('Current name of the tag to rename'),
newName: z.string().describe('New name for the tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting rename-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await renameTagDirect(
{
tasksJsonPath: tasksJsonPath,
oldName: args.oldName,
newName: args.newName,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error renaming tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in rename-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -0,0 +1,102 @@
/**
* tools/research.js
* Tool to perform AI-powered research queries with project context
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { researchDirect } from '../core/task-master-core.js';
/**
* Register the research tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerResearchTool(server) {
server.addTool({
name: 'research',
description: 'Perform AI-powered research queries with project context',
parameters: z.object({
query: z.string().describe('Research query/prompt (required)'),
taskIds: z
.string()
.optional()
.describe(
'Comma-separated list of task/subtask IDs for context (e.g., "15,16.2,17")'
),
filePaths: z
.string()
.optional()
.describe(
'Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md")'
),
customContext: z
.string()
.optional()
.describe('Additional custom context text to include in the research'),
includeProjectTree: z
.boolean()
.optional()
.describe(
'Include project file tree structure in context (default: false)'
),
detailLevel: z
.enum(['low', 'medium', 'high'])
.optional()
.describe('Detail level for the research response (default: medium)'),
saveTo: z
.string()
.optional()
.describe(
'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")'
),
saveToFile: z
.boolean()
.optional()
.describe(
'Save research results to .taskmaster/docs/research/ directory (default: false)'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(
`Starting research with query: "${args.query.substring(0, 100)}${args.query.length > 100 ? '...' : ''}"`
);
// Call the direct function
const result = await researchDirect(
{
query: args.query,
taskIds: args.taskIds,
filePaths: args.filePaths,
customContext: args.customContext,
includeProjectTree: args.includeProjectTree || false,
detailLevel: args.detailLevel || 'medium',
saveTo: args.saveTo,
saveToFile: args.saveToFile || false,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error performing research',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in research tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -49,7 +49,7 @@ export function registerSetTaskStatusTool(server) {
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
@@ -85,9 +85,11 @@ export function registerSetTaskStatusTool(server) {
tasksJsonPath: tasksJsonPath,
id: args.id,
status: args.status,
complexityReportPath
complexityReportPath,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {
@@ -100,7 +102,13 @@ export function registerSetTaskStatusTool(server) {
);
}
return handleApiResult(result, log, 'Error setting task status');
return handleApiResult(
result,
log,
'Error setting task status',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`);
return createErrorResponse(

View File

@@ -75,7 +75,13 @@ export function registerUpdateSubtaskTool(server) {
);
}
return handleApiResult(result, log, 'Error updating subtask');
return handleApiResult(
result,
log,
'Error updating subtask',
undefined,
args.projectRoot
);
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -34,6 +34,12 @@ export function registerUpdateTaskTool(server) {
.boolean()
.optional()
.describe('Use Perplexity AI for research-backed updates'),
append: z
.boolean()
.optional()
.describe(
'Append timestamped information to task details instead of full update'
),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@@ -67,6 +73,7 @@ export function registerUpdateTaskTool(server) {
id: args.id,
prompt: args.prompt,
research: args.research,
append: args.append,
projectRoot: args.projectRoot
},
log,
@@ -77,7 +84,13 @@ export function registerUpdateTaskTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(result, log, 'Error updating task');
return handleApiResult(
result,
log,
'Error updating task',
undefined,
args.projectRoot
);
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -80,7 +80,13 @@ export function registerUpdateTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(result, log, 'Error updating tasks');
return handleApiResult(
result,
log,
'Error updating tasks',
undefined,
args.projectRoot
);
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -0,0 +1,75 @@
/**
* tools/use-tag.js
* Tool to switch to a different tag context
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { useTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the useTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerUseTagTool(server) {
server.addTool({
name: 'use_tag',
description: 'Switch to a different tag context for task operations',
parameters: z.object({
name: z.string().describe('Name of the tag to switch to'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting use-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await useTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error switching tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in use-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -8,6 +8,7 @@ import path from 'path';
import fs from 'fs';
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import { fileURLToPath } from 'url';
import { getCurrentTag } from '../../../scripts/modules/utils.js';
// Import path utilities to ensure consistent path resolution
import {
@@ -59,6 +60,64 @@ function getVersionInfo() {
}
}
/**
* Get current tag information for MCP responses
* @param {string} projectRoot - The project root directory
* @param {Object} log - Logger object
* @returns {Object} Tag information object
*/
function getTagInfo(projectRoot, log) {
try {
if (!projectRoot) {
log.warn('No project root provided for tag information');
return { currentTag: 'master', availableTags: ['master'] };
}
const currentTag = getCurrentTag(projectRoot);
// Read available tags from tasks.json
let availableTags = ['master']; // Default fallback
try {
const tasksJsonPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
if (fs.existsSync(tasksJsonPath)) {
const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf-8'));
// If it's the new tagged format, extract tag keys
if (
tasksData &&
typeof tasksData === 'object' &&
!Array.isArray(tasksData.tasks)
) {
const tagKeys = Object.keys(tasksData).filter(
(key) =>
tasksData[key] &&
typeof tasksData[key] === 'object' &&
Array.isArray(tasksData[key].tasks)
);
if (tagKeys.length > 0) {
availableTags = tagKeys;
}
}
}
} catch (tagError) {
log.debug(`Could not read available tags: ${tagError.message}`);
}
return {
currentTag: currentTag || 'master',
availableTags: availableTags
};
} catch (error) {
log.warn(`Error getting tag information: ${error.message}`);
return { currentTag: 'master', availableTags: ['master'] };
}
}
/**
* Get normalized project root path
* @param {string|undefined} projectRootRaw - Raw project root from arguments
@@ -242,21 +301,26 @@ function getProjectRootFromSession(session, log) {
* @param {Object} log - Logger object
* @param {string} errorPrefix - Prefix for error messages
* @param {Function} processFunction - Optional function to process successful result data
* @param {string} [projectRoot] - Optional project root for tag information
* @returns {Object} - Standardized MCP response object
*/
async function handleApiResult(
result,
log,
errorPrefix = 'API error',
processFunction = processMCPResponseData
processFunction = processMCPResponseData,
projectRoot = null
) {
// Get version info for every response
const versionInfo = getVersionInfo();
// Get tag info if project root is provided
const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;
if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
log.error(`${errorPrefix}: ${errorMsg}`);
return createErrorResponse(errorMsg, versionInfo);
return createErrorResponse(errorMsg, versionInfo, tagInfo);
}
// Process the result data if needed
@@ -266,12 +330,17 @@ async function handleApiResult(
log.info('Successfully completed operation');
// Create the response payload including version info
// Create the response payload including version info and tag info
const responsePayload = {
data: processedData,
version: versionInfo
};
// Add tag information if available
if (tagInfo) {
responsePayload.tag = tagInfo;
}
return createContentResponse(responsePayload);
}
@@ -496,21 +565,30 @@ function createContentResponse(content) {
* Creates error response for tools
* @param {string} errorMessage - Error message to include in response
* @param {Object} [versionInfo] - Optional version information object
* @param {Object} [tagInfo] - Optional tag information object
* @returns {Object} - Error content response object in FastMCP format
*/
function createErrorResponse(errorMessage, versionInfo) {
function createErrorResponse(errorMessage, versionInfo, tagInfo) {
// Provide fallback version info if not provided
if (!versionInfo) {
versionInfo = getVersionInfo();
}
let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;
// Add tag information if available
if (tagInfo) {
responseText += `
Current Tag: ${tagInfo.currentTag}`;
}
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`
text: responseText
}
],
isError: true
@@ -704,6 +782,7 @@ function withNormalizedProjectRoot(executeFn) {
export {
getProjectRoot,
getProjectRootFromSession,
getTagInfo,
handleApiResult,
executeTaskMasterCommand,
getCachedOrExecute,

View File

@@ -60,7 +60,13 @@ export function registerValidateDependenciesTool(server) {
log.error(`Failed to validate dependencies: ${result.error.message}`);
}
return handleApiResult(result, log, 'Error validating dependencies');
return handleApiResult(
result,
log,
'Error validating dependencies',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`);
return createErrorResponse(error.message);