UNPKG

conductor-tasks

Version:

Task Manager for AI Development

337 lines 23.5 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import * as fs from 'fs'; import * as path from 'path'; import dotenv from 'dotenv'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import logger from "./core/logger.js"; import { ContextManager } from "./core/contextManager.js"; import { LLMManager } from "./llm/llmManager.js"; import { TaskManager } from "./task/taskManager.js"; import { TaskPriority, TaskStatus } from "./core/types.js"; import { IDERulesManager } from "./ide/ideRulesManager.js"; import { CreateTaskSchema, createTaskHandler } from "./commands/createTaskHandler.js"; import { UpdateTaskSchema, updateTaskHandler } from "./commands/updateTaskHandler.js"; import { ListTasksSchema, listTasksHandler } from "./commands/listTasksHandler.js"; import { GetTaskSchema, getTaskHandler } from "./commands/getTaskHandler.js"; import { AddTaskNoteSchema, addTaskNoteHandler } from "./commands/addTaskNoteHandler.js"; import { GetNextTaskSchema, getNextTaskHandler } from "./commands/getNextTaskHandler.js"; import { ParsePrdSchema, parsePrdHandler } from "./commands/parsePrdHandler.js"; import { DeleteTaskSchema, deleteTaskHandler } from "./commands/deleteTaskHandler.js"; import { InitializeProjectSchema, initializeProjectHandler } from "./commands/initializeProjectHandler.js"; import { GenerateImplementationStepsSchema, generateImplementationStepsHandler } from "./commands/generateImplementationStepsHandler.js"; import { ExpandTaskSchema, expandTaskHandler } from "./commands/expandTaskHandler.js"; import { SuggestTaskImprovementsSchema, suggestTaskImprovementsHandler } from "./commands/suggestTaskImprovementsHandler.js"; import { HelpImplementTaskSchema, helpImplementTaskHandler } from "./commands/helpImplementTaskHandler.js"; import { VisualizeTasksKanbanSchema, visualizeTasksKanbanHandler } from "./commands/visualizeTasksKanbanHandler.js"; import { VisualizeTasksDependencyTreeSchema, visualizeTasksDependencyTreeHandler } from "./commands/visualizeTasksDependencyTreeHandler.js"; import { VisualizeTasksDashboardSchema, visualizeTasksDashboardHandler } from "./commands/visualizeTasksDashboardHandler.js"; import { ParsePrdFileSchema, parsePrdFileHandler } from "./commands/parsePrdFileHandler.js"; import packageInfo from '../package.json' with { type: 'json' }; dotenv.config(); const llmManager = new LLMManager(); const contextManager = new ContextManager(); const taskManager = new TaskManager(llmManager, contextManager); const createTaskSchemaObject = z.object(CreateTaskSchema); const updateTaskSchemaObject = z.object(UpdateTaskSchema); const listTasksSchemaObject = z.object(ListTasksSchema); const getTaskSchemaObject = z.object(GetTaskSchema); const addTaskNoteSchemaObject = z.object(AddTaskNoteSchema); const getNextTaskSchemaObject = z.object(GetNextTaskSchema); const parsePrdSchemaObject = z.object(ParsePrdSchema); const deleteTaskSchemaObject = z.object(DeleteTaskSchema); const initializeProjectSchemaObject = z.object(InitializeProjectSchema); const generateImplementationStepsSchemaObject = z.object(GenerateImplementationStepsSchema); const expandTaskSchemaObject = z.object(ExpandTaskSchema); const suggestTaskImprovementsSchemaObject = z.object(SuggestTaskImprovementsSchema); const helpImplementTaskSchemaObject = z.object(HelpImplementTaskSchema); const visualizeTasksKanbanSchemaObject = z.object(VisualizeTasksKanbanSchema); const visualizeTasksDependencyTreeSchemaObject = z.object(VisualizeTasksDependencyTreeSchema); const visualizeTasksDashboardSchemaObject = z.object(VisualizeTasksDashboardSchema); const parsePrdFileSchemaObject = z.object(ParsePrdFileSchema); function setupTaskManager(filePath) { const workspaceRoot = taskManager.getWorkspaceRoot() || process.cwd(); const effectivePath = filePath ? path.resolve(workspaceRoot, filePath) : path.resolve(workspaceRoot, taskManager['config']?.tasksFileName || 'TASKS.md'); const projectName = path.basename(path.dirname(effectivePath)); const dir = path.dirname(effectivePath); if (!fs.existsSync(dir)) { logger.info(`Creating directory for tasks file: ${dir}`); fs.mkdirSync(dir, { recursive: true }); } taskManager.setTasksFilePath(effectivePath); logger.info(`TaskManager initialized with tasks file: ${taskManager.getTasksFilePath()}`); return taskManager; } function formatOutput(data) { if (typeof data === 'string') { return data; } return JSON.stringify(data, null, 2); } async function runCliCommand(handler, args, schema) { const tm = setupTaskManager(args.file); const handlerArgs = { ...args }; delete handlerArgs.$0; delete handlerArgs._; delete handlerArgs.file; delete handlerArgs.serveMcp; try { const validatedParams = schema ? schema.parse(handlerArgs) : handlerArgs; const result = await handler(tm, validatedParams); console.log(formatOutput(result)); } catch (error) { logger.error(`CLI command failed: ${error.message}`, error); console.error(`Error: ${error.message}`); process.exit(1); } } async function runInitializeCliCommand(args) { const tm = taskManager; const handlerArgs = { ...args }; delete handlerArgs.$0; delete handlerArgs._; delete handlerArgs.file; delete handlerArgs.serveMcp; try { const filePath = args.file || path.join(process.cwd(), 'TASKS.md'); const dir = path.dirname(filePath); const projectName = args.projectName || path.basename(dir); const projectDescription = args.projectDescription || `Project located at ${dir}`; const params = { filePath: filePath, projectName: projectName, projectDescription: projectDescription }; const validatedParams = initializeProjectSchemaObject.parse(params); const result = await initializeProjectHandler(tm, validatedParams); console.log(formatOutput(result)); } catch (error) { logger.error(`CLI command 'init-project' failed: ${error.message}`, error); console.error(`Error: ${error.message}`); process.exit(1); } } async function startMcpServer() { process.env.MCP_MODE = "true"; if (!process.env.IDE) { process.env.IDE = "cursor"; logger.info(`No IDE environment variable found, setting default: ${process.env.IDE}`); } else { logger.info(`Using IDE environment variable: ${process.env.IDE}`); } logger.info(` ======================================================== Conductor Task Management System Starting (MCP Mode) ======================================================== Current directory: ${process.cwd()} Default Tasks file path: ${path.join(process.cwd(), 'TASKS.md')} IDE Type: ${process.env.IDE} ======================================================== `); const tm = setupTaskManager(undefined); if (fs.existsSync(tm.getTasksFilePath())) { logger.info(`TASKS.md exists at ${tm.getTasksFilePath()}`); logger.info(`Task count: ${tm.getTaskCount()}`); logger.info(`TaskManager initialized: ${tm.isInitialized()}`); } else { logger.warn(`TASKS.md not found at ${tm.getTasksFilePath()}. Use 'initialize-project' tool.`); } try { const ideRulesManager = IDERulesManager.getInstance(); ideRulesManager.setIDEType(process.env.IDE.toLowerCase()); await ideRulesManager.loadRules(); const currentIdeType = ideRulesManager.getIDEType(); logger.info(`Using IDE type: ${currentIdeType}`); const ideRules = await ideRulesManager.getRules(); if (ideRules) { logger.info(`Loaded ${ideRules.rules.length} IDE-specific rules`); } else { logger.warn('No IDE-specific rules loaded'); } contextManager.setIDERulesManager(ideRulesManager); } catch (error) { logger.error('Failed to initialize IDERulesManager', { error }); } logger.info('=== Conductor Task Management System Started (MCP Mode) ==='); logger.info(`Default LLM Provider: ${llmManager.getDefaultProvider()}`); logger.info(`Available Providers: ${llmManager.getAvailableProviders().join(', ')}`); logger.info(`Tasks File Path Used by Server: ${tm.getTasksFilePath()}`); logger.info('=================================='); const server = new McpServer({ name: "conductor", version: packageInfo.version, }); server.tool("create-task", "Create a new task with details", CreateTaskSchema, async (params) => { return createTaskHandler(tm, params); }); server.tool("update-task", "Update an existing task's details", UpdateTaskSchema, async (params) => updateTaskHandler(tm, params)); server.tool("list-tasks", "Get a list of tasks with filtering and sorting options", ListTasksSchema, async (params) => listTasksHandler(tm, params)); server.tool("get-task", "Get details of a specific task", GetTaskSchema, async (params) => getTaskHandler(tm, params)); server.tool("add-task-note", "Add a note, progress update, or comment to a task", AddTaskNoteSchema, async (params) => addTaskNoteHandler(tm, params)); server.tool("get-next-task", "Get the next task to work on", GetNextTaskSchema, async (params) => getNextTaskHandler(tm, params)); server.tool("parse-prd", "Parse a PRD (Product Requirements Document) and create tasks from it", ParsePrdSchema, async (params) => parsePrdHandler(tm, params)); server.tool("delete-task", "Delete a task", DeleteTaskSchema, async (params) => deleteTaskHandler(tm, params)); server.tool("initialize-project", "Initialize the project, including task management (TASKS.md) and IDE rules.", InitializeProjectSchema, async (params) => initializeProjectHandler(tm, params)); server.tool("generate-implementation-steps", "Generate detailed implementation steps for a task", GenerateImplementationStepsSchema, async (params) => generateImplementationStepsHandler(tm, params)); server.tool("expand-task", "Expand a task with more detailed information and subtasks", ExpandTaskSchema, async (params) => expandTaskHandler(tm, params)); server.tool("suggest-task-improvements", "Get AI suggestions for improving a task", SuggestTaskImprovementsSchema, async (params) => suggestTaskImprovementsHandler(tm, params)); server.tool("help-implement-task", "Get AI assistance to implement a specific task", HelpImplementTaskSchema, async (params) => helpImplementTaskHandler(tm, llmManager, contextManager, params)); server.tool("visualize-tasks-kanban", "Display tasks in a Kanban board view", VisualizeTasksKanbanSchema, async (params) => visualizeTasksKanbanHandler(tm, params)); server.tool("visualize-tasks-dependency-tree", "Display task dependency tree", VisualizeTasksDependencyTreeSchema, async (params) => visualizeTasksDependencyTreeHandler(tm, params)); server.tool("visualize-tasks-dashboard", "Display task dashboard with summary statistics", VisualizeTasksDashboardSchema, async (params) => visualizeTasksDashboardHandler(tm, params)); server.tool("parse-prd-file", "Parse a PRD file from disk and extract tasks", ParsePrdFileSchema, async (params) => parsePrdFileHandler(tm, params)); server.tool("list-task-templates", "List available task templates", {}, async (params) => (await import('./commands/listTaskTemplatesHandler.js')).listTaskTemplatesHandler(tm)); server.tool("get-task-template", "Get details of a specific task template", (await import('./commands/getTaskTemplateHandler.js')).GetTaskTemplateSchema, async (params) => (await import('./commands/getTaskTemplateHandler.js')).getTaskTemplateHandler(tm, params)); server.tool("create-task-from-template", "Create a new task from a template", (await import('./commands/createTaskFromTemplateHandler.js')).CreateTaskFromTemplateSchema, async (params) => (await import('./commands/createTaskFromTemplateHandler.js')).createTaskFromTemplateHandler(tm, params)); server.tool("research-topic", "Research a topic using available LLM capabilities (e.g., Perplexity, tool-calling for search).", (await import('./commands/researchTopicHandler.js')).ResearchTopicSchema, async (params) => (await import('./commands/researchTopicHandler.js')).researchTopicHandler(tm, llmManager, params)); server.tool("generate-diff", "Generate a diff for a file based on a change description.", (await import('./commands/generateDiffHandler.js')).GenerateDiffSchema, async (params) => (await import('./commands/generateDiffHandler.js')).generateDiffHandler(tm, params)); server.tool("propose-diff", "Propose a diff to be applied to a file. Currently acknowledges only; does not apply.", (await import('./commands/proposeDiffHandler.js')).ProposeDiffSchema, async (params) => (await import('./commands/proposeDiffHandler.js')).proposeDiffHandler(tm, params)); function isLLMAvailable() { try { return llmManager.getAvailableProviders().length > 0; } catch (error) { logger.error('Failed to check LLM provider availability', { error }); return false; } } if (!isLLMAvailable()) { logger.warn("No LLM providers available. AI-related features (enhancement, parsing, generation) might be limited or unavailable."); logger.warn("To enable LLM features, set at least one API key (e.g., ANTHROPIC_API_KEY, OPENAI_API_KEY) in your environment variables or .env file."); } logger.info('Starting Conductor MCP Server connection...'); const transport = new StdioServerTransport(); await server.connect(transport); logger.info('Conductor MCP Server connected and listening...'); } async function main() { const cliArgs = yargs(hideBin(process.argv)) .scriptName("conductor-tasks") .option('file', { alias: 'f', type: 'string', description: 'Path to the TASKS.md file', global: true }) .option('serve-mcp', { type: 'boolean', description: 'Run in MCP server mode (for IDE integration)', default: false }) .command('init-project', 'Initialize the project, including TASKS.md and IDE rules.', (y) => y .option('projectName', { type: 'string', desc: 'Name of the project (inferred from directory if not provided)' }) .option('projectDescription', { type: 'string', desc: 'Description of the project' }) .option('filePath', { type: 'string', alias: 'f', desc: 'Path to the TASKS.md file (overrides global -f if used here)' }), (argv) => runInitializeCliCommand(argv)) .command('create', 'Create a new task', (y) => y .option('title', { type: 'string', demandOption: true, desc: 'Title of the task' }) .option('description', { type: 'string', desc: 'Detailed description' }) .option('priority', { choices: Object.values(TaskPriority), desc: 'Priority level' }) .option('status', { choices: Object.values(TaskStatus), desc: 'Initial status' }) .option('assignee', { type: 'string', desc: 'Assignee username or email' }) .option('tags', { type: 'array', string: true, desc: 'List of tags' }) .option('dueDate', { type: 'string', desc: 'Due date (e.g., YYYY-MM-DD)' }) .option('complexity', { type: 'number', desc: 'Complexity score (1-10)' }) .option('dependencies', { type: 'array', string: true, desc: 'List of task IDs this task depends on' }), (argv) => runCliCommand(createTaskHandler, argv, createTaskSchemaObject)) .command('update <id>', 'Update an existing task', (y) => y .positional('id', { type: 'string', demandOption: true, desc: 'ID of the task to update' }) .option('title', { type: 'string', desc: 'New title' }) .option('description', { type: 'string', desc: 'New description' }) .option('priority', { choices: Object.values(TaskPriority), desc: 'New priority' }) .option('status', { choices: Object.values(TaskStatus), desc: 'New status' }) .option('assignee', { type: 'string', desc: 'New assignee' }) .option('tags', { type: 'array', string: true, desc: 'Replace tags' }) .option('dueDate', { type: 'string', desc: 'New due date' }) .option('complexity', { type: 'number', desc: 'New complexity score' }) .option('dependencies', { type: 'array', string: true, desc: 'Replace dependencies' }), (argv) => runCliCommand(updateTaskHandler, argv, updateTaskSchemaObject)) .command('list', 'List tasks', (y) => y .option('status', { choices: Object.values(TaskStatus), desc: 'Filter by status' }) .option('priority', { choices: Object.values(TaskPriority), desc: 'Filter by priority' }) .option('tags', { type: 'array', string: true, desc: 'Filter by tags (any match)' }) .option('sortBy', { choices: ['priority', 'dueDate', 'createdAt', 'updatedAt', 'complexity'], desc: 'Field to sort by' }) .option('sortDirection', { choices: ['asc', 'desc'], default: 'asc', desc: 'Sort direction' }), (argv) => runCliCommand(listTasksHandler, argv, listTasksSchemaObject)) .command('get <id>', 'Get details of a specific task', (y) => y.positional('id', { type: 'string', demandOption: true, desc: 'ID of the task' }), (argv) => runCliCommand(getTaskHandler, argv, getTaskSchemaObject)) .command('add-note <taskId>', 'Add a note to a task', (y) => y .positional('taskId', { type: 'string', demandOption: true, desc: 'ID of the task' }) .option('content', { type: 'string', demandOption: true, desc: 'Content of the note' }) .option('author', { type: 'string', demandOption: true, desc: 'Author of the note' }) .option('type', { choices: ['progress', 'comment', 'blocker', 'solution'], default: 'comment', desc: 'Type of note' }), (argv) => runCliCommand(addTaskNoteHandler, argv, addTaskNoteSchemaObject)) .command('next', 'Get the next task to work on (simple logic)', (y) => y.option('random_string', { type: 'string', default: 'dummy', hidden: true }), (argv) => runCliCommand(getNextTaskHandler, argv, getNextTaskSchemaObject)) .command('parse-prd', 'Parse PRD content from stdin or string argument to create tasks', (y) => y .option('prdContent', { type: 'string', demandOption: true, desc: 'The PRD content as a string' }) .option('createTasksFile', { type: 'boolean', default: true, desc: 'Create/update the TASKS.md file' }), (argv) => runCliCommand(parsePrdHandler, argv, parsePrdSchemaObject)) .command('parse-prd-file <filePath>', 'Parse a PRD file from disk', (y) => y .positional('filePath', { type: 'string', demandOption: true, desc: 'Path to the PRD file' }) .option('createTasksFile', { type: 'boolean', default: true, desc: 'Create/update the TASKS.md file' }) .option('verbose', { type: 'boolean', default: false, desc: 'Show detailed output' }), (argv) => runCliCommand(parsePrdFileHandler, argv, parsePrdFileSchemaObject)) .command('delete <id>', 'Delete a task', (y) => y.positional('id', { type: 'string', demandOption: true, desc: 'ID of the task to delete' }), (argv) => runCliCommand(deleteTaskHandler, argv, deleteTaskSchemaObject)) .command('generate-steps <taskId>', 'Generate implementation steps for a task', (y) => y.positional('taskId', { type: 'string', demandOption: true, desc: 'ID of the task' }), (argv) => runCliCommand(generateImplementationStepsHandler, argv, generateImplementationStepsSchemaObject)) .command('expand <taskId>', 'Expand a task with more details/subtasks', (y) => y .positional('taskId', { type: 'string', demandOption: true, desc: 'ID of the task' }) .option('expansionPrompt', { type: 'string', desc: 'Additional context for expansion' }), (argv) => runCliCommand(expandTaskHandler, argv, expandTaskSchemaObject)) .command('suggest-improvements <taskId>', 'Suggest improvements for a task', (y) => y.positional('taskId', { type: 'string', demandOption: true, desc: 'ID of the task' }), (argv) => runCliCommand(suggestTaskImprovementsHandler, argv, suggestTaskImprovementsSchemaObject)) .command('help-implement <taskId>', 'Get AI assistance to implement a task', (y) => y .positional('taskId', { type: 'string', demandOption: true, desc: 'ID of the task' }) .option('additionalContext', { type: 'string', desc: 'Additional context for implementation' }), async (argv) => { const tm = setupTaskManager(argv.file); const handlerArgs = { ...argv }; delete handlerArgs.$0; delete handlerArgs._; delete handlerArgs.file; delete handlerArgs.serveMcp; try { const validatedParams = helpImplementTaskSchemaObject.parse(handlerArgs); const result = await helpImplementTaskHandler(tm, llmManager, contextManager, validatedParams); console.log(formatOutput(result)); } catch (error) { logger.error(`CLI command 'help-implement' failed: ${error.message}`, error); console.error(`Error: ${error.message}`); process.exit(1); } }) .command('kanban', 'Display tasks in a Kanban board view', (y) => y .option('priority', { choices: ['critical', 'high', 'medium', 'low', 'backlog'], desc: 'Filter by priority' }) .option('tag', { type: 'string', desc: 'Filter by tag' }) .option('compact', { type: 'boolean', default: false, desc: 'Use compact display' }) .option('showPriority', { type: 'boolean', default: true, desc: 'Show priority indicators' }) .option('showComplexity', { type: 'boolean', default: true, desc: 'Show complexity indicators' }), (argv) => runCliCommand(visualizeTasksKanbanHandler, argv, visualizeTasksKanbanSchemaObject)) .command('tree [taskId]', 'Display task dependency tree', (y) => y.positional('taskId', { type: 'string', desc: 'ID of the task to focus on (optional)' }), (argv) => runCliCommand(visualizeTasksDependencyTreeHandler, argv, visualizeTasksDependencyTreeSchemaObject)) .command('dashboard', 'Display task dashboard', (y) => y.option('random_string', { type: 'string', default: 'dummy', hidden: true }), (argv) => runCliCommand(visualizeTasksDashboardHandler, argv, visualizeTasksDashboardSchemaObject)) .demandCommand(0, 'Please specify a command or use --serve-mcp to start the server.') .strict() .help() .alias('help', 'h') .alias('version', 'v'); const argv = await cliArgs.argv; if (argv.serveMcp || argv._.length === 0) { if (argv._.length > 0 && !argv.serveMcp) { console.error("Error: Unknown command provided. Use --help to see available commands."); cliArgs.showHelp(); process.exit(1); } if (argv.serveMcp && argv._.length > 0) { logger.warn(`Ignoring commands (${argv._.join(', ')}) because --serve-mcp flag is set.`); } await startMcpServer(); } else { logger.debug('CLI command executed.'); } } main().catch(err => { logger.error('Unhandled error in main execution:', err); console.error('An unexpected error occurred:', err); process.exit(1); }); //# sourceMappingURL=index.js.map