2025-04-21 17:48:30 -04:00
import path from 'path' ;
import chalk from 'chalk' ;
import boxen from 'boxen' ;
import Table from 'cli-table3' ;
2025-04-24 01:59:41 -04:00
import { z } from 'zod' ;
2025-04-21 17:48:30 -04:00
import {
displayBanner ,
getStatusWithColor ,
startLoadingIndicator ,
stopLoadingIndicator
} from '../ui.js' ;
2025-05-01 14:53:15 -04:00
import { readJSON , writeJSON , log as consoleLog , truncate } from '../utils.js' ;
2025-04-24 01:59:41 -04:00
import { generateObjectService } from '../ai-services-unified.js' ;
import { getDefaultPriority } from '../config-manager.js' ;
import generateTaskFiles from './generate-task-files.js' ;
// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z . object ( {
title : z . string ( ) . describe ( 'Clear, concise title for the task' ) ,
description : z
. string ( )
. describe ( 'A one or two sentence description of the task' ) ,
details : z
. string ( )
. describe ( 'In-depth implementation details, considerations, and guidance' ) ,
testStrategy : z
. string ( )
. describe ( 'Detailed approach for verifying task completion' )
} ) ;
2025-04-21 17:48:30 -04:00
/ * *
* Add a new task using AI
* @ param { string } tasksPath - Path to the tasks . json file
* @ param { string } prompt - Description of the task to add ( required for AI - driven creation )
* @ param { Array } dependencies - Task dependencies
* @ param { string } priority - Task priority
* @ param { function } reportProgress - Function to report progress to MCP server ( optional )
* @ param { Object } mcpLog - MCP logger object ( optional )
* @ param { Object } session - Session object from MCP server ( optional )
* @ param { string } outputFormat - Output format ( text or json )
2025-04-24 01:59:41 -04:00
* @ param { Object } customEnv - Custom environment variables ( optional ) - Note : AI params override deprecated
2025-04-21 17:48:30 -04:00
* @ param { Object } manualTaskData - Manual task data ( optional , for direct task creation without AI )
2025-04-24 01:59:41 -04:00
* @ param { boolean } useResearch - Whether to use the research model ( passed to unified service )
2025-05-01 14:53:15 -04:00
* @ param { Object } context - Context object containing session and potentially projectRoot
* @ param { string } [ context . projectRoot ] - Project root path ( for MCP / env fallback )
2025-04-21 17:48:30 -04:00
* @ returns { number } The new task ID
* /
async function addTask (
tasksPath ,
prompt ,
dependencies = [ ] ,
2025-05-01 14:53:15 -04:00
priority = null ,
context = { } ,
outputFormat = 'text' , // Default to text for CLI
2025-04-24 01:59:41 -04:00
manualTaskData = null ,
2025-05-01 14:53:15 -04:00
useResearch = false
2025-04-21 17:48:30 -04:00
) {
2025-05-01 14:53:15 -04:00
const { session , mcpLog , projectRoot } = context ;
const isMCP = ! ! mcpLog ;
// Create a consistent logFn object regardless of context
const logFn = isMCP
? mcpLog // Use MCP logger if provided
: {
// Create a wrapper around consoleLog for CLI
info : ( ... args ) => consoleLog ( 'info' , ... args ) ,
warn : ( ... args ) => consoleLog ( 'warn' , ... args ) ,
error : ( ... args ) => consoleLog ( 'error' , ... args ) ,
debug : ( ... args ) => consoleLog ( 'debug' , ... args ) ,
success : ( ... args ) => consoleLog ( 'success' , ... args )
} ;
const effectivePriority = priority || getDefaultPriority ( projectRoot ) ;
logFn . info (
` Adding new task with prompt: " ${ prompt } ", Priority: ${ effectivePriority } , Dependencies: ${ dependencies . join ( ', ' ) || 'None' } , Research: ${ useResearch } , ProjectRoot: ${ projectRoot } `
) ;
2025-04-24 01:59:41 -04:00
let loadingIndicator = null ;
// Create custom reporter that checks for MCP log
const report = ( message , level = 'info' ) => {
if ( mcpLog ) {
mcpLog [ level ] ( message ) ;
} else if ( outputFormat === 'text' ) {
2025-05-01 14:53:15 -04:00
consoleLog ( level , message ) ;
2025-04-24 01:59:41 -04:00
}
} ;
2025-04-21 17:48:30 -04:00
try {
// Only display banner and UI elements for text output (CLI)
if ( outputFormat === 'text' ) {
displayBanner ( ) ;
console . log (
boxen ( chalk . white . bold ( ` Creating New Task ` ) , {
padding : 1 ,
borderColor : 'blue' ,
borderStyle : 'round' ,
margin : { top : 1 , bottom : 1 }
} )
) ;
}
// Read the existing tasks
const data = readJSON ( tasksPath ) ;
if ( ! data || ! data . tasks ) {
2025-04-24 01:59:41 -04:00
report ( 'Invalid or missing tasks.json.' , 'error' ) ;
2025-04-21 17:48:30 -04:00
throw new Error ( 'Invalid or missing tasks.json.' ) ;
}
// Find the highest task ID to determine the next ID
2025-04-24 01:59:41 -04:00
const highestId =
data . tasks . length > 0 ? Math . max ( ... data . tasks . map ( ( t ) => t . id ) ) : 0 ;
2025-04-21 17:48:30 -04:00
const newTaskId = highestId + 1 ;
// Only show UI box for CLI mode
if ( outputFormat === 'text' ) {
console . log (
boxen ( chalk . white . bold ( ` Creating New Task # ${ newTaskId } ` ) , {
padding : 1 ,
borderColor : 'blue' ,
borderStyle : 'round' ,
margin : { top : 1 , bottom : 1 }
} )
) ;
}
// Validate dependencies before proceeding
const invalidDeps = dependencies . filter ( ( depId ) => {
2025-04-24 01:59:41 -04:00
// Ensure depId is parsed as a number for comparison
const numDepId = parseInt ( depId , 10 ) ;
return isNaN ( numDepId ) || ! data . tasks . some ( ( t ) => t . id === numDepId ) ;
2025-04-21 17:48:30 -04:00
} ) ;
if ( invalidDeps . length > 0 ) {
2025-04-24 01:59:41 -04:00
report (
` The following dependencies do not exist or are invalid: ${ invalidDeps . join ( ', ' ) } ` ,
'warn'
2025-04-21 17:48:30 -04:00
) ;
2025-04-24 01:59:41 -04:00
report ( 'Removing invalid dependencies...' , 'info' ) ;
2025-04-21 17:48:30 -04:00
dependencies = dependencies . filter (
( depId ) => ! invalidDeps . includes ( depId )
) ;
}
2025-04-24 01:59:41 -04:00
// Ensure dependencies are numbers
const numericDependencies = dependencies . map ( ( dep ) => parseInt ( dep , 10 ) ) ;
2025-04-21 17:48:30 -04:00
let taskData ;
// Check if manual task data is provided
if ( manualTaskData ) {
2025-04-24 01:59:41 -04:00
report ( 'Using manually provided task data' , 'info' ) ;
2025-04-21 17:48:30 -04:00
taskData = manualTaskData ;
2025-04-26 18:30:02 -04:00
report ( 'DEBUG: Taking MANUAL task data path.' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
// Basic validation for manual data
if (
! taskData . title ||
typeof taskData . title !== 'string' ||
! taskData . description ||
typeof taskData . description !== 'string'
) {
throw new Error (
'Manual task data must include at least a title and description.'
) ;
}
2025-04-21 17:48:30 -04:00
} else {
2025-04-26 18:30:02 -04:00
report ( 'DEBUG: Taking AI task generation path.' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
// --- Refactored AI Interaction ---
report ( 'Generating task data with AI...' , 'info' ) ;
2025-04-21 17:48:30 -04:00
// Create context string for task creation prompt
let contextTasks = '' ;
2025-04-24 01:59:41 -04:00
if ( numericDependencies . length > 0 ) {
2025-04-21 17:48:30 -04:00
const dependentTasks = data . tasks . filter ( ( t ) =>
2025-04-24 01:59:41 -04:00
numericDependencies . includes ( t . id )
2025-04-21 17:48:30 -04:00
) ;
contextTasks = ` \n This task depends on the following tasks: \n ${ dependentTasks
. map ( ( t ) => ` - Task ${ t . id } : ${ t . title } - ${ t . description } ` )
. join ( '\n' ) } ` ;
} else {
const recentTasks = [ ... data . tasks ]
. sort ( ( a , b ) => b . id - a . id )
. slice ( 0 , 3 ) ;
2025-04-24 01:59:41 -04:00
if ( recentTasks . length > 0 ) {
contextTasks = ` \n Recent tasks in the project: \n ${ recentTasks
. map ( ( t ) => ` - Task ${ t . id } : ${ t . title } - ${ t . description } ` )
. join ( '\n' ) } ` ;
}
2025-04-21 17:48:30 -04:00
}
2025-04-24 01:59:41 -04:00
// System Prompt
const systemPrompt =
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema." ;
// Task Structure Description (for user prompt)
const taskStructureDesc = `
{
"title" : "Task title goes here" ,
"description" : "A concise one or two sentence description of what the task involves" ,
"details" : "In-depth implementation details, considerations, and guidance." ,
"testStrategy" : "Detailed approach for verifying task completion."
} ` ;
2025-04-26 18:30:02 -04:00
// Add any manually provided details to the prompt for context
let contextFromArgs = '' ;
if ( manualTaskData ? . title )
contextFromArgs += ` \n - Suggested Title: " ${ manualTaskData . title } " ` ;
if ( manualTaskData ? . description )
contextFromArgs += ` \n - Suggested Description: " ${ manualTaskData . description } " ` ;
if ( manualTaskData ? . details )
contextFromArgs += ` \n - Additional Details Context: " ${ manualTaskData . details } " ` ;
if ( manualTaskData ? . testStrategy )
contextFromArgs += ` \n - Additional Test Strategy Context: " ${ manualTaskData . testStrategy } " ` ;
2025-04-24 01:59:41 -04:00
// User Prompt
const userPrompt = ` Create a comprehensive new task (Task # ${ newTaskId } ) for a software development project based on this description: " ${ prompt } "
$ { contextTasks }
2025-04-26 18:30:02 -04:00
$ { contextFromArgs ? ` \n Consider these additional details provided by the user: ${ contextFromArgs } ` : '' }
Return your answer as a single JSON object matching the schema precisely :
$ { taskStructureDesc }
2025-04-24 01:59:41 -04:00
Make sure the details and test strategy are thorough and specific . ` ;
2025-04-21 17:48:30 -04:00
// Start the loading indicator - only for text mode
if ( outputFormat === 'text' ) {
loadingIndicator = startLoadingIndicator (
2025-04-24 01:59:41 -04:00
` Generating new task with ${ useResearch ? 'Research' : 'Main' } AI... `
2025-04-21 17:48:30 -04:00
) ;
}
try {
2025-04-24 01:59:41 -04:00
// Determine the service role based on the useResearch flag
const serviceRole = useResearch ? 'research' : 'main' ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: Calling generateObjectService...' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
// Call the unified AI service
const aiGeneratedTaskData = await generateObjectService ( {
role : serviceRole , // <-- Use the determined role
session : session , // Pass session for API key resolution
2025-05-01 14:53:15 -04:00
projectRoot : projectRoot , // <<< Pass projectRoot here
2025-04-24 01:59:41 -04:00
schema : AiTaskDataSchema , // Pass the Zod schema
objectName : 'newTaskData' , // Name for the object
systemPrompt : systemPrompt ,
2025-05-01 14:53:15 -04:00
prompt : userPrompt
2025-04-24 01:59:41 -04:00
} ) ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: generateObjectService returned successfully.' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
report ( 'Successfully generated task data from AI.' , 'success' ) ;
taskData = aiGeneratedTaskData ; // Assign the validated object
2025-04-21 17:48:30 -04:00
} catch ( error ) {
2025-04-29 01:54:42 -04:00
report (
` DEBUG: generateObjectService caught error: ${ error . message } ` ,
'debug'
) ;
2025-04-24 01:59:41 -04:00
report ( ` Error generating task with AI: ${ error . message } ` , 'error' ) ;
if ( loadingIndicator ) stopLoadingIndicator ( loadingIndicator ) ;
throw error ; // Re-throw error after logging
} finally {
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: generateObjectService finally block reached.' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
if ( loadingIndicator ) stopLoadingIndicator ( loadingIndicator ) ; // Ensure indicator stops
2025-04-21 17:48:30 -04:00
}
2025-04-24 01:59:41 -04:00
// --- End Refactored AI Interaction ---
2025-04-21 17:48:30 -04:00
}
// Create the new task object
const newTask = {
id : newTaskId ,
title : taskData . title ,
description : taskData . description ,
details : taskData . details || '' ,
testStrategy : taskData . testStrategy || '' ,
status : 'pending' ,
2025-04-24 01:59:41 -04:00
dependencies : numericDependencies , // Use validated numeric dependencies
2025-05-01 14:53:15 -04:00
priority : effectivePriority ,
2025-04-24 01:59:41 -04:00
subtasks : [ ] // Initialize with empty subtasks array
2025-04-21 17:48:30 -04:00
} ;
// Add the task to the tasks array
data . tasks . push ( newTask ) ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: Writing tasks.json...' , 'debug' ) ;
2025-04-21 17:48:30 -04:00
// Write the updated tasks to the file
writeJSON ( tasksPath , data ) ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: tasks.json written.' , 'debug' ) ;
2025-04-21 17:48:30 -04:00
// Generate markdown task files
2025-04-24 01:59:41 -04:00
report ( 'Generating task files...' , 'info' ) ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: Calling generateTaskFiles...' , 'debug' ) ;
2025-04-24 01:59:41 -04:00
// Pass mcpLog if available to generateTaskFiles
await generateTaskFiles ( tasksPath , path . dirname ( tasksPath ) , { mcpLog } ) ;
2025-04-29 01:54:42 -04:00
report ( 'DEBUG: generateTaskFiles finished.' , 'debug' ) ;
2025-04-21 17:48:30 -04:00
// Show success message - only for text output (CLI)
if ( outputFormat === 'text' ) {
const table = new Table ( {
head : [
chalk . cyan . bold ( 'ID' ) ,
chalk . cyan . bold ( 'Title' ) ,
chalk . cyan . bold ( 'Description' )
] ,
2025-04-24 01:59:41 -04:00
colWidths : [ 5 , 30 , 50 ] // Adjust widths as needed
2025-04-21 17:48:30 -04:00
} ) ;
table . push ( [
newTask . id ,
truncate ( newTask . title , 27 ) ,
truncate ( newTask . description , 47 )
] ) ;
console . log ( chalk . green ( '✅ New task created successfully:' ) ) ;
console . log ( table . toString ( ) ) ;
2025-04-24 01:59:41 -04:00
// Helper to get priority color
const getPriorityColor = ( p ) => {
switch ( p ? . toLowerCase ( ) ) {
case 'high' :
return 'red' ;
case 'low' :
return 'gray' ;
case 'medium' :
default :
return 'yellow' ;
}
} ;
// Show success message box
2025-04-21 17:48:30 -04:00
console . log (
boxen (
chalk . white . bold ( ` Task ${ newTaskId } Created Successfully ` ) +
'\n\n' +
chalk . white ( ` Title: ${ newTask . title } ` ) +
'\n' +
chalk . white ( ` Status: ${ getStatusWithColor ( newTask . status ) } ` ) +
'\n' +
chalk . white (
2025-04-29 01:54:42 -04:00
` Priority: ${ chalk [ getPriorityColor ( newTask . priority ) ] ( newTask . priority ) } `
2025-04-21 17:48:30 -04:00
) +
'\n' +
2025-04-24 01:59:41 -04:00
( numericDependencies . length > 0
? chalk . white ( ` Dependencies: ${ numericDependencies . join ( ', ' ) } ` ) +
'\n'
2025-04-21 17:48:30 -04:00
: '' ) +
'\n' +
chalk . white . bold ( 'Next Steps:' ) +
'\n' +
chalk . cyan (
` 1. Run ${ chalk . yellow ( ` task-master show ${ newTaskId } ` ) } to see complete task details `
) +
'\n' +
chalk . cyan (
` 2. Run ${ chalk . yellow ( ` task-master set-status --id= ${ newTaskId } --status=in-progress ` ) } to start working on it `
) +
'\n' +
chalk . cyan (
` 3. Run ${ chalk . yellow ( ` task-master expand --id= ${ newTaskId } ` ) } to break it down into subtasks `
) ,
{ padding : 1 , borderColor : 'green' , borderStyle : 'round' }
)
) ;
}
// Return the new task ID
2025-04-29 01:54:42 -04:00
report ( ` DEBUG: Returning new task ID: ${ newTaskId } ` , 'debug' ) ;
2025-04-21 17:48:30 -04:00
return newTaskId ;
} catch ( error ) {
2025-04-24 01:59:41 -04:00
// Stop any loading indicator on error
if ( loadingIndicator ) {
2025-04-21 17:48:30 -04:00
stopLoadingIndicator ( loadingIndicator ) ;
}
2025-04-24 01:59:41 -04:00
report ( ` Error adding task: ${ error . message } ` , 'error' ) ;
2025-04-21 17:48:30 -04:00
if ( outputFormat === 'text' ) {
console . error ( chalk . red ( ` Error: ${ error . message } ` ) ) ;
}
2025-04-24 01:59:41 -04:00
// In MCP mode, we let the direct function handler catch and format
2025-04-21 17:48:30 -04:00
throw error ;
}
}
export default addTask ;