New update-subtask command.

This commit is contained in:
Eyal Toledano
2025-03-29 19:14:44 -04:00
parent e70f44b6fb
commit 4604f96a92
6 changed files with 877 additions and 454 deletions

View File

@@ -44,6 +44,56 @@ function getPerplexityClient() {
return perplexity;
}
/**
* Get the best available AI model for a given operation
* @param {Object} options - Options for model selection
* @param {boolean} options.claudeOverloaded - Whether Claude is currently overloaded
* @param {boolean} options.requiresResearch - Whether the operation requires research capabilities
* @returns {Object} Selected model info with type and client
*/
function getAvailableAIModel(options = {}) {
const { claudeOverloaded = false, requiresResearch = false } = options;
// First choice: Perplexity if research is required and it's available
if (requiresResearch && process.env.PERPLEXITY_API_KEY) {
try {
const client = getPerplexityClient();
return { type: 'perplexity', client };
} catch (error) {
log('warn', `Perplexity not available: ${error.message}`);
// Fall through to Claude
}
}
// Second choice: Claude if not overloaded
if (!claudeOverloaded && process.env.ANTHROPIC_API_KEY) {
return { type: 'claude', client: anthropic };
}
// Third choice: Perplexity as Claude fallback (even if research not required)
if (process.env.PERPLEXITY_API_KEY) {
try {
const client = getPerplexityClient();
log('info', 'Claude is overloaded, falling back to Perplexity');
return { type: 'perplexity', client };
} catch (error) {
log('warn', `Perplexity fallback not available: ${error.message}`);
// Fall through to Claude anyway with warning
}
}
// Last resort: Use Claude even if overloaded (might fail)
if (process.env.ANTHROPIC_API_KEY) {
if (claudeOverloaded) {
log('warn', 'Claude is overloaded but no alternatives are available. Proceeding with Claude anyway.');
}
return { type: 'claude', client: anthropic };
}
// No models available
throw new Error('No AI models available. Please set ANTHROPIC_API_KEY and/or PERPLEXITY_API_KEY.');
}
/**
* Handle Claude API errors with user-friendly messages
* @param {Error} error - The error from Claude API
@@ -54,6 +104,10 @@ function handleClaudeError(error) {
if (error.type === 'error' && error.error) {
switch (error.error.type) {
case 'overloaded_error':
// Check if we can use Perplexity as a fallback
if (process.env.PERPLEXITY_API_KEY) {
return 'Claude is currently overloaded. Trying to fall back to Perplexity AI.';
}
return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.';
case 'rate_limit_error':
return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.';
@@ -676,5 +730,6 @@ export {
generateSubtasksWithPerplexity,
parseSubtasksFromText,
generateComplexityAnalysisPrompt,
handleClaudeError
handleClaudeError,
getAvailableAIModel
};

View File

@@ -24,7 +24,8 @@ import {
addSubtask,
removeSubtask,
analyzeTaskComplexity,
updateTaskById
updateTaskById,
updateSubtaskById
} from './task-manager.js';
import {
@@ -145,7 +146,7 @@ function registerCommands(programInstance) {
await updateTasks(tasksPath, fromId, prompt, useResearch);
});
// updateTask command
// update-task command
programInstance
.command('update-task')
.description('Update a single task by ID with new information')
@@ -231,6 +232,91 @@ function registerCommands(programInstance) {
}
});
// update-subtask command
programInstance
.command('update-subtask')
.description('Update a subtask by appending additional timestamped information')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'Subtask ID to update in format "parentId.subtaskId" (required)')
.option('-p, --prompt <text>', 'Prompt explaining what information to add (required)')
.option('-r, --research', 'Use Perplexity AI for research-backed updates')
.action(async (options) => {
try {
const tasksPath = options.file;
// Validate required parameters
if (!options.id) {
console.error(chalk.red('Error: --id parameter is required'));
console.log(chalk.yellow('Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'));
process.exit(1);
}
// Validate subtask ID format (should contain a dot)
const subtaskId = options.id;
if (!subtaskId.includes('.')) {
console.error(chalk.red(`Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`));
console.log(chalk.yellow('Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'));
process.exit(1);
}
if (!options.prompt) {
console.error(chalk.red('Error: --prompt parameter is required. Please provide information to add to the subtask.'));
console.log(chalk.yellow('Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'));
process.exit(1);
}
const prompt = options.prompt;
const useResearch = options.research || false;
// Validate tasks file exists
if (!fs.existsSync(tasksPath)) {
console.error(chalk.red(`Error: Tasks file not found at path: ${tasksPath}`));
if (tasksPath === 'tasks/tasks.json') {
console.log(chalk.yellow('Hint: Run task-master init or task-master parse-prd to create tasks.json first'));
} else {
console.log(chalk.yellow(`Hint: Check if the file path is correct: ${tasksPath}`));
}
process.exit(1);
}
console.log(chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`));
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
if (useResearch) {
// Verify Perplexity API key exists if using research
if (!process.env.PERPLEXITY_API_KEY) {
console.log(chalk.yellow('Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.'));
console.log(chalk.yellow('Falling back to Claude AI for subtask update.'));
} else {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask update'));
}
}
const result = await updateSubtaskById(tasksPath, subtaskId, prompt, useResearch);
if (!result) {
console.log(chalk.yellow('\nSubtask update was not completed. Review the messages above for details.'));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
// Provide more helpful error messages for common issues
if (error.message.includes('subtask') && error.message.includes('not found')) {
console.log(chalk.yellow('\nTo fix this issue:'));
console.log(' 1. Run task-master list --with-subtasks to see all available subtask IDs');
console.log(' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"');
} else if (error.message.includes('API key')) {
console.log(chalk.yellow('\nThis error is related to API keys. Check your environment variables.'));
}
if (CONFIG.debug) {
console.error(error);
}
process.exit(1);
}
});
// generate command
programInstance
.command('generate')

View File

@@ -2969,11 +2969,319 @@ async function removeSubtask(tasksPath, subtaskId, convertToTask = false, genera
}
}
/**
* Update a subtask by appending additional information to its description and details
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} subtaskId - ID of the subtask to update in format "parentId.subtaskId"
* @param {string} prompt - Prompt for generating additional information
* @param {boolean} useResearch - Whether to use Perplexity AI for research-backed updates
* @returns {Object|null} - The updated subtask or null if update failed
*/
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) {
try {
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
// Validate subtask ID format
if (!subtaskId || typeof subtaskId !== 'string' || !subtaskId.includes('.')) {
throw new Error(`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`);
}
// Validate prompt
if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
throw new Error('Prompt cannot be empty. Please provide context for the subtask update.');
}
// Validate research flag
if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY)) {
log('warn', 'Perplexity AI is not available. Falling back to Claude AI.');
console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.'));
useResearch = false;
}
// Validate tasks file exists
if (!fs.existsSync(tasksPath)) {
throw new Error(`Tasks file not found at path: ${tasksPath}`);
}
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`);
}
// Parse parent and subtask IDs
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
const parentId = parseInt(parentIdStr, 10);
const subtaskIdNum = parseInt(subtaskIdStr, 10);
if (isNaN(parentId) || parentId <= 0 || isNaN(subtaskIdNum) || subtaskIdNum <= 0) {
throw new Error(`Invalid subtask ID format: ${subtaskId}. Both parent ID and subtask ID must be positive integers.`);
}
// Find the parent task
const parentTask = data.tasks.find(task => task.id === parentId);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentId} not found. Please verify the task ID and try again.`);
}
// Find the subtask
if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) {
throw new Error(`Parent task ${parentId} has no subtasks.`);
}
const subtask = parentTask.subtasks.find(st => st.id === subtaskIdNum);
if (!subtask) {
throw new Error(`Subtask with ID ${subtaskId} not found. Please verify the subtask ID and try again.`);
}
// Check if subtask is already completed
if (subtask.status === 'done' || subtask.status === 'completed') {
log('warn', `Subtask ${subtaskId} is already marked as done and cannot be updated`);
console.log(boxen(
chalk.yellow(`Subtask ${subtaskId} is already marked as ${subtask.status} and cannot be updated.`) + '\n\n' +
chalk.white('Completed subtasks are locked to maintain consistency. To modify a completed subtask, you must first:') + '\n' +
chalk.white('1. Change its status to "pending" or "in-progress"') + '\n' +
chalk.white('2. Then run the update-subtask command'),
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
));
return null;
}
// Show the subtask that will be updated
const table = new Table({
head: [
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Status')
],
colWidths: [10, 55, 10]
});
table.push([
subtaskId,
truncate(subtask.title, 52),
getStatusWithColor(subtask.status)
]);
console.log(boxen(
chalk.white.bold(`Updating Subtask #${subtaskId}`),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
console.log(table.toString());
// Build the system prompt
const systemPrompt = `You are an AI assistant helping to enhance a software development subtask with additional information.
You will be given a subtask and a prompt requesting specific details or clarification.
Your job is to generate concise, technically precise information that addresses the prompt.
Guidelines:
1. Focus ONLY on generating the additional information requested in the prompt
2. Be specific, technical, and actionable in your response
3. Keep your response as low level as possible, the goal is to provide the most detailed information possible to complete the task.
4. Format your response to be easily readable when appended to existing text
5. Include code snippets, links to documentation, or technical details when appropriate
6. Do NOT include any preamble, conclusion or meta-commentary
7. Return ONLY the new information to be added - do not repeat or summarize existing content`;
const subtaskData = JSON.stringify(subtask, null, 2);
let additionalInformation;
const loadingIndicator = startLoadingIndicator(useResearch
? 'Generating additional information with Perplexity AI research...'
: 'Generating additional information with Claude AI...');
try {
if (useResearch) {
log('info', 'Using Perplexity AI for research-backed subtask update');
// Verify Perplexity API key exists
if (!process.env.PERPLEXITY_API_KEY) {
throw new Error('PERPLEXITY_API_KEY environment variable is missing but --research flag was used.');
}
try {
// Call Perplexity AI
const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro';
const result = await perplexity.chat.completions.create({
model: perplexityModel,
messages: [
{
role: "system",
content: `${systemPrompt}\n\nUse your online search capabilities to research up-to-date information about the technologies and concepts mentioned in the subtask. Look for best practices, common issues, and implementation details that would be helpful.`
},
{
role: "user",
content: `Here is the subtask to enhance:
${subtaskData}
Please provide additional information addressing this request:
${prompt}
Return ONLY the new information to add - do not repeat existing content.`
}
],
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature),
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens),
});
additionalInformation = result.choices[0].message.content.trim();
} catch (perplexityError) {
throw new Error(`Perplexity API error: ${perplexityError.message}`);
}
} else {
// Call Claude to generate additional information
try {
// Verify Anthropic API key exists
if (!process.env.ANTHROPIC_API_KEY) {
throw new Error('ANTHROPIC_API_KEY environment variable is missing. Required for subtask updates.');
}
// Use streaming API call
let responseText = '';
let streamingInterval = null;
// Update loading indicator to show streaming progress
let dotCount = 0;
const readline = await import('readline');
streamingInterval = setInterval(() => {
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Receiving streaming response from Claude${'.'.repeat(dotCount)}`);
dotCount = (dotCount + 1) % 4;
}, 500);
// Use streaming API call
const stream = await anthropic.messages.create({
model: CONFIG.model,
max_tokens: CONFIG.maxTokens,
temperature: CONFIG.temperature,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Here is the subtask to enhance:
${subtaskData}
Please provide additional information addressing this request:
${prompt}
Return ONLY the new information to add - do not repeat existing content.`
}
],
stream: true
});
// Process the stream
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += chunk.delta.text;
}
}
if (streamingInterval) clearInterval(streamingInterval);
log('info', "Completed streaming response from Claude API!");
additionalInformation = responseText.trim();
} catch (claudeError) {
throw new Error(`Claude API error: ${claudeError.message}`);
}
}
// Validate the generated information
if (!additionalInformation || additionalInformation.trim() === '') {
throw new Error('Received empty response from AI. Unable to generate additional information.');
}
// Create timestamp
const currentDate = new Date();
const timestamp = currentDate.toISOString();
// Format the additional information with timestamp
const formattedInformation = `\n\n<info added on ${timestamp}>\n${additionalInformation}\n</info added on ${timestamp}>`;
// Append to subtask details and description
if (subtask.details) {
subtask.details += formattedInformation;
} else {
subtask.details = `${formattedInformation}`;
}
if (subtask.description) {
// Only append to description if it makes sense (for shorter updates)
if (additionalInformation.length < 200) {
subtask.description += ` [Updated: ${currentDate.toLocaleDateString()}]`;
}
}
// Update the subtask in the parent task
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskIdNum);
if (subtaskIndex !== -1) {
parentTask.subtasks[subtaskIndex] = subtask;
} else {
throw new Error(`Subtask with ID ${subtaskId} not found in parent task's subtasks array.`);
}
// Update the parent task in the original data
const parentIndex = data.tasks.findIndex(t => t.id === parentId);
if (parentIndex !== -1) {
data.tasks[parentIndex] = parentTask;
} else {
throw new Error(`Parent task with ID ${parentId} not found in tasks array.`);
}
// Write the updated tasks to the file
writeJSON(tasksPath, data);
log('success', `Successfully updated subtask ${subtaskId}`);
// Generate individual task files
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
console.log(boxen(
chalk.green(`Successfully updated subtask #${subtaskId}`) + '\n\n' +
chalk.white.bold('Title:') + ' ' + subtask.title + '\n\n' +
chalk.white.bold('Information Added:') + '\n' +
chalk.white(truncate(additionalInformation, 300, true)),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
));
// Return the updated subtask for testing purposes
return subtask;
} finally {
stopLoadingIndicator(loadingIndicator);
}
} catch (error) {
log('error', `Error updating subtask: ${error.message}`);
console.error(chalk.red(`Error: ${error.message}`));
// Provide more helpful error messages for common issues
if (error.message.includes('ANTHROPIC_API_KEY')) {
console.log(chalk.yellow('\nTo fix this issue, set your Anthropic API key:'));
console.log(' export ANTHROPIC_API_KEY=your_api_key_here');
} else if (error.message.includes('PERPLEXITY_API_KEY')) {
console.log(chalk.yellow('\nTo fix this issue:'));
console.log(' 1. Set your Perplexity API key: export PERPLEXITY_API_KEY=your_api_key_here');
console.log(' 2. Or run without the research flag: task-master update-subtask --id=<id> --prompt="..."');
} else if (error.message.includes('not found')) {
console.log(chalk.yellow('\nTo fix this issue:'));
console.log(' 1. Run task-master list --with-subtasks to see all available subtask IDs');
console.log(' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"');
}
if (CONFIG.debug) {
console.error(error);
}
return null;
}
}
// Export task manager functions
export {
parsePRD,
updateTasks,
updateTaskById,
updateSubtaskById,
generateTaskFiles,
setTaskStatus,
updateSingleTaskStatus,