From da61e9dccf2fb2581770b6369cdb186e50c03121 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Fri, 21 Mar 2025 14:06:36 -0400 Subject: [PATCH] feat: add cursor_rules and self_improve support, enhance versioning --- .cursor/rules/cursor_rules.mdc | 53 ++ .cursor/rules/self_improve.mdc | 73 +++ package.json | 4 +- scripts/dev.js | 122 +++-- scripts/init.js | 12 + scripts/prepare-package.js | 56 +- templates/dev.js | 898 +++++++++++++++++++++++---------- templates/env.example | 3 +- 8 files changed, 900 insertions(+), 321 deletions(-) create mode 100644 .cursor/rules/cursor_rules.mdc create mode 100644 .cursor/rules/self_improve.mdc diff --git a/.cursor/rules/cursor_rules.mdc b/.cursor/rules/cursor_rules.mdc new file mode 100644 index 00000000..7dfae3de --- /dev/null +++ b/.cursor/rules/cursor_rules.mdc @@ -0,0 +1,53 @@ +--- +description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. +globs: .cursor/rules/*.mdc +alwaysApply: true +--- + +- **Required Rule Structure:** + ```markdown + --- + description: Clear, one-line description of what the rule enforces + globs: path/to/files/*.ext, other/path/**/* + alwaysApply: boolean + --- + + - **Main Points in Bold** + - Sub-points with details + - Examples and explanations + ``` + +- **File References:** + - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files + - Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references + - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references + +- **Code Examples:** + - Use language-specific code blocks + ```typescript + // ✅ DO: Show good examples + const goodExample = true; + + // ❌ DON'T: Show anti-patterns + const badExample = false; + ``` + +- **Rule Content Guidelines:** + - Start with high-level overview + - Include specific, actionable requirements + - Show examples of correct implementation + - Reference existing code when possible + - Keep rules DRY by referencing other rules + +- **Rule Maintenance:** + - Update rules when new patterns emerge + - Add examples from actual codebase + - Remove outdated patterns + - Cross-reference related rules + +- **Best Practices:** + - Use bullet points for clarity + - Keep descriptions concise + - Include both DO and DON'T examples + - Reference actual code over theoretical examples + - Use consistent formatting across rules \ No newline at end of file diff --git a/.cursor/rules/self_improve.mdc b/.cursor/rules/self_improve.mdc new file mode 100644 index 00000000..a7ea8f28 --- /dev/null +++ b/.cursor/rules/self_improve.mdc @@ -0,0 +1,73 @@ +--- +description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices. +globs: **/* +alwaysApply: true +--- + +- **Rule Improvement Triggers:** + - New code patterns not covered by existing rules + - Repeated similar implementations across files + - Common error patterns that could be prevented + - New libraries or tools being used consistently + - Emerging best practices in the codebase + +- **Analysis Process:** + - Compare new code with existing rules + - Identify patterns that should be standardized + - Look for references to external documentation + - Check for consistent error handling patterns + - Monitor test patterns and coverage + +- **Rule Updates:** + - **Add New Rules When:** + - A new technology/pattern is used in 3+ files + - Common bugs could be prevented by a rule + - Code reviews repeatedly mention the same feedback + - New security or performance patterns emerge + + - **Modify Existing Rules When:** + - Better examples exist in the codebase + - Additional edge cases are discovered + - Related rules have been updated + - Implementation details have changed + +- **Example Pattern Recognition:** + ```typescript + // If you see repeated patterns like: + const data = await prisma.user.findMany({ + select: { id: true, email: true }, + where: { status: 'ACTIVE' } + }); + + // Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc): + // - Standard select fields + // - Common where conditions + // - Performance optimization patterns + ``` + +- **Rule Quality Checks:** + - Rules should be actionable and specific + - Examples should come from actual code + - References should be up to date + - Patterns should be consistently enforced + +- **Continuous Improvement:** + - Monitor code review comments + - Track common development questions + - Update rules after major refactors + - Add links to relevant documentation + - Cross-reference related rules + +- **Rule Deprecation:** + - Mark outdated patterns as deprecated + - Remove rules that no longer apply + - Update references to deprecated rules + - Document migration paths for old patterns + +- **Documentation Updates:** + - Keep examples synchronized with code + - Update references to external docs + - Maintain links between related rules + - Document breaking changes + +Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure. \ No newline at end of file diff --git a/package.json b/package.json index 319d836a..c59460bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-task-master", - "version": "1.4.0", + "version": "1.4.2", "description": "A task management system for AI-driven development with Claude", "main": "index.js", "type": "module", @@ -50,4 +50,4 @@ "README.md", "index.js" ] -} +} \ No newline at end of file diff --git a/scripts/dev.js b/scripts/dev.js index b27b64ef..61e75124 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -537,7 +537,8 @@ function generateTaskFiles(tasksPath, outputDir) { const filename = `task_${String(task.id).padStart(3, '0')}.txt`; const filepath = path.join(outputDir, filename); - const content = [ + // Create the base content + const contentParts = [ `# Task ID: ${task.id}`, `# Title: ${task.title}`, `# Status: ${task.status}`, @@ -547,8 +548,24 @@ function generateTaskFiles(tasksPath, outputDir) { `# Details:\n${task.details}\n`, `# Test Strategy:`, `${task.testStrategy}\n` - ].join('\n'); + ]; + + // Add subtasks if they exist + if (task.subtasks && task.subtasks.length > 0) { + contentParts.push(`# Subtasks:`); + task.subtasks.forEach(subtask => { + contentParts.push(`## Subtask ID: ${subtask.id}`); + contentParts.push(`## Title: ${subtask.title}`); + contentParts.push(`## Status: ${subtask.status}`); + contentParts.push(`## Dependencies: ${subtask.dependencies ? subtask.dependencies.join(", ") : ""}`); + contentParts.push(`## Description: ${subtask.description}`); + if (subtask.acceptanceCriteria) { + contentParts.push(`## Acceptance Criteria:\n${subtask.acceptanceCriteria}\n`); + } + }); + } + const content = contentParts.join('\n'); fs.writeFileSync(filepath, content, 'utf8'); log('info', `Generated: ${filename}`); }); @@ -560,74 +577,94 @@ function generateTaskFiles(tasksPath, outputDir) { // 4) set-status // function setTaskStatus(tasksPath, taskIdInput, newStatus) { - // For recursive calls with multiple IDs, we need to read the latest data each time + // Validate inputs + if (!taskIdInput || !newStatus) { + log('error', 'Task ID and new status are required'); + process.exit(1); + } + + // Read fresh data for each status update const data = readJSON(tasksPath); if (!data || !data.tasks) { - log('error', "No valid tasks found."); + log('error', 'No valid tasks found in tasks.json'); process.exit(1); } // Handle multiple task IDs (comma-separated) if (typeof taskIdInput === 'string' && taskIdInput.includes(',')) { const taskIds = taskIdInput.split(',').map(id => id.trim()); - log('info', `Processing multiple tasks: ${taskIds.join(', ')}`); + log('info', `Processing multiple task IDs: ${taskIds.join(', ')}`); // Process each task ID individually - for (const taskId of taskIds) { - // Create a new instance for each task to ensure we're working with fresh data - setTaskStatus(tasksPath, taskId, newStatus); - } - + taskIds.forEach(id => { + setTaskStatus(tasksPath, id, newStatus); + }); return; } - // Convert numeric taskId to number if it's not a subtask ID - const taskId = (!isNaN(taskIdInput) && !String(taskIdInput).includes('.')) - ? parseInt(taskIdInput, 10) - : taskIdInput; - - // Check if this is a subtask ID (e.g., "1.1") - if (typeof taskId === 'string' && taskId.includes('.')) { - const [parentIdStr, subtaskIdStr] = taskId.split('.'); + // Handle subtask IDs (e.g., "1.1") + if (String(taskIdInput).includes('.')) { + const [parentIdStr, subtaskIdStr] = String(taskIdInput).split('.'); const parentId = parseInt(parentIdStr, 10); const subtaskId = parseInt(subtaskIdStr, 10); - + + if (isNaN(parentId) || isNaN(subtaskId)) { + log('error', `Invalid subtask ID format: ${taskIdInput}`); + process.exit(1); + } + + // Find the parent task const parentTask = data.tasks.find(t => t.id === parentId); - if (!parentTask) { - log('error', `Parent task with ID=${parentId} not found.`); + log('error', `Parent task ${parentId} not found`); process.exit(1); } - - if (!parentTask.subtasks || parentTask.subtasks.length === 0) { - log('error', `Parent task with ID=${parentId} has no subtasks.`); + + // Ensure subtasks array exists + if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) { + log('error', `Parent task ${parentId} has no subtasks array`); process.exit(1); } - + + // Find and update the subtask const subtask = parentTask.subtasks.find(st => st.id === subtaskId); if (!subtask) { - log('error', `Subtask with ID=${subtaskId} not found in parent task ID=${parentId}.`); + log('error', `Subtask ${subtaskId} not found in task ${parentId}`); process.exit(1); } - - const oldStatus = subtask.status; + + // Update the subtask status + const oldStatus = subtask.status || 'pending'; subtask.status = newStatus; + + // Save the changes writeJSON(tasksPath, data); - log('info', `Subtask ${parentId}.${subtaskId} status changed from '${oldStatus}' to '${newStatus}'.`); + log('info', `Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'`); + return; } // Handle regular task ID - const task = data.tasks.find(t => t.id === taskId); - if (!task) { - log('error', `Task with ID=${taskId} not found.`); + const taskId = parseInt(String(taskIdInput), 10); + if (isNaN(taskId)) { + log('error', `Invalid task ID: ${taskIdInput}`); process.exit(1); } - const oldStatus = task.status; + // Find the task + const task = data.tasks.find(t => t.id === taskId); + if (!task) { + log('error', `Task ${taskId} not found`); + process.exit(1); + } + + // Update the task status + const oldStatus = task.status || 'pending'; task.status = newStatus; + + // Save the changes writeJSON(tasksPath, data); - log('info', `Task ID=${taskId} status changed from '${oldStatus}' to '${newStatus}'.`); + log('info', `Updated task ${taskId} status from '${oldStatus}' to '${newStatus}'`); } // @@ -1279,14 +1316,20 @@ async function main() { program .command('set-status') .description('Set the status of a task') - .argument('', 'Task ID') - .argument('', 'New status (todo, in-progress, review, done)') + .option('-i, --id ', 'Task ID (can be comma-separated for multiple tasks)') + .option('-s, --status ', 'New status (todo, in-progress, review, done)') .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (id, status, options) => { + .action(async (options) => { const tasksPath = options.file; - const taskId = parseInt(id, 10); + const taskId = options.id; + const status = options.status; - console.log(chalk.blue(`Setting status of task ${taskId} to: ${status}`)); + if (!taskId || !status) { + console.error(chalk.red('Error: Both --id and --status are required')); + process.exit(1); + } + + console.log(chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`)); await setTaskStatus(tasksPath, taskId, status); }); @@ -1362,7 +1405,6 @@ async function main() { await program.parseAsync(process.argv); } -// ... existing code ... main().catch(err => { log('error', err); diff --git a/scripts/init.js b/scripts/init.js index 24b26f32..dd32f3a9 100755 --- a/scripts/init.js +++ b/scripts/init.js @@ -86,6 +86,12 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) { case 'dev_workflow.mdc': sourcePath = path.join(__dirname, '..', '.cursor', 'rules', 'dev_workflow.mdc'); break; + case 'cursor_rules.mdc': + sourcePath = path.join(__dirname, '..', '.cursor', 'rules', 'cursor_rules.mdc'); + break; + case 'self_improve.mdc': + sourcePath = path.join(__dirname, '..', '.cursor', 'rules', 'self_improve.mdc'); + break; case 'README.md': sourcePath = path.join(__dirname, '..', 'README.md'); break; @@ -234,6 +240,12 @@ function createProjectStructure(projectName, projectDescription, projectVersion, // Copy dev_workflow.mdc copyTemplateFile('dev_workflow.mdc', path.join(targetDir, '.cursor', 'rules', 'dev_workflow.mdc')); + // Copy cursor_rules.mdc + copyTemplateFile('cursor_rules.mdc', path.join(targetDir, '.cursor', 'rules', 'cursor_rules.mdc')); + + // Copy self_improve.mdc + copyTemplateFile('self_improve.mdc', path.join(targetDir, '.cursor', 'rules', 'self_improve.mdc')); + // Copy scripts/dev.js copyTemplateFile('dev.js', path.join(targetDir, 'scripts', 'dev.js')); diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index a4abaac3..88b77eaf 100755 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -3,6 +3,12 @@ /** * This script prepares the package for publication to NPM. * It ensures all necessary files are included and properly configured. + * + * Additional options: + * --patch: Increment patch version (default) + * --minor: Increment minor version + * --major: Increment major version + * --version=x.y.z: Set specific version */ import fs from 'fs'; @@ -27,6 +33,16 @@ const COLORS = { cyan: '\x1b[36m' }; +// Parse command line arguments +const args = process.argv.slice(2); +const versionBump = args.includes('--major') ? 'major' : + args.includes('--minor') ? 'minor' : + 'patch'; + +// Check for explicit version +const versionArg = args.find(arg => arg.startsWith('--version=')); +const explicitVersion = versionArg ? versionArg.split('=')[1] : null; + // Log function with color support function log(level, ...args) { const prefix = { @@ -63,11 +79,44 @@ function syncTemplateFiles() { return true; } +// Function to increment version +function incrementVersion(currentVersion, type = 'patch') { + const [major, minor, patch] = currentVersion.split('.').map(Number); + + switch (type) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + default: + return `${major}.${minor}.${patch + 1}`; + } +} + // Main function to prepare the package function preparePackage() { const rootDir = path.join(__dirname, '..'); log('info', `Preparing package in ${rootDir}`); + // Update version in package.json + const packageJsonPath = path.join(rootDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const currentVersion = packageJson.version; + + let newVersion; + if (explicitVersion) { + newVersion = explicitVersion; + log('info', `Setting version to specified ${newVersion} (was ${currentVersion})`); + } else { + newVersion = incrementVersion(currentVersion, versionBump); + log('info', `Incrementing ${versionBump} version to ${newVersion} (was ${currentVersion})`); + } + + packageJson.version = newVersion; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + log('success', `Updated package.json version to ${newVersion}`); + // Check for required files const requiredFiles = [ 'package.json', @@ -78,7 +127,9 @@ function preparePackage() { 'assets/env.example', 'assets/gitignore', 'assets/example_prd.txt', - '.cursor/rules/dev_workflow.mdc' + '.cursor/rules/dev_workflow.mdc', + '.cursor/rules/cursor_rules.mdc', + '.cursor/rules/self_improve.mdc' ]; let allFilesExist = true; @@ -134,7 +185,8 @@ function preparePackage() { log('error', 'Failed to make scripts executable:', error.message); } - log('success', 'Package preparation completed successfully!'); + log('success', `Package preparation completed successfully! 🎉`); + log('success', `Version updated to ${newVersion}`); log('info', 'You can now publish the package with:'); log('info', ' npm publish'); } diff --git a/templates/dev.js b/templates/dev.js index 4a6b535a..c5068efb 100755 --- a/templates/dev.js +++ b/templates/dev.js @@ -9,7 +9,9 @@ * -> Optional --tasks parameter limits the number of tasks generated. * * 2) update --from=5 --prompt="We changed from Slack to Discord." - * -> Regenerates tasks from ID >= 5 using the provided prompt (or naive approach). + * -> Regenerates tasks from ID >= 5 using the provided prompt. + * -> Only updates tasks that aren't marked as 'done'. + * -> The --prompt parameter is required and should explain the changes or new context. * * 3) generate * -> Generates per-task files (e.g., task_001.txt) from tasks.json @@ -21,10 +23,12 @@ * 5) list * -> Lists tasks in a brief console view (ID, title, status). * - * 6) expand --id=3 --subtasks=5 [--prompt="Additional context"] + * 6) expand --id=3 [--num=5] [--no-research] [--prompt="Additional context"] * -> Expands a task with subtasks for more detailed implementation. * -> Use --all instead of --id to expand all tasks. - * -> Optional --subtasks parameter controls number of subtasks (default: 3). + * -> Optional --num parameter controls number of subtasks (default: 3). + * -> Uses Perplexity AI for research-backed subtask generation by default. + * -> Use --no-research to disable research-backed generation. * -> Add --force when using --all to regenerate subtasks for tasks that already have them. * -> Note: Tasks marked as 'done' or 'completed' are always skipped. * @@ -35,31 +39,51 @@ * node dev.js generate * node dev.js set-status --id=3 --status=done * node dev.js list - * node dev.js expand --id=3 --subtasks=5 + * node dev.js expand --id=3 --num=5 + * node dev.js expand --id=3 --no-research * node dev.js expand --all * node dev.js expand --all --force */ import fs from 'fs'; import path from 'path'; -import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import readline from 'readline'; +import { program } from 'commander'; +import chalk from 'chalk'; +import { Anthropic } from '@anthropic-ai/sdk'; +import OpenAI from 'openai'; +import dotenv from 'dotenv'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Load environment variables from .env file +// Load environment variables dotenv.config(); -import Anthropic from '@anthropic-ai/sdk'; +// Configure Anthropic client +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Configure OpenAI client for Perplexity +const perplexity = new OpenAI({ + apiKey: process.env.PERPLEXITY_API_KEY, + baseURL: 'https://api.perplexity.ai', +}); + +// Model configuration +const MODEL = process.env.MODEL || 'claude-3-7-sonnet-20250219'; +const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || 'sonar-small-online'; +const MAX_TOKENS = parseInt(process.env.MAX_TOKENS || '4000'); +const TEMPERATURE = parseFloat(process.env.TEMPERATURE || '0.7'); // Set up configuration with environment variables or defaults const CONFIG = { - model: process.env.MODEL || "claude-3-7-sonnet-20250219", - maxTokens: parseInt(process.env.MAX_TOKENS || "4000"), - temperature: parseFloat(process.env.TEMPERATURE || "0.7"), + model: MODEL, + maxTokens: MAX_TOKENS, + temperature: TEMPERATURE, debug: process.env.DEBUG === "true", logLevel: process.env.LOG_LEVEL || "info", defaultSubtasks: parseInt(process.env.DEFAULT_SUBTASKS || "3"), @@ -95,10 +119,6 @@ function log(level, ...args) { } } -const anthropic = new Anthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, -}); - function readJSON(filepath) { if (!fs.existsSync(filepath)) return null; const content = fs.readFileSync(filepath, 'utf8'); @@ -517,7 +537,8 @@ function generateTaskFiles(tasksPath, outputDir) { const filename = `task_${String(task.id).padStart(3, '0')}.txt`; const filepath = path.join(outputDir, filename); - const content = [ + // Create the base content + const contentParts = [ `# Task ID: ${task.id}`, `# Title: ${task.title}`, `# Status: ${task.status}`, @@ -527,8 +548,24 @@ function generateTaskFiles(tasksPath, outputDir) { `# Details:\n${task.details}\n`, `# Test Strategy:`, `${task.testStrategy}\n` - ].join('\n'); + ]; + + // Add subtasks if they exist + if (task.subtasks && task.subtasks.length > 0) { + contentParts.push(`# Subtasks:`); + task.subtasks.forEach(subtask => { + contentParts.push(`## Subtask ID: ${subtask.id}`); + contentParts.push(`## Title: ${subtask.title}`); + contentParts.push(`## Status: ${subtask.status}`); + contentParts.push(`## Dependencies: ${subtask.dependencies ? subtask.dependencies.join(", ") : ""}`); + contentParts.push(`## Description: ${subtask.description}`); + if (subtask.acceptanceCriteria) { + contentParts.push(`## Acceptance Criteria:\n${subtask.acceptanceCriteria}\n`); + } + }); + } + const content = contentParts.join('\n'); fs.writeFileSync(filepath, content, 'utf8'); log('info', `Generated: ${filename}`); }); @@ -540,80 +577,100 @@ function generateTaskFiles(tasksPath, outputDir) { // 4) set-status // function setTaskStatus(tasksPath, taskIdInput, newStatus) { - // For recursive calls with multiple IDs, we need to read the latest data each time + // Validate inputs + if (!taskIdInput || !newStatus) { + log('error', 'Task ID and new status are required'); + process.exit(1); + } + + // Read fresh data for each status update const data = readJSON(tasksPath); if (!data || !data.tasks) { - log('error', "No valid tasks found."); + log('error', 'No valid tasks found in tasks.json'); process.exit(1); } // Handle multiple task IDs (comma-separated) if (typeof taskIdInput === 'string' && taskIdInput.includes(',')) { const taskIds = taskIdInput.split(',').map(id => id.trim()); - log('info', `Processing multiple tasks: ${taskIds.join(', ')}`); + log('info', `Processing multiple task IDs: ${taskIds.join(', ')}`); // Process each task ID individually - for (const taskId of taskIds) { - // Create a new instance for each task to ensure we're working with fresh data - setTaskStatus(tasksPath, taskId, newStatus); - } - + taskIds.forEach(id => { + setTaskStatus(tasksPath, id, newStatus); + }); return; } - // Convert numeric taskId to number if it's not a subtask ID - const taskId = (!isNaN(taskIdInput) && !String(taskIdInput).includes('.')) - ? parseInt(taskIdInput, 10) - : taskIdInput; - - // Check if this is a subtask ID (e.g., "1.1") - if (typeof taskId === 'string' && taskId.includes('.')) { - const [parentIdStr, subtaskIdStr] = taskId.split('.'); + // Handle subtask IDs (e.g., "1.1") + if (String(taskIdInput).includes('.')) { + const [parentIdStr, subtaskIdStr] = String(taskIdInput).split('.'); const parentId = parseInt(parentIdStr, 10); const subtaskId = parseInt(subtaskIdStr, 10); - + + if (isNaN(parentId) || isNaN(subtaskId)) { + log('error', `Invalid subtask ID format: ${taskIdInput}`); + process.exit(1); + } + + // Find the parent task const parentTask = data.tasks.find(t => t.id === parentId); - if (!parentTask) { - log('error', `Parent task with ID=${parentId} not found.`); + log('error', `Parent task ${parentId} not found`); process.exit(1); } - - if (!parentTask.subtasks || parentTask.subtasks.length === 0) { - log('error', `Parent task with ID=${parentId} has no subtasks.`); + + // Ensure subtasks array exists + if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) { + log('error', `Parent task ${parentId} has no subtasks array`); process.exit(1); } - + + // Find and update the subtask const subtask = parentTask.subtasks.find(st => st.id === subtaskId); if (!subtask) { - log('error', `Subtask with ID=${subtaskId} not found in parent task ID=${parentId}.`); + log('error', `Subtask ${subtaskId} not found in task ${parentId}`); process.exit(1); } - - const oldStatus = subtask.status; + + // Update the subtask status + const oldStatus = subtask.status || 'pending'; subtask.status = newStatus; + + // Save the changes writeJSON(tasksPath, data); - log('info', `Subtask ${parentId}.${subtaskId} status changed from '${oldStatus}' to '${newStatus}'.`); + log('info', `Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'`); + return; } // Handle regular task ID - const task = data.tasks.find(t => t.id === taskId); - if (!task) { - log('error', `Task with ID=${taskId} not found.`); + const taskId = parseInt(String(taskIdInput), 10); + if (isNaN(taskId)) { + log('error', `Invalid task ID: ${taskIdInput}`); process.exit(1); } - const oldStatus = task.status; + // Find the task + const task = data.tasks.find(t => t.id === taskId); + if (!task) { + log('error', `Task ${taskId} not found`); + process.exit(1); + } + + // Update the task status + const oldStatus = task.status || 'pending'; task.status = newStatus; + + // Save the changes writeJSON(tasksPath, data); - log('info', `Task ID=${taskId} status changed from '${oldStatus}' to '${newStatus}'.`); + log('info', `Updated task ${taskId} status from '${oldStatus}' to '${newStatus}'`); } // // 5) list tasks // -function listTasks(tasksPath) { +function listTasks(tasksPath, statusFilter, withSubtasks = false) { const data = readJSON(tasksPath); if (!data || !data.tasks) { log('error', "No valid tasks found."); @@ -621,114 +678,138 @@ function listTasks(tasksPath) { } log('info', `Tasks in ${tasksPath}:`); - data.tasks.forEach(t => { + + // Filter tasks by status if a filter is provided + const filteredTasks = statusFilter + ? data.tasks.filter(t => t.status === statusFilter) + : data.tasks; + + filteredTasks.forEach(t => { log('info', `- ID=${t.id}, [${t.status}] ${t.title}`); + + // Display subtasks if requested and they exist + if (withSubtasks && t.subtasks && t.subtasks.length > 0) { + t.subtasks.forEach(st => { + log('info', ` └─ ID=${t.id}.${st.id}, [${st.status || 'pending'}] ${st.title}`); + }); + } }); + + // If no tasks match the filter, show a message + if (filteredTasks.length === 0) { + log('info', `No tasks found${statusFilter ? ` with status '${statusFilter}'` : ''}.`); + } } // // 6) expand task with subtasks // -async function expandTask(tasksPath, taskId, numSubtasks, additionalContext = '') { - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', "No valid tasks found."); - process.exit(1); +/** + * Expand a task by generating subtasks + * @param {string} taskId - The ID of the task to expand + * @param {number} numSubtasks - The number of subtasks to generate + * @param {boolean} useResearch - Whether to use Perplexity for research-backed subtask generation + * @returns {Promise} + */ +async function expandTask(taskId, numSubtasks = CONFIG.defaultSubtasks, useResearch = false, additionalContext = '') { + try { + // Get the tasks + const tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json')); + const task = tasksData.tasks.find(t => t.id === parseInt(taskId)); + + if (!task) { + console.error(chalk.red(`Task with ID ${taskId} not found.`)); + return; + } + + // Check if the task is already completed + if (task.status === 'completed' || task.status === 'done') { + console.log(chalk.yellow(`Task ${taskId} is already completed. Skipping expansion.`)); + return; + } + + // Initialize subtasks array if it doesn't exist + if (!task.subtasks) { + task.subtasks = []; + } + + // Calculate the next subtask ID + const nextSubtaskId = task.subtasks.length > 0 + ? Math.max(...task.subtasks.map(st => st.id)) + 1 + : 1; + + // Generate subtasks + let subtasks; + if (useResearch) { + console.log(chalk.blue(`Using Perplexity AI for research-backed subtask generation...`)); + subtasks = await generateSubtasksWithPerplexity(task, numSubtasks, nextSubtaskId, additionalContext); + } else { + subtasks = await generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext); + } + + // Add the subtasks to the task + task.subtasks = [...task.subtasks, ...subtasks]; + + // Save the updated tasks + fs.writeFileSync( + path.join(process.cwd(), 'tasks', 'tasks.json'), + JSON.stringify(tasksData, null, 2) + ); + + console.log(chalk.green(`Added ${subtasks.length} subtasks to task ${taskId}.`)); + + // Log the added subtasks + subtasks.forEach(st => { + console.log(chalk.cyan(` ${st.id}. ${st.title}`)); + console.log(chalk.gray(` ${st.description.substring(0, 100)}${st.description.length > 100 ? '...' : ''}`)); + }); + } catch (error) { + console.error(chalk.red('Error expanding task:'), error); } - - // Use default subtasks count from config if not specified - numSubtasks = numSubtasks || CONFIG.defaultSubtasks; - - const task = data.tasks.find(t => t.id === taskId); - if (!task) { - log('error', `Task with ID=${taskId} not found.`); - process.exit(1); - } - - // Skip tasks that are already completed - if (task.status === 'done' || task.status === 'completed') { - log('info', `Skipping task ID=${taskId} "${task.title}" - task is already marked as ${task.status}.`); - log('info', `Use set-status command to change the status if you want to modify this task.`); - return false; - } - - log('info', `Expanding task: ${task.title}`); - - // Initialize subtasks array if it doesn't exist - if (!task.subtasks) { - task.subtasks = []; - } - - // Calculate next subtask ID - const nextSubtaskId = task.subtasks.length > 0 - ? Math.max(...task.subtasks.map(st => st.id)) + 1 - : 1; - - // Generate subtasks using Claude - const subtasks = await generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext); - - // Add new subtasks to the task - task.subtasks = [...task.subtasks, ...subtasks]; - - // Update tasks.json - writeJSON(tasksPath, data); - log('info', `Added ${subtasks.length} subtasks to task ID=${taskId}.`); - - // Print the new subtasks - log('info', "New subtasks:"); - subtasks.forEach(st => { - log('info', `- ${st.id}. ${st.title}`); - }); - - return true; } -// -// Expand all tasks with subtasks -// -async function expandAllTasks(tasksPath, numSubtasks, additionalContext = '', forceRegenerate = false) { - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', "No valid tasks found."); - process.exit(1); - } - - log('info', `Expanding all ${data.tasks.length} tasks with subtasks...`); - - let tasksExpanded = 0; - let tasksSkipped = 0; - let tasksCompleted = 0; - - // Process each task sequentially to avoid overwhelming the API - for (const task of data.tasks) { - // Skip tasks that are already completed - if (task.status === 'done' || task.status === 'completed') { - log('info', `Skipping task ID=${task.id} "${task.title}" - task is already marked as ${task.status}.`); - tasksCompleted++; - continue; +/** + * Expand all tasks that are not completed + * @param {number} numSubtasks - The number of subtasks to generate for each task + * @param {boolean} useResearch - Whether to use Perplexity for research-backed subtask generation + * @returns {Promise} - The number of tasks expanded + */ +async function expandAllTasks(numSubtasks = CONFIG.defaultSubtasks, useResearch = false, additionalContext = '', forceFlag = false) { + try { + // Get the tasks + const tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json')); + + if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { + console.error(chalk.red('No valid tasks found.')); + return 0; } - // Skip tasks that already have subtasks unless force regeneration is enabled - if (!forceRegenerate && task.subtasks && task.subtasks.length > 0) { - log('info', `Skipping task ID=${task.id} "${task.title}" - already has ${task.subtasks.length} subtasks`); - tasksSkipped++; - continue; + // Filter tasks that are not completed + const tasksToExpand = tasksData.tasks.filter(task => + task.status !== 'completed' && task.status !== 'done' + ); + + if (tasksToExpand.length === 0) { + console.log(chalk.yellow('No tasks to expand. All tasks are already completed.')); + return 0; } - const success = await expandTask(tasksPath, task.id, numSubtasks, additionalContext); - if (success) { + console.log(chalk.blue(`Expanding ${tasksToExpand.length} tasks with ${numSubtasks} subtasks each...`)); + + let tasksExpanded = 0; + + // Expand each task + for (const task of tasksToExpand) { + console.log(chalk.blue(`\nExpanding task ${task.id}: ${task.title}`)); + await expandTask(task.id, numSubtasks, useResearch, additionalContext); tasksExpanded++; } - } - - log('info', `Expansion complete: ${tasksExpanded} tasks expanded, ${tasksSkipped} tasks skipped (already had subtasks), ${tasksCompleted} tasks skipped (already completed).`); - - if (tasksSkipped > 0) { - log('info', `Tip: Use --force flag to regenerate subtasks for all tasks, including those that already have subtasks.`); - } - - if (tasksCompleted > 0) { - log('info', `Note: Completed tasks are always skipped. Use set-status command to change task status if needed.`); + + console.log(chalk.green(`\nExpanded ${tasksExpanded} tasks with ${numSubtasks} subtasks each.`)); + return tasksExpanded; + } catch (error) { + console.error(chalk.red('Error expanding all tasks:'), error); + return 0; } } @@ -769,31 +850,61 @@ Then continue with Subtask ${nextSubtaskId + 1}, and so on. log('info', "Calling Claude to generate subtasks..."); - const response = await anthropic.messages.create({ - max_tokens: CONFIG.maxTokens, - model: CONFIG.model, - temperature: CONFIG.temperature, - messages: [ - { - role: "user", - content: prompt + // Start loading indicator + const loadingIndicator = startLoadingIndicator("Waiting for Claude to generate subtasks..."); + + let fullResponse = ''; + let streamingInterval = null; + + try { + const stream = await anthropic.messages.create({ + max_tokens: CONFIG.maxTokens, + model: CONFIG.model, + temperature: CONFIG.temperature, + messages: [ + { + role: "user", + content: prompt + } + ], + system: "You are a helpful assistant that generates detailed subtasks for software development tasks. Your subtasks should be specific, actionable, and help accomplish the main task. Format each subtask with a title, description, dependencies, and acceptance criteria.", + stream: true + }); + + // Update loading indicator to show streaming progress + let dotCount = 0; + streamingInterval = setInterval(() => { + readline.cursorTo(process.stdout, 0); + process.stdout.write(`Receiving streaming response from Claude${'.'.repeat(dotCount)}`); + dotCount = (dotCount + 1) % 4; + }, 500); + + // Process the stream + for await (const chunk of stream) { + if (chunk.type === 'content_block_delta' && chunk.delta.text) { + fullResponse += chunk.delta.text; } - ], - system: "You are a helpful assistant that generates detailed subtasks for software development tasks. Your subtasks should be specific, actionable, and help accomplish the main task. Format each subtask with a title, description, dependencies, and acceptance criteria." - }); - - log('info', "Received response from Claude API!"); - - // Extract the text content from the response - const textContent = response.content[0].text; - - // Log the first part of the response for debugging - log('debug', "Response preview:", textContent.substring(0, 200) + "..."); - - // Parse the subtasks from the text response - const subtasks = parseSubtasksFromText(textContent, nextSubtaskId, numSubtasks); - - return subtasks; + } + + clearInterval(streamingInterval); + + // Stop loading indicator + stopLoadingIndicator(loadingIndicator); + log('info', "Received complete response from Claude API!"); + + // Log the first part of the response for debugging + log('debug', "Response preview:", fullResponse.substring(0, 200) + "..."); + + // Parse the subtasks from the text response + const subtasks = parseSubtasksFromText(fullResponse, nextSubtaskId, numSubtasks); + + return subtasks; + } catch (error) { + if (streamingInterval) clearInterval(streamingInterval); + stopLoadingIndicator(loadingIndicator); + log('error', "Error during streaming response:", error); + throw error; + } } // @@ -931,135 +1042,370 @@ function parseSubtasksFromText(text, startId, expectedCount) { return subtasks; } +/** + * Generate subtasks for a task using Perplexity AI with research capabilities + * @param {Object} task - The task to generate subtasks for + * @param {number} numSubtasks - The number of subtasks to generate + * @param {number} nextSubtaskId - The ID to start assigning to subtasks + * @returns {Promise} - The generated subtasks + */ +async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '') { + const { title, description, details = '', subtasks = [] } = task; + + console.log(chalk.blue(`Generating ${numSubtasks} subtasks for task: ${title}`)); + if (subtasks.length > 0) { + console.log(chalk.yellow(`Task already has ${subtasks.length} subtasks. Adding ${numSubtasks} more.`)); + } + + // Get the tasks.json content for context + let tasksData = {}; + try { + tasksData = readJSON(path.join(process.cwd(), 'tasks', 'tasks.json')); + } catch (error) { + console.log(chalk.yellow('Could not read tasks.json for context. Proceeding without it.')); + } + + // Get the PRD content for context if available + let prdContent = ''; + if (tasksData.meta && tasksData.meta.source) { + try { + prdContent = fs.readFileSync(path.join(process.cwd(), tasksData.meta.source), 'utf8'); + console.log(chalk.green(`Successfully loaded PRD from ${tasksData.meta.source} (${prdContent.length} characters)`)); + } catch (error) { + console.log(chalk.yellow(`Could not read PRD at ${tasksData.meta.source}. Proceeding without it.`)); + } + } + + // Get the specific task file for more detailed context if available + let taskFileContent = ''; + try { + const taskFileName = `task_${String(task.id).padStart(3, '0')}.txt`; + const taskFilePath = path.join(process.cwd(), 'tasks', taskFileName); + if (fs.existsSync(taskFilePath)) { + taskFileContent = fs.readFileSync(taskFilePath, 'utf8'); + console.log(chalk.green(`Successfully loaded task file ${taskFileName} for additional context`)); + } + } catch (error) { + console.log(chalk.yellow(`Could not read task file for task ${task.id}. Proceeding without it.`)); + } + + // Get dependency task details for better context + let dependencyDetails = ''; + if (task.dependencies && task.dependencies.length > 0) { + dependencyDetails = 'Dependency Tasks:\n'; + for (const depId of task.dependencies) { + const depTask = tasksData.tasks.find(t => t.id === depId); + if (depTask) { + dependencyDetails += `Task ${depId}: ${depTask.title}\n`; + dependencyDetails += `Description: ${depTask.description}\n`; + if (depTask.details) { + dependencyDetails += `Details: ${depTask.details.substring(0, 200)}${depTask.details.length > 200 ? '...' : ''}\n`; + } + dependencyDetails += '\n'; + } + } + } + + // Extract project metadata for context + const projectContext = tasksData.meta ? + `Project: ${tasksData.meta.projectName || 'Unknown'} +Version: ${tasksData.meta.version || '1.0.0'} +Description: ${tasksData.meta.description || 'No description available'}` : ''; + + // Construct the prompt for Perplexity/Anthropic with enhanced context + const prompt = `I need to break down the following task into ${numSubtasks} detailed subtasks for a software development project. + +${projectContext} + +CURRENT TASK: +Task ID: ${task.id} +Task Title: ${title} +Task Description: ${description} +Priority: ${task.priority || 'medium'} +Additional Details: ${details} +${additionalContext ? `\nADDITIONAL CONTEXT PROVIDED BY USER:\n${additionalContext}` : ''} + +${taskFileContent ? `DETAILED TASK INFORMATION: +${taskFileContent}` : ''} + +${dependencyDetails ? dependencyDetails : ''} + +${subtasks.length > 0 ? `Existing Subtasks: +${subtasks.map(st => `- ${st.title}: ${st.description}`).join('\n')}` : ''} + +${prdContent ? `PRODUCT REQUIREMENTS DOCUMENT: +${prdContent}` : ''} + +${tasksData.tasks ? `PROJECT CONTEXT - OTHER RELATED TASKS: +${JSON.stringify( + tasksData.tasks + .filter(t => t.id !== task.id) + // Prioritize tasks that are dependencies or depend on this task + .sort((a, b) => { + const aIsRelated = task.dependencies?.includes(a.id) || a.dependencies?.includes(task.id); + const bIsRelated = task.dependencies?.includes(b.id) || b.dependencies?.includes(task.id); + return bIsRelated - aIsRelated; + }) + .slice(0, 5) // Limit to 5 most relevant tasks to avoid context overload + .map(t => ({ + id: t.id, + title: t.title, + description: t.description, + status: t.status, + dependencies: t.dependencies + })), + null, 2 +)}` : ''} + +Please generate ${numSubtasks} subtasks that are: +1. Specific and actionable +2. Relevant to the current technology stack and project requirements +3. Properly sequenced with clear dependencies +4. Detailed enough to be implemented without further clarification + +For each subtask, provide: +1. A clear, concise title +2. A detailed description explaining what needs to be done +3. Dependencies (if any) - list the IDs of tasks this subtask depends on +4. Acceptance criteria - specific conditions that must be met for the subtask to be considered complete + +Format each subtask as follows: + +Subtask 1: [Title] +Description: [Detailed description] +Dependencies: [List of task IDs, or "None" if no dependencies] +Acceptance Criteria: [List of criteria] + +Subtask 2: [Title] +... + +Research the task thoroughly and ensure the subtasks are comprehensive, specific, and actionable. Focus on technical implementation details rather than generic steps.`; + + // Start loading indicator + const loadingInterval = startLoadingIndicator('Researching and generating subtasks with AI'); + + try { + let responseText; + + try { + // Try to use Perplexity first + console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation...')); + const result = await perplexity.chat.completions.create({ + model: PERPLEXITY_MODEL, + messages: [{ + role: "user", + content: prompt + }], + temperature: TEMPERATURE, + max_tokens: MAX_TOKENS, + }); + + // Extract the response text + responseText = result.choices[0].message.content; + console.log(chalk.green('Successfully generated subtasks with Perplexity AI')); + } catch (perplexityError) { + console.log(chalk.yellow('Falling back to Anthropic for subtask generation...')); + console.log(chalk.gray('Perplexity error:'), perplexityError.message); + + // Use Anthropic as fallback + const stream = await anthropic.messages.create({ + model: MODEL, + max_tokens: MAX_TOKENS, + temperature: TEMPERATURE, + system: "You are an expert software developer and project manager. Your task is to break down software development tasks into detailed subtasks that are specific, actionable, and technically relevant.", + messages: [ + { + role: "user", + content: prompt + } + ], + stream: true + }); + + // Process the stream + responseText = ''; + for await (const chunk of stream) { + if (chunk.type === 'content_block_delta' && chunk.delta.text) { + responseText += chunk.delta.text; + } + } + + console.log(chalk.green('Successfully generated subtasks with Anthropic AI')); + } + + // Stop loading indicator + stopLoadingIndicator(loadingInterval); + + if (CONFIG.debug) { + console.log(chalk.gray('AI Response:')); + console.log(chalk.gray(responseText)); + } + + // Parse the subtasks from the response text + const subtasks = parseSubtasksFromText(responseText, nextSubtaskId, numSubtasks); + return subtasks; + } catch (error) { + stopLoadingIndicator(loadingInterval); + console.error(chalk.red('Error generating subtasks:'), error); + throw error; + } +} + // ------------------------------------------ // Main CLI // ------------------------------------------ -(async function main() { - const args = process.argv.slice(2); - const command = args[0]; +async function main() { + program + .name('dev') + .description('AI-driven development task management') + .version('1.3.1'); - const outputDir = path.resolve(process.cwd(), 'tasks'); - // Update tasksPath to be inside the tasks directory - const tasksPath = path.resolve(outputDir, 'tasks.json'); + program + .command('parse-prd') + .description('Parse a PRD file and generate tasks') + .argument('', 'Path to the PRD file') + .option('-o, --output ', 'Output file path', 'tasks/tasks.json') + .option('-n, --num-tasks ', 'Number of tasks to generate', '10') + .action(async (file, options) => { + const numTasks = parseInt(options.numTasks, 10); + const outputPath = options.output; + + console.log(chalk.blue(`Parsing PRD file: ${file}`)); + console.log(chalk.blue(`Generating ${numTasks} tasks...`)); + + await parsePRD(file, outputPath, numTasks); + }); - const inputArg = (args.find(a => a.startsWith('--input=')) || '').split('=')[1] || 'sample-prd.txt'; - const fromArg = (args.find(a => a.startsWith('--from=')) || '').split('=')[1]; - const promptArg = (args.find(a => a.startsWith('--prompt=')) || '').split('=')[1] || ''; - const idArg = (args.find(a => a.startsWith('--id=')) || '').split('=')[1]; - const statusArg = (args.find(a => a.startsWith('--status=')) || '').split('=')[1] || ''; - const tasksCountArg = (args.find(a => a.startsWith('--tasks=')) || '').split('=')[1]; - const numTasks = tasksCountArg ? parseInt(tasksCountArg, 10) : undefined; - const subtasksArg = (args.find(a => a.startsWith('--subtasks=')) || '').split('=')[1]; - const numSubtasks = subtasksArg ? parseInt(subtasksArg, 10) : 3; // Default to 3 subtasks if not specified - const forceFlag = args.includes('--force'); // Check if --force flag is present - - log('info', `Executing command: ${command}`); - - // Make sure the tasks directory exists - if (!fs.existsSync(outputDir)) { - log('info', `Creating tasks directory: ${outputDir}`); - fs.mkdirSync(outputDir, { recursive: true }); - } - - switch (command) { - case 'parse-prd': - log('info', `Parsing PRD from ${inputArg} to generate tasks.json...`); - if (numTasks) { - log('info', `Limiting to ${numTasks} tasks as specified`); - } - await parsePRD(inputArg, tasksPath, numTasks); - break; - - case 'update': - if (!fromArg) { - log('error', "Please specify --from=. e.g. node dev.js update --from=3 --prompt='Changes...'"); + program + .command('update') + .description('Update tasks based on new information or implementation changes') + .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option('--from ', 'Task ID to start updating from (tasks with ID >= this value will be updated)', '1') + .option('-p, --prompt ', 'Prompt explaining the changes or new context (required)') + .action(async (options) => { + const tasksPath = options.file; + const fromId = parseInt(options.from, 10); + const prompt = options.prompt; + + if (!prompt) { + console.error(chalk.red('Error: --prompt parameter is required. Please provide information about the changes.')); process.exit(1); } - log('info', `Updating tasks from ID ${fromArg} based on prompt...`); - await updateTasks(tasksPath, parseInt(fromArg, 10), promptArg); - break; + + console.log(chalk.blue(`Updating tasks from ID >= ${fromId} with prompt: "${prompt}"`)); + console.log(chalk.blue(`Tasks file: ${tasksPath}`)); + + await updateTasks(tasksPath, fromId, prompt); + }); - case 'generate': - log('info', `Generating individual task files from ${tasksPath} to ${outputDir}...`); - generateTaskFiles(tasksPath, outputDir); - break; + program + .command('generate') + .description('Generate task files from tasks.json') + .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option('-o, --output ', 'Output directory', 'tasks') + .action(async (options) => { + const tasksPath = options.file; + const outputDir = options.output; + + console.log(chalk.blue(`Generating task files from: ${tasksPath}`)); + console.log(chalk.blue(`Output directory: ${outputDir}`)); + + await generateTaskFiles(tasksPath, outputDir); + }); - case 'set-status': - if (!idArg) { - log('error', "Missing --id= argument."); + program + .command('set-status') + .description('Set the status of a task') + .option('-i, --id ', 'Task ID (can be comma-separated for multiple tasks)') + .option('-s, --status ', 'New status (todo, in-progress, review, done)') + .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .action(async (options) => { + const tasksPath = options.file; + const taskId = options.id; + const status = options.status; + + if (!taskId || !status) { + console.error(chalk.red('Error: Both --id and --status are required')); process.exit(1); } - if (!statusArg) { - log('error', "Missing --status= argument (e.g., done, pending, deferred, in-progress)."); - process.exit(1); + + console.log(chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`)); + + await setTaskStatus(tasksPath, taskId, status); + }); + + program + .command('list') + .description('List all tasks') + .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option('-s, --status ', 'Filter by status') + .option('--with-subtasks', 'Show subtasks for each task') + .action(async (options) => { + const tasksPath = options.file; + const statusFilter = options.status; + const withSubtasks = options.withSubtasks || false; + + console.log(chalk.blue(`Listing tasks from: ${tasksPath}`)); + if (statusFilter) { + console.log(chalk.blue(`Filtering by status: ${statusFilter}`)); } - log('info', `Setting task(s) ${idArg} status to "${statusArg}"...`); - setTaskStatus(tasksPath, idArg, statusArg); - break; + if (withSubtasks) { + console.log(chalk.blue('Including subtasks in listing')); + } + + await listTasks(tasksPath, statusFilter, withSubtasks); + }); - case 'list': - log('info', `Listing tasks from ${tasksPath}...`); - listTasks(tasksPath); - break; - - case 'expand': - if (args.includes('--all')) { - // Expand all tasks - log('info', `Expanding all tasks with ${numSubtasks} subtasks each...`); - await expandAllTasks(tasksPath, numSubtasks, promptArg, forceFlag); + program + .command('expand') + .description('Expand tasks with subtasks') + .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') + .option('-i, --id ', 'Task ID to expand') + .option('-a, --all', 'Expand all tasks') + .option('-n, --num ', 'Number of subtasks to generate', CONFIG.defaultSubtasks.toString()) + .option('-r, --no-research', 'Disable Perplexity AI for research-backed subtask generation') + .option('-p, --prompt ', 'Additional context to guide subtask generation') + .option('--force', 'Force regeneration of subtasks for tasks that already have them') + .action(async (options) => { + const tasksPath = options.file; + const idArg = options.id ? parseInt(options.id, 10) : null; + const allFlag = options.all; + const numSubtasks = parseInt(options.num, 10); + const forceFlag = options.force; + const useResearch = options.research !== false; // Default to true unless explicitly disabled + const additionalContext = options.prompt || ''; + + if (allFlag) { + console.log(chalk.blue(`Expanding all tasks with ${numSubtasks} subtasks each...`)); + if (useResearch) { + console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation')); + } else { + console.log(chalk.yellow('Research-backed subtask generation disabled')); + } + if (additionalContext) { + console.log(chalk.blue(`Additional context: "${additionalContext}"`)); + } + await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag); } else if (idArg) { - // Expand a specific task - log('info', `Expanding task ${idArg} with ${numSubtasks} subtasks...`); - await expandTask(tasksPath, parseInt(idArg, 10), numSubtasks, promptArg); + console.log(chalk.blue(`Expanding task ${idArg} with ${numSubtasks} subtasks...`)); + if (useResearch) { + console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation')); + } else { + console.log(chalk.yellow('Research-backed subtask generation disabled')); + } + if (additionalContext) { + console.log(chalk.blue(`Additional context: "${additionalContext}"`)); + } + await expandTask(idArg, numSubtasks, useResearch, additionalContext); } else { - log('error', "Error: Please specify a task ID with --id= or use --all to expand all tasks."); - process.exit(1); + console.error(chalk.red('Error: Please specify a task ID with --id= or use --all to expand all tasks.')); } - break; + }); - default: - log('info', ` -Dev.js - Task Management Script + await program.parseAsync(process.argv); +} -Subcommands: - 1) parse-prd --input=some-prd.txt [--tasks=10] - -> Creates/overwrites tasks.json with a set of tasks. - -> Optional --tasks parameter limits the number of tasks generated. - - 2) update --from=5 --prompt="We changed from Slack to Discord." - -> Regenerates tasks from ID >= 5 using the provided prompt. - - 3) generate - -> Generates per-task files (e.g., task_001.txt) from tasks.json - - 4) set-status --id=4 --status=done - -> Updates a single task's status to done (or pending, deferred, in-progress, etc.). - -> Supports comma-separated IDs for updating multiple tasks: --id=1,2,3,1.1,1.2 - - 5) list - -> Lists tasks in a brief console view (ID, title, status). - - 6) expand --id=3 --subtasks=5 [--prompt="Additional context"] - -> Expands a task with subtasks for more detailed implementation. - -> Use --all instead of --id to expand all tasks. - -> Optional --subtasks parameter controls number of subtasks (default: 3). - -> Add --force when using --all to regenerate subtasks for tasks that already have them. - -> Note: Tasks marked as 'done' or 'completed' are always skipped. - -Usage examples: - node dev.js parse-prd --input=scripts/prd.txt - node dev.js parse-prd --input=scripts/prd.txt --tasks=10 - node dev.js update --from=4 --prompt="Refactor tasks from ID 4 onward" - node dev.js generate - node dev.js set-status --id=3 --status=done - node dev.js list - node dev.js expand --id=3 --subtasks=5 - node dev.js expand --all - node dev.js expand --all --force - `); - break; - } -})().catch(err => { +main().catch(err => { log('error', err); process.exit(1); }); \ No newline at end of file diff --git a/templates/env.example b/templates/env.example index 2bae4f48..0dcb9549 100644 --- a/templates/env.example +++ b/templates/env.example @@ -1,5 +1,6 @@ # Required ANTHROPIC_API_KEY=your-api-key-here # Format: sk-ant-api03-... +PERPLEXITY_API_KEY=pplx-abcde # Optional - defaults shown MODEL=claude-3-7-sonnet-20250219 # Recommended models: claude-3-7-sonnet-20250219, claude-3-opus-20240229 @@ -10,4 +11,4 @@ LOG_LEVEL=info # Log level (debug, info, warn, error) DEFAULT_SUBTASKS=3 # Default number of subtasks when expanding DEFAULT_PRIORITY=medium # Default priority for generated tasks (high, medium, low) PROJECT_NAME={{projectName}} # Project name for tasks.json metadata -PROJECT_VERSION={{projectVersion}} # Project version for tasks.json metadata \ No newline at end of file +PROJECT_VERSION={{projectVersion}} # Project version for tasks.json metadata \ No newline at end of file