fix(tasks): Enable removing multiple tasks/subtasks via comma-separated IDs
- Refactors the core `removeTask` function (`task-manager/remove-task.js`) to accept and iterate over comma-separated task/subtask IDs. - Updates dependency cleanup and file regeneration logic to run once after processing all specified IDs. - Adjusts the `remove-task` CLI command (`commands.js`) description and confirmation prompt to handle multiple IDs correctly. - Fixes a bug in the CLI confirmation prompt where task/subtask titles were not being displayed correctly. - Updates the `remove_task` MCP tool description to reflect the new multi-ID capability. This addresses the previously known issue where only the first ID in a comma-separated list was processed. Closes #140
This commit is contained in:
5
.changeset/neat-donkeys-shave.md
Normal file
5
.changeset/neat-donkeys-shave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'task-master-ai': patch
|
||||
---
|
||||
|
||||
Fixes an issue that prevented remove-subtask with comma separated tasks/subtasks from being deleted (only the first ID was being deleted). Closes #140
|
||||
@@ -23,7 +23,9 @@ export function registerRemoveTaskTool(server) {
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"),
|
||||
.describe(
|
||||
"ID of the task or subtask to remove (e.g., '5' or '5.2'). Can be comma-separated to update multiple tasks/subtasks at once."
|
||||
),
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
|
||||
@@ -1780,27 +1780,39 @@ function registerCommands(programInstance) {
|
||||
// remove-task command
|
||||
programInstance
|
||||
.command('remove-task')
|
||||
.description('Remove a task or subtask permanently')
|
||||
.description('Remove one or more tasks or subtasks permanently')
|
||||
.option(
|
||||
'-i, --id <id>',
|
||||
'ID of the task or subtask to remove (e.g., "5" or "5.2")'
|
||||
'-i, --id <ids>',
|
||||
'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")'
|
||||
)
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-y, --yes', 'Skip confirmation prompt', false)
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const taskId = options.id;
|
||||
const taskIdsString = options.id;
|
||||
|
||||
if (!taskId) {
|
||||
console.error(chalk.red('Error: Task ID is required'));
|
||||
if (!taskIdsString) {
|
||||
console.error(chalk.red('Error: Task ID(s) are required'));
|
||||
console.error(
|
||||
chalk.yellow('Usage: task-master remove-task --id=<taskId>')
|
||||
chalk.yellow(
|
||||
'Usage: task-master remove-task --id=<taskId1,taskId2...>'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const taskIdsToRemove = taskIdsString
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (taskIdsToRemove.length === 0) {
|
||||
console.error(chalk.red('Error: No valid task IDs provided.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the task exists
|
||||
// Read data once for checks and confirmation
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
console.error(
|
||||
@@ -1809,75 +1821,119 @@ function registerCommands(programInstance) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
console.error(chalk.red(`Error: Task with ID ${taskId} not found`));
|
||||
process.exit(1);
|
||||
const existingTasksToRemove = [];
|
||||
const nonExistentIds = [];
|
||||
let totalSubtasksToDelete = 0;
|
||||
const dependentTaskMessages = [];
|
||||
|
||||
for (const taskId of taskIdsToRemove) {
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
nonExistentIds.push(taskId);
|
||||
} else {
|
||||
// Correctly extract the task object from the result of findTaskById
|
||||
const findResult = findTaskById(data.tasks, taskId);
|
||||
const taskObject = findResult.task; // Get the actual task/subtask object
|
||||
|
||||
if (taskObject) {
|
||||
existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object
|
||||
|
||||
// If it's a main task, count its subtasks and check dependents
|
||||
if (!taskObject.isSubtask) {
|
||||
// Check the actual task object
|
||||
if (taskObject.subtasks && taskObject.subtasks.length > 0) {
|
||||
totalSubtasksToDelete += taskObject.subtasks.length;
|
||||
}
|
||||
const dependentTasks = data.tasks.filter(
|
||||
(t) =>
|
||||
t.dependencies &&
|
||||
t.dependencies.includes(parseInt(taskId, 10))
|
||||
);
|
||||
if (dependentTasks.length > 0) {
|
||||
dependentTaskMessages.push(
|
||||
` - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(', ')})`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle case where findTaskById returned null for the task property (should be rare)
|
||||
nonExistentIds.push(`${taskId} (error finding details)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load task for display
|
||||
const task = findTaskById(data.tasks, taskId);
|
||||
if (nonExistentIds.length > 0) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Warning: The following task IDs were not found: ${nonExistentIds.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingTasksToRemove.length === 0) {
|
||||
console.log(chalk.blue('No existing tasks found to remove.'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Skip confirmation if --yes flag is provided
|
||||
if (!options.yes) {
|
||||
// Display task information
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'⚠️ WARNING: This will permanently delete the following task:'
|
||||
`⚠️ WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):`
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
// It's a subtask
|
||||
const [parentId, subtaskId] = taskId.split('.');
|
||||
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
|
||||
existingTasksToRemove.forEach(({ id, task }) => {
|
||||
if (!task) return; // Should not happen due to taskExists check, but safeguard
|
||||
if (task.isSubtask) {
|
||||
// Subtask - title is directly on the task object
|
||||
console.log(
|
||||
chalk.white(` Subtask ${id}: ${task.title || '(no title)'}`)
|
||||
);
|
||||
// Optionally show parent context if available
|
||||
if (task.parentTask) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` (Parent: ${task.parentTask.id} - ${task.parentTask.title || '(no title)'})`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Main task - title is directly on the task object
|
||||
console.log(
|
||||
chalk.white.bold(` Task ${id}: ${task.title || '(no title)'}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (totalSubtasksToDelete > 0) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
|
||||
chalk.yellow(
|
||||
`⚠️ This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// It's a main task
|
||||
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
|
||||
}
|
||||
|
||||
// Show if it has subtasks
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Show if other tasks depend on it
|
||||
const dependentTasks = data.tasks.filter(
|
||||
(t) =>
|
||||
t.dependencies && t.dependencies.includes(parseInt(taskId, 10))
|
||||
if (dependentTaskMessages.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Warning: Dependencies on the following tasks will be removed:'
|
||||
)
|
||||
);
|
||||
dependentTaskMessages.forEach((msg) =>
|
||||
console.log(chalk.yellow(msg))
|
||||
);
|
||||
|
||||
if (dependentTasks.length > 0) {
|
||||
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();
|
||||
|
||||
// Prompt for confirmation
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: chalk.red.bold(
|
||||
'Are you sure you want to permanently delete this task?'
|
||||
`Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?`
|
||||
),
|
||||
default: false
|
||||
}
|
||||
@@ -1889,30 +1945,55 @@ function registerCommands(programInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
const indicator = startLoadingIndicator('Removing task...');
|
||||
const indicator = startLoadingIndicator(
|
||||
`Removing ${existingTasksToRemove.length} task(s)/subtask(s)...`
|
||||
);
|
||||
|
||||
// Remove the task
|
||||
const result = await removeTask(tasksPath, taskId);
|
||||
// Use the string of existing IDs for the core function
|
||||
const existingIdsString = existingTasksToRemove
|
||||
.map(({ id }) => id)
|
||||
.join(',');
|
||||
const result = await removeTask(tasksPath, existingIdsString);
|
||||
|
||||
stopLoadingIndicator(indicator);
|
||||
|
||||
// Display success message with appropriate color based on task or subtask
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
// It was a subtask
|
||||
if (result.success) {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(`Subtask ${taskId} has been successfully removed`),
|
||||
chalk.green(
|
||||
`Successfully removed ${result.removedTasks.length} task(s)/subtask(s).`
|
||||
) +
|
||||
(result.message ? `\n\nDetails:\n${result.message}` : '') +
|
||||
(result.error
|
||||
? `\n\nWarnings:\n${chalk.yellow(result.error)}`
|
||||
: ''),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// It was a main task
|
||||
console.log(
|
||||
boxen(chalk.green(`Task ${taskId} has been successfully removed`), {
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round'
|
||||
})
|
||||
console.error(
|
||||
boxen(
|
||||
chalk.red(
|
||||
`Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).`
|
||||
) +
|
||||
(result.message ? `\n\nDetails:\n${result.message}` : '') +
|
||||
(result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ''),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'red',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
process.exit(1); // Exit with error code if any part failed
|
||||
}
|
||||
|
||||
// Log any initially non-existent IDs again for clarity
|
||||
if (nonExistentIds.length > 0) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,151 +6,200 @@ import generateTaskFiles from './generate-task-files.js';
|
||||
import taskExists from './task-exists.js';
|
||||
|
||||
/**
|
||||
* Removes a task or subtask from the tasks file
|
||||
* Removes one or more tasks or subtasks from the tasks file
|
||||
* @param {string} tasksPath - Path to the tasks file
|
||||
* @param {string|number} taskId - ID of task or subtask to remove (e.g., '5' or '5.2')
|
||||
* @returns {Object} Result object with success message and removed task info
|
||||
* @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
|
||||
* @returns {Object} Result object with success status, messages, and removed task info
|
||||
*/
|
||||
async function removeTask(tasksPath, taskId) {
|
||||
async function removeTask(tasksPath, taskIds) {
|
||||
const results = {
|
||||
success: true,
|
||||
messages: [],
|
||||
errors: [],
|
||||
removedTasks: []
|
||||
};
|
||||
const taskIdsToRemove = taskIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean); // Remove empty strings if any
|
||||
|
||||
if (taskIdsToRemove.length === 0) {
|
||||
results.success = false;
|
||||
results.errors.push('No valid task IDs provided.');
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the tasks file
|
||||
// Read the tasks file ONCE before the loop
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Check if the task ID exists
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted
|
||||
|
||||
// Handle subtask removal (e.g., '5.2')
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentTaskId, subtaskId] = taskId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find((t) => t.id === parentTaskId);
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
throw new Error(
|
||||
`Parent task with ID ${parentTaskId} or its subtasks not found`
|
||||
);
|
||||
for (const taskId of taskIdsToRemove) {
|
||||
// Check if the task ID exists *before* attempting removal
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
const errorMsg = `Task with ID ${taskId} not found or already removed.`;
|
||||
results.errors.push(errorMsg);
|
||||
results.success = false; // Mark overall success as false if any error occurs
|
||||
continue; // Skip to the next ID
|
||||
}
|
||||
|
||||
// Find the subtask to remove
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(
|
||||
(st) => st.id === subtaskId
|
||||
);
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(
|
||||
`Subtask with ID ${subtaskId} not found in parent task ${parentTaskId}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Handle subtask removal (e.g., '5.2')
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentTaskId, subtaskId] = taskId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Store the subtask info before removal for the result
|
||||
const removedSubtask = parentTask.subtasks[subtaskIndex];
|
||||
|
||||
// Remove the subtask
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
// Remove references to this subtask in other subtasks' dependencies
|
||||
if (parentTask.subtasks && parentTask.subtasks.length > 0) {
|
||||
parentTask.subtasks.forEach((subtask) => {
|
||||
if (
|
||||
subtask.dependencies &&
|
||||
subtask.dependencies.includes(subtaskId)
|
||||
) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) => depId !== subtaskId
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find((t) => t.id === parentTaskId);
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
throw new Error(
|
||||
`Parent task ${parentTaskId} or its subtasks not found for subtask ${taskId}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save the updated tasks
|
||||
// Find the subtask to remove
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(
|
||||
(st) => st.id === subtaskId
|
||||
);
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(
|
||||
`Subtask ${subtaskId} not found in parent task ${parentTaskId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Store the subtask info before removal
|
||||
const removedSubtask = {
|
||||
...parentTask.subtasks[subtaskIndex],
|
||||
parentTaskId: parentTaskId
|
||||
};
|
||||
results.removedTasks.push(removedSubtask);
|
||||
|
||||
// Remove the subtask from the parent
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed subtask ${taskId}`);
|
||||
}
|
||||
// Handle main task removal
|
||||
else {
|
||||
const taskIdNum = parseInt(taskId, 10);
|
||||
const taskIndex = data.tasks.findIndex((t) => t.id === taskIdNum);
|
||||
if (taskIndex === -1) {
|
||||
// This case should theoretically be caught by the taskExists check above,
|
||||
// but keep it as a safeguard.
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Store the task info before removal
|
||||
const removedTask = data.tasks[taskIndex];
|
||||
results.removedTasks.push(removedTask);
|
||||
tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion
|
||||
|
||||
// Remove the task from the main array
|
||||
data.tasks.splice(taskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed task ${taskId}`);
|
||||
}
|
||||
} catch (innerError) {
|
||||
// Catch errors specific to processing *this* ID
|
||||
const errorMsg = `Error processing ID ${taskId}: ${innerError.message}`;
|
||||
results.errors.push(errorMsg);
|
||||
results.success = false;
|
||||
log('warn', errorMsg); // Log as warning and continue with next ID
|
||||
}
|
||||
} // End of loop through taskIdsToRemove
|
||||
|
||||
// --- Post-Loop Operations ---
|
||||
|
||||
// Only proceed with cleanup and saving if at least one task was potentially removed
|
||||
if (results.removedTasks.length > 0) {
|
||||
// Remove all references AFTER all tasks/subtasks are removed
|
||||
const allRemovedIds = new Set(
|
||||
taskIdsToRemove.map((id) =>
|
||||
typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10)
|
||||
)
|
||||
);
|
||||
|
||||
data.tasks.forEach((task) => {
|
||||
// Clean dependencies in main tasks
|
||||
if (task.dependencies) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => !allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
// Clean dependencies in remaining subtasks
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (subtask.dependencies) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) =>
|
||||
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
||||
!allRemovedIds.has(depId) // check both subtask and main task refs
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Save the updated tasks file ONCE
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate updated task files
|
||||
// Delete task files AFTER saving tasks.json
|
||||
for (const taskIdNum of tasksToDeleteFiles) {
|
||||
const taskFileName = path.join(
|
||||
path.dirname(tasksPath),
|
||||
`task_${taskIdNum.toString().padStart(3, '0')}.txt`
|
||||
);
|
||||
if (fs.existsSync(taskFileName)) {
|
||||
try {
|
||||
fs.unlinkSync(taskFileName);
|
||||
results.messages.push(`Deleted task file: ${taskFileName}`);
|
||||
} catch (unlinkError) {
|
||||
const unlinkMsg = `Failed to delete task file ${taskFileName}: ${unlinkError.message}`;
|
||||
results.errors.push(unlinkMsg);
|
||||
results.success = false;
|
||||
log('warn', unlinkMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate updated task files ONCE
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
results.messages.push('Task files regenerated successfully.');
|
||||
} catch (genError) {
|
||||
log(
|
||||
'warn',
|
||||
`Successfully removed subtask but failed to regenerate task files: ${genError.message}`
|
||||
);
|
||||
const genErrMsg = `Failed to regenerate task files: ${genError.message}`;
|
||||
results.errors.push(genErrMsg);
|
||||
results.success = false;
|
||||
log('warn', genErrMsg);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully removed subtask ${subtaskId} from task ${parentTaskId}`,
|
||||
removedTask: removedSubtask,
|
||||
parentTaskId: parentTaskId
|
||||
};
|
||||
} else if (results.errors.length === 0) {
|
||||
// Case where valid IDs were provided but none existed
|
||||
results.messages.push('No tasks found matching the provided IDs.');
|
||||
}
|
||||
|
||||
// Handle main task removal
|
||||
const taskIdNum = parseInt(taskId, 10);
|
||||
const taskIndex = data.tasks.findIndex((t) => t.id === taskIdNum);
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Store the task info before removal for the result
|
||||
const removedTask = data.tasks[taskIndex];
|
||||
|
||||
// Remove the task
|
||||
data.tasks.splice(taskIndex, 1);
|
||||
|
||||
// Remove references to this task in other tasks' dependencies
|
||||
data.tasks.forEach((task) => {
|
||||
if (task.dependencies && task.dependencies.includes(taskIdNum)) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => depId !== taskIdNum
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Save the updated tasks
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Delete the task file if it exists
|
||||
const taskFileName = path.join(
|
||||
path.dirname(tasksPath),
|
||||
`task_${taskIdNum.toString().padStart(3, '0')}.txt`
|
||||
);
|
||||
if (fs.existsSync(taskFileName)) {
|
||||
try {
|
||||
fs.unlinkSync(taskFileName);
|
||||
} catch (unlinkError) {
|
||||
log(
|
||||
'warn',
|
||||
`Successfully removed task from tasks.json but failed to delete task file: ${unlinkError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate updated task files
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
} catch (genError) {
|
||||
log(
|
||||
'warn',
|
||||
`Successfully removed task but failed to regenerate task files: ${genError.message}`
|
||||
);
|
||||
}
|
||||
// Consolidate messages for final output
|
||||
const finalMessage = results.messages.join('\n');
|
||||
const finalError = results.errors.join('\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully removed task ${taskId}`,
|
||||
removedTask: removedTask
|
||||
success: results.success,
|
||||
message: finalMessage || 'No tasks were removed.',
|
||||
error: finalError || null,
|
||||
removedTasks: results.removedTasks
|
||||
};
|
||||
} catch (error) {
|
||||
log('error', `Error removing task: ${error.message}`);
|
||||
throw {
|
||||
code: 'REMOVE_TASK_ERROR',
|
||||
message: error.message,
|
||||
details: error.stack
|
||||
// Catch errors from reading file or other initial setup
|
||||
log('error', `Error removing tasks: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
message: '',
|
||||
error: `Operation failed: ${error.message}`,
|
||||
removedTasks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task ID: 36
|
||||
# Title: Add Ollama Support for AI Services as Claude Alternative
|
||||
# Status: pending
|
||||
# Status: deferred
|
||||
# Dependencies: None
|
||||
# Priority: medium
|
||||
# Description: Implement Ollama integration as an alternative to Claude for all main AI services, allowing users to run local language models instead of relying on cloud-based Claude API.
|
||||
|
||||
@@ -1009,7 +1009,7 @@ When refactoring `sendChatWithContext` and related functions, ensure they align
|
||||
</info added on 2025-04-20T03:53:03.709Z>
|
||||
|
||||
## 18. Refactor Callers of AI Parsing Utilities [deferred]
|
||||
### Dependencies: 61.11,61.12,61.13,61.14,61.15,61.16,61.17,61.19
|
||||
### Dependencies: None
|
||||
### Description: Update the code that calls `parseSubtasksFromText`, `parseTaskJsonResponse`, and `parseTasksFromCompletion` to instead directly handle the structured JSON output provided by `generateObjectService` (as the refactored AI calls will now use it).
|
||||
### Details:
|
||||
|
||||
@@ -1588,7 +1588,7 @@ export async function streamOpenAITextSimplified(params) {
|
||||
</info added on 2025-04-27T05:39:31.942Z>
|
||||
|
||||
## 23. Implement Conditional Provider Logic in `ai-services-unified.js` [done]
|
||||
### Dependencies: 61.20,61.21,61.22,61.24,61.25,61.26,61.27,61.28,61.29,61.30,61.34
|
||||
### Dependencies: None
|
||||
### Description: Implement logic within the functions of `ai-services-unified.js` (e.g., `generateTextService`, `generateObjectService`, `streamChatService`) to dynamically select and call the appropriate provider module (`anthropic.js`, `perplexity.js`, etc.) based on configuration (e.g., environment variables like `AI_PROVIDER` and `AI_MODEL` from `process.env` or `session.env`).
|
||||
### Details:
|
||||
|
||||
|
||||
@@ -2298,7 +2298,7 @@
|
||||
"id": 36,
|
||||
"title": "Add Ollama Support for AI Services as Claude Alternative",
|
||||
"description": "Implement Ollama integration as an alternative to Claude for all main AI services, allowing users to run local language models instead of relying on cloud-based Claude API.",
|
||||
"status": "pending",
|
||||
"status": "deferred",
|
||||
"dependencies": [],
|
||||
"priority": "medium",
|
||||
"details": "This task involves creating a comprehensive Ollama integration that can replace Claude across all main AI services in the application. Implementation should include:\n\n1. Create an OllamaService class that implements the same interface as the ClaudeService to ensure compatibility\n2. Add configuration options to specify Ollama endpoint URL (default: http://localhost:11434)\n3. Implement model selection functionality to allow users to choose which Ollama model to use (e.g., llama3, mistral, etc.)\n4. Handle prompt formatting specific to Ollama models, ensuring proper system/user message separation\n5. Implement proper error handling for cases where Ollama server is unavailable or returns errors\n6. Add fallback mechanism to Claude when Ollama fails or isn't configured\n7. Update the AI service factory to conditionally create either Claude or Ollama service based on configuration\n8. Ensure token counting and rate limiting are appropriately handled for Ollama models\n9. Add documentation for users explaining how to set up and use Ollama with the application\n10. Optimize prompt templates specifically for Ollama models if needed\n\nThe implementation should be toggled through a configuration option (useOllama: true/false) and should maintain all existing functionality currently provided by Claude.",
|
||||
@@ -3193,9 +3193,7 @@
|
||||
"description": "Update the code that calls `parseSubtasksFromText`, `parseTaskJsonResponse`, and `parseTasksFromCompletion` to instead directly handle the structured JSON output provided by `generateObjectService` (as the refactored AI calls will now use it).",
|
||||
"details": "\n\n<info added on 2025-04-20T03:52:45.518Z>\nThe refactoring of callers to AI parsing utilities should align with the new configuration system. When updating these callers:\n\n1. Replace direct API key references with calls to the configuration system using `resolveEnvVariable` for sensitive credentials.\n\n2. Update model selection logic to use the centralized configuration from `.taskmasterconfig` via the getter functions in `config-manager.js`. For example:\n ```javascript\n // Old approach\n const model = \"gpt-4\";\n \n // New approach\n import { getModelForRole } from './config-manager';\n const model = getModelForRole('parsing'); // or appropriate role\n ```\n\n3. Similarly, replace hardcoded parameters with configuration-based values:\n ```javascript\n // Old approach\n const maxTokens = 2000;\n const temperature = 0.2;\n \n // New approach\n import { getAIParameterValue } from './config-manager';\n const maxTokens = getAIParameterValue('maxTokens', 'parsing');\n const temperature = getAIParameterValue('temperature', 'parsing');\n ```\n\n4. Ensure logging behavior respects the centralized logging configuration settings.\n\n5. When calling `generateObjectService`, pass the appropriate configuration context to ensure it uses the correct settings from the centralized configuration system.\n</info added on 2025-04-20T03:52:45.518Z>",
|
||||
"status": "deferred",
|
||||
"dependencies": [
|
||||
"61.11,61.12,61.13,61.14,61.15,61.16,61.17,61.19"
|
||||
],
|
||||
"dependencies": [],
|
||||
"parentTaskId": 61
|
||||
},
|
||||
{
|
||||
@@ -3242,9 +3240,7 @@
|
||||
"description": "Implement logic within the functions of `ai-services-unified.js` (e.g., `generateTextService`, `generateObjectService`, `streamChatService`) to dynamically select and call the appropriate provider module (`anthropic.js`, `perplexity.js`, etc.) based on configuration (e.g., environment variables like `AI_PROVIDER` and `AI_MODEL` from `process.env` or `session.env`).",
|
||||
"details": "\n\n<info added on 2025-04-20T03:52:13.065Z>\nThe unified service should now use the configuration manager for provider selection rather than directly accessing environment variables. Here's the implementation approach:\n\n1. Import the config-manager functions:\n```javascript\nconst { \n getMainProvider, \n getResearchProvider, \n getFallbackProvider,\n getModelForRole,\n getProviderParameters\n} = require('./config-manager');\n```\n\n2. Implement provider selection based on context/role:\n```javascript\nfunction selectProvider(role = 'default', context = {}) {\n // Try to get provider based on role or context\n let provider;\n \n if (role === 'research') {\n provider = getResearchProvider();\n } else if (context.fallback) {\n provider = getFallbackProvider();\n } else {\n provider = getMainProvider();\n }\n \n // Dynamically import the provider module\n return require(`./${provider}.js`);\n}\n```\n\n3. Update service functions to use this selection logic:\n```javascript\nasync function generateTextService(prompt, options = {}) {\n const { role = 'default', ...otherOptions } = options;\n const provider = selectProvider(role, options);\n const model = getModelForRole(role);\n const parameters = getProviderParameters(provider.name);\n \n return provider.generateText(prompt, { \n model, \n ...parameters,\n ...otherOptions \n });\n}\n```\n\n4. Implement fallback logic for service resilience:\n```javascript\nasync function executeWithFallback(serviceFunction, ...args) {\n try {\n return await serviceFunction(...args);\n } catch (error) {\n console.error(`Primary provider failed: ${error.message}`);\n const fallbackProvider = require(`./${getFallbackProvider()}.js`);\n return fallbackProvider[serviceFunction.name](...args);\n }\n}\n```\n\n5. Add provider capability checking to prevent calling unsupported features:\n```javascript\nfunction checkProviderCapability(provider, capability) {\n const capabilities = {\n 'anthropic': ['text', 'chat', 'stream'],\n 'perplexity': ['text', 'chat', 'stream', 'research'],\n 'openai': ['text', 'chat', 'stream', 'embedding', 'vision']\n // Add other providers as needed\n };\n \n return capabilities[provider]?.includes(capability) || false;\n}\n```\n</info added on 2025-04-20T03:52:13.065Z>",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"61.20,61.21,61.22,61.24,61.25,61.26,61.27,61.28,61.29,61.30,61.34"
|
||||
],
|
||||
"dependencies": [],
|
||||
"parentTaskId": 61
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user