taskqueue-mcp

import { Command } from "commander"; import chalk from "chalk"; import { TaskState, Task, Project } from "../types/data.js"; import { TaskManager } from "../server/TaskManager.js"; import { formatCliError } from "./errors.js"; import { formatProjectsList, formatTaskProgressTable } from "./taskFormattingUtils.js"; const program = new Command(); program .name("taskqueue") .description("CLI for the Task Manager MCP Server") .version("1.4.0") .option( '-f, --file-path <path>', 'Specify the path to the tasks JSON file. Overrides TASK_MANAGER_FILE_PATH env var.' ); let taskManager: TaskManager; program.hook('preAction', (thisCommand, actionCommand) => { const cliFilePath = program.opts().filePath; const envFilePath = process.env.TASK_MANAGER_FILE_PATH; const resolvedPath = cliFilePath || envFilePath || undefined; try { taskManager = new TaskManager(resolvedPath); } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } }); program .command("approve") .description("Approve a completed task") .argument("<projectId>", "Project ID") .argument("<taskId>", "Task ID") .option('-f, --force', 'Force approval even if task is not marked as done') .action(async (projectId, taskId, options) => { try { console.log(chalk.blue(`Attempting to approve task ${chalk.bold(taskId)} in project ${chalk.bold(projectId)}...`)); // First, verify the project and task exist and get their details let project: Project; let task: Task | undefined; try { project = await taskManager.readProject(projectId); task = project.tasks.find((t: Task) => t.id === taskId); if (!task) { console.error(chalk.red(`Task ${chalk.bold(taskId)} not found in project ${chalk.bold(projectId)}.`)); console.log(chalk.yellow('Available tasks in this project:')); project.tasks.forEach((t: Task) => { console.log(` - ${t.id}: ${t.title} (Status: ${t.status}, Approved: ${t.approved ? 'Yes' : 'No'})`); }); process.exit(1); } } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } // Pre-check task status if not using force if (task.status !== "done" && !options.force) { console.error(chalk.red(`Task ${chalk.bold(taskId)} is not marked as done yet. Current status: ${chalk.bold(task.status)}`)); console.log(chalk.yellow(`Use the --force flag to attempt approval anyway (may fail if underlying logic prevents it), or wait for the task to be marked as done.`)); process.exit(1); } if (task.approved) { console.log(chalk.yellow(`Task ${chalk.bold(taskId)} is already approved.`)); process.exit(0); } // Attempt to approve the task const approvedTask = await taskManager.approveTaskCompletion(projectId, taskId); console.log(chalk.green(`✅ Task ${chalk.bold(taskId)} in project ${chalk.bold(projectId)} has been approved.`)); // Fetch updated project data for display const updatedProject = await taskManager.readProject(projectId); const updatedTask = updatedProject.tasks.find((t: Task) => t.id === taskId); // Show task info if (updatedTask) { console.log(chalk.cyan('\n📋 Task details:')); console.log(` - ${chalk.bold('Title:')} ${updatedTask.title}`); console.log(` - ${chalk.bold('Description:')} ${updatedTask.description}`); console.log(` - ${chalk.bold('Status:')} ${updatedTask.status === 'done' ? chalk.green('Done ✓') : updatedTask.status === 'in progress' ? chalk.yellow('In Progress ⟳') : chalk.blue('Not Started ○')}`); console.log(` - ${chalk.bold('Completed details:')} ${updatedTask.completedDetails || chalk.gray("None")}`); console.log(` - ${chalk.bold('Approved:')} ${updatedTask.approved ? chalk.green('Yes ✓') : chalk.red('No ✗')}`); if (updatedTask.toolRecommendations) { console.log(` - ${chalk.bold('Tool Recommendations:')} ${updatedTask.toolRecommendations}`); } if (updatedTask.ruleRecommendations) { console.log(` - ${chalk.bold('Rule Recommendations:')} ${updatedTask.ruleRecommendations}`); } } // Show progress info const totalTasks = updatedProject.tasks.length; const completedTasks = updatedProject.tasks.filter((t: Task) => t.status === "done").length; const approvedTasks = updatedProject.tasks.filter((t: Task) => t.approved).length; console.log(chalk.cyan(`\n📊 Progress: ${chalk.bold(`${approvedTasks}/${completedTasks}/${totalTasks}`)} (approved/completed/total)`)); // Create a progress bar const bar = '▓'.repeat(approvedTasks) + '▒'.repeat(completedTasks - approvedTasks) + '░'.repeat(totalTasks - completedTasks); console.log(` ${bar}`); if (completedTasks === totalTasks && approvedTasks === totalTasks) { console.log(chalk.green('\n🎉 All tasks are completed and approved!')); console.log(chalk.blue(`The project can now be finalized using: taskqueue finalize ${projectId}`)); } else { if (totalTasks - completedTasks > 0) { console.log(chalk.yellow(`\n${totalTasks - completedTasks} tasks remaining to be completed.`)); } if (completedTasks - approvedTasks > 0) { console.log(chalk.yellow(`${completedTasks - approvedTasks} tasks remaining to be approved.`)); } } } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } }); program .command("finalize") .description("Mark a project as complete") .argument("<projectId>", "Project ID") .action(async (projectId) => { try { console.log(chalk.blue(`Attempting to finalize project ${chalk.bold(projectId)}...`)); // First, verify the project exists and get its details let project: Project; try { project = await taskManager.readProject(projectId); } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } // Pre-check project status if (project.completed) { console.log(chalk.yellow(`Project ${chalk.bold(projectId)} is already marked as completed.`)); process.exit(0); } // Pre-check task status (for better user feedback before attempting finalization) const allDone = project.tasks.every((t: Task) => t.status === "done"); if (!allDone) { console.error(chalk.red(`Not all tasks in project ${chalk.bold(projectId)} are marked as done.`)); console.log(chalk.yellow('\nPending tasks:')); project.tasks.filter((t: Task) => t.status !== "done").forEach((t: Task) => { console.log(` - ${chalk.bold(t.id)}: ${t.title} (Status: ${t.status})`); }); process.exit(1); } const allApproved = project.tasks.every((t: Task) => t.approved); if (!allApproved) { console.error(chalk.red(`Not all tasks in project ${chalk.bold(projectId)} are approved yet.`)); console.log(chalk.yellow('\nUnapproved tasks:')); project.tasks.filter((t: Task) => !t.approved).forEach((t: Task) => { console.log(` - ${chalk.bold(t.id)}: ${t.title}`); }); process.exit(1); } // Attempt to finalize the project await taskManager.approveProjectCompletion(projectId); console.log(chalk.green(`✅ Project ${chalk.bold(projectId)} has been approved and marked as complete.`)); // Fetch updated project data for display const updatedProject = await taskManager.readProject(projectId); // Show project info console.log(chalk.cyan('\n📋 Project details:')); console.log(` - ${chalk.bold('Initial Prompt:')} ${updatedProject.initialPrompt}`); if (updatedProject.projectPlan && updatedProject.projectPlan !== updatedProject.initialPrompt) { console.log(` - ${chalk.bold('Project Plan:')} ${updatedProject.projectPlan}`); } console.log(` - ${chalk.bold('Status:')} ${chalk.green('Completed ✓')}`); // Show progress info const totalTasks = updatedProject.tasks.length; const completedTasks = updatedProject.tasks.filter((t: Task) => t.status === "done").length; const approvedTasks = updatedProject.tasks.filter((t: Task) => t.approved).length; console.log(chalk.cyan(`\n📊 Final Progress: ${chalk.bold(`${approvedTasks}/${completedTasks}/${totalTasks}`)} (approved/completed/total)`)); // Create a progress bar const bar = '▓'.repeat(approvedTasks) + '▒'.repeat(completedTasks - approvedTasks) + '░'.repeat(totalTasks - completedTasks); console.log(` ${bar}`); console.log(chalk.green('\n🎉 Project successfully completed and approved!')); console.log(chalk.gray('You can view the project details anytime using:')); console.log(chalk.blue(` taskqueue list -p ${projectId}`)); } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } }); program .command("list") .description("List project summaries, or list tasks for a specific project") .option('-p, --project <projectId>', 'Show details and tasks for a specific project') .option('-s, --state <state>', "Filter by task/project state (open, pending_approval, completed, all)") .action(async (options) => { try { // Validate state option if provided const validStates = ['open', 'pending_approval', 'completed', 'all'] as const; const stateOption = options.state as TaskState | undefined | 'all'; if (stateOption && !validStates.includes(stateOption)) { console.error(chalk.red(`Invalid state value: ${options.state}`)); console.log(chalk.yellow(`Valid states are: ${validStates.join(', ')}`)); process.exit(1); } const filterState = (stateOption === 'all' || !stateOption) ? undefined : stateOption as TaskState; if (options.project) { // Show details for a specific project const projectId = options.project; try { const project = await taskManager.readProject(projectId); // Filter tasks based on state if provided const tasksToList = filterState ? project.tasks.filter((task: Task) => { if (filterState === 'open') return !task.approved; if (filterState === 'pending_approval') return task.status === 'done' && !task.approved; if (filterState === 'completed') return task.status === 'done' && task.approved; return true; // Should not happen }) : project.tasks; // Use the formatter for the progress table - it now includes the header const projectForTableDisplay = { ...project, tasks: tasksToList }; console.log(formatTaskProgressTable(projectForTableDisplay)); if (tasksToList.length === 0) { console.log(chalk.yellow(`\nNo tasks found${filterState ? ` matching state '${filterState}'` : ''} in project ${projectId}.`)); } else if (filterState) { console.log(chalk.dim(`(Filtered by state: ${filterState})`)); } } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } } else { // List all projects, potentially filtered const projects = await taskManager.listProjects(filterState); if (projects.projects.length === 0) { console.log(chalk.yellow(`No projects found${filterState ? ` matching state '${filterState}'` : ''}.`)); return; } // Use the formatter directly with the summary data console.log(chalk.cyan(formatProjectsList(projects.projects))); if (filterState) { console.log(chalk.dim(`(Filtered by state: ${filterState})`)); } } } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } }); program .command("generate-plan") .description("Generate a project plan using an LLM") .requiredOption("--prompt <text>", "Prompt text to feed to the LLM") .option("--model <model>", "LLM model to use", "gpt-4-turbo") .option("--provider <provider>", "LLM provider to use (openai, google, or deepseek)", "openai") .option("--attachment <file>", "File to attach as context (can be specified multiple times)", collect, []) .action(async (options) => { try { console.log(chalk.blue(`Generating project plan from prompt...`)); // Pass attachment filenames directly to the server const result = await taskManager.generateProjectPlan({ prompt: options.prompt, provider: options.provider, model: options.model, attachments: options.attachment }); // Display the results console.log(chalk.green(`✅ Project plan generated successfully!`)); console.log(chalk.cyan('\n📋 Project details:')); console.log(` - ${chalk.bold('Project ID:')} ${result.projectId}`); console.log(` - ${chalk.bold('Total Tasks:')} ${result.totalTasks}`); console.log(chalk.cyan('\n📝 Tasks:')); result.tasks.forEach((task) => { console.log(`\n ${chalk.bold(task.id)}:`); console.log(` Title: ${task.title}`); console.log(` Description: ${task.description}`); }); if (result.message) { console.log(`\n${result.message}`); } } catch (error) { console.error(chalk.red(formatCliError(error as Error))); process.exit(1); } }); // Helper function for collecting multiple values for the same option function collect(value: string, previous: string[]) { return previous.concat([value]); } // Export program for testing purposes export { program };