mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-06 00:20:36 +00:00

Implements AI usage telemetry capture and propagation for the command and MCP tool, following the established telemetry pattern. Key changes: - **Core ():** - Modified the call to include and . - Updated to receive from . - Adjusted to return an object . - Added a call to to show telemetry data in the CLI output when not in MCP mode. - **Direct Function ():** - Updated the call to the core function to pass , , and . - Modified to correctly handle the new return structure from the core function. - Ensures received from the core function is included in the field of the successful MCP response. - **MCP Tool ():** - No changes required; existing correctly passes through the object containing . - **CLI Command ():** - The command's action now relies on the core function to handle CLI success messages and telemetry display. This ensures that AI usage for the functionality is tracked and can be displayed or logged as appropriate for both CLI and MCP interactions.
343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import chalk from 'chalk';
|
|
import boxen from 'boxen';
|
|
import { z } from 'zod';
|
|
|
|
import {
|
|
log,
|
|
writeJSON,
|
|
enableSilentMode,
|
|
disableSilentMode,
|
|
isSilentMode,
|
|
readJSON,
|
|
findTaskById
|
|
} from '../utils.js';
|
|
|
|
import { generateObjectService } from '../ai-services-unified.js';
|
|
import { getDebugFlag } from '../config-manager.js';
|
|
import generateTaskFiles from './generate-task-files.js';
|
|
import { displayAiUsageSummary } from '../ui.js';
|
|
|
|
// Define the Zod schema for a SINGLE task object
|
|
const prdSingleTaskSchema = z.object({
|
|
id: z.number().int().positive(),
|
|
title: z.string().min(1),
|
|
description: z.string().min(1),
|
|
details: z.string().optional().default(''),
|
|
testStrategy: z.string().optional().default(''),
|
|
priority: z.enum(['high', 'medium', 'low']).default('medium'),
|
|
dependencies: z.array(z.number().int().positive()).optional().default([]),
|
|
status: z.string().optional().default('pending')
|
|
});
|
|
|
|
// Define the Zod schema for the ENTIRE expected AI response object
|
|
const prdResponseSchema = z.object({
|
|
tasks: z.array(prdSingleTaskSchema),
|
|
metadata: z.object({
|
|
projectName: z.string(),
|
|
totalTasks: z.number(),
|
|
sourceFile: z.string(),
|
|
generatedAt: z.string()
|
|
})
|
|
});
|
|
|
|
/**
|
|
* Parse a PRD file and generate tasks
|
|
* @param {string} prdPath - Path to the PRD file
|
|
* @param {string} tasksPath - Path to the tasks.json file
|
|
* @param {number} numTasks - Number of tasks to generate
|
|
* @param {Object} options - Additional options
|
|
* @param {boolean} [options.useForce=false] - Whether to overwrite existing tasks.json.
|
|
* @param {boolean} [options.useAppend=false] - Append to existing tasks file.
|
|
* @param {Object} [options.reportProgress] - Function to report progress (optional, likely unused).
|
|
* @param {Object} [options.mcpLog] - MCP logger object (optional).
|
|
* @param {Object} [options.session] - Session object from MCP server (optional).
|
|
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
|
|
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
|
*/
|
|
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
|
const {
|
|
reportProgress,
|
|
mcpLog,
|
|
session,
|
|
projectRoot,
|
|
useForce = false,
|
|
useAppend = false
|
|
} = options;
|
|
const isMCP = !!mcpLog;
|
|
const outputFormat = isMCP ? 'json' : 'text';
|
|
|
|
const logFn = mcpLog
|
|
? mcpLog
|
|
: {
|
|
// Wrapper for CLI
|
|
info: (...args) => log('info', ...args),
|
|
warn: (...args) => log('warn', ...args),
|
|
error: (...args) => log('error', ...args),
|
|
debug: (...args) => log('debug', ...args),
|
|
success: (...args) => log('success', ...args)
|
|
};
|
|
|
|
// Create custom reporter using logFn
|
|
const report = (message, level = 'info') => {
|
|
// Check logFn directly
|
|
if (logFn && typeof logFn[level] === 'function') {
|
|
logFn[level](message);
|
|
} else if (!isSilentMode() && outputFormat === 'text') {
|
|
// Fallback to original log only if necessary and in CLI text mode
|
|
log(level, message);
|
|
}
|
|
};
|
|
|
|
report(
|
|
`Parsing PRD file: ${prdPath}, Force: ${useForce}, Append: ${useAppend}`
|
|
);
|
|
|
|
let existingTasks = [];
|
|
let nextId = 1;
|
|
let aiServiceResponse = null;
|
|
|
|
try {
|
|
// Handle file existence and overwrite/append logic
|
|
if (fs.existsSync(tasksPath)) {
|
|
if (useAppend) {
|
|
report(
|
|
`Append mode enabled. Reading existing tasks from ${tasksPath}`,
|
|
'info'
|
|
);
|
|
const existingData = readJSON(tasksPath); // Use readJSON utility
|
|
if (existingData && Array.isArray(existingData.tasks)) {
|
|
existingTasks = existingData.tasks;
|
|
if (existingTasks.length > 0) {
|
|
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
|
report(
|
|
`Found ${existingTasks.length} existing tasks. Next ID will be ${nextId}.`,
|
|
'info'
|
|
);
|
|
}
|
|
} else {
|
|
report(
|
|
`Could not read existing tasks from ${tasksPath} or format is invalid. Proceeding without appending.`,
|
|
'warn'
|
|
);
|
|
existingTasks = []; // Reset if read fails
|
|
}
|
|
} else if (!useForce) {
|
|
// Not appending and not forcing overwrite
|
|
const overwriteError = new Error(
|
|
`Output file ${tasksPath} already exists. Use --force to overwrite or --append.`
|
|
);
|
|
report(overwriteError.message, 'error');
|
|
if (outputFormat === 'text') {
|
|
console.error(chalk.red(overwriteError.message));
|
|
process.exit(1);
|
|
} else {
|
|
throw overwriteError;
|
|
}
|
|
} else {
|
|
// Force overwrite is true
|
|
report(
|
|
`Force flag enabled. Overwriting existing file: ${tasksPath}`,
|
|
'info'
|
|
);
|
|
}
|
|
}
|
|
|
|
report(`Reading PRD content from ${prdPath}`, 'info');
|
|
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
|
if (!prdContent) {
|
|
throw new Error(`Input file ${prdPath} is empty or could not be read.`);
|
|
}
|
|
|
|
// Build system prompt for PRD parsing
|
|
const systemPrompt = `You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.
|
|
Analyze the provided PRD content and generate approximately ${numTasks} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD
|
|
Each task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.
|
|
Assign sequential IDs starting from ${nextId}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.
|
|
Set status to 'pending', dependencies to an empty array [], and priority to 'medium' initially for all tasks.
|
|
Respond ONLY with a valid JSON object containing a single key "tasks", where the value is an array of task objects adhering to the provided Zod schema. Do not include any explanation or markdown formatting.
|
|
|
|
Each task should follow this JSON structure:
|
|
{
|
|
"id": number,
|
|
"title": string,
|
|
"description": string,
|
|
"status": "pending",
|
|
"dependencies": number[] (IDs of tasks this depends on),
|
|
"priority": "high" | "medium" | "low",
|
|
"details": string (implementation details),
|
|
"testStrategy": string (validation approach)
|
|
}
|
|
|
|
Guidelines:
|
|
1. Unless complexity warrants otherwise, create exactly ${numTasks} tasks, numbered sequentially starting from ${nextId}
|
|
2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards
|
|
3. Order tasks logically - consider dependencies and implementation sequence
|
|
4. Early tasks should focus on setup, core functionality first, then advanced features
|
|
5. Include clear validation/testing approach for each task
|
|
6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than ${nextId} if applicable)
|
|
7. Assign priority (high/medium/low) based on criticality and dependency order
|
|
8. Include detailed implementation guidance in the "details" field
|
|
9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance
|
|
10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements
|
|
11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches`;
|
|
|
|
// Build user prompt with PRD content
|
|
const userPrompt = `Here's the Product Requirements Document (PRD) to break down into approximately ${numTasks} tasks, starting IDs from ${nextId}:\n\n${prdContent}\n\n
|
|
|
|
Return your response in this format:
|
|
{
|
|
"tasks": [
|
|
{
|
|
"id": 1,
|
|
"title": "Setup Project Repository",
|
|
"description": "...",
|
|
...
|
|
},
|
|
...
|
|
],
|
|
"metadata": {
|
|
"projectName": "PRD Implementation",
|
|
"totalTasks": ${numTasks},
|
|
"sourceFile": "${prdPath}",
|
|
"generatedAt": "YYYY-MM-DD"
|
|
}
|
|
}`;
|
|
|
|
// Call the unified AI service
|
|
report('Calling AI service to generate tasks from PRD...', 'info');
|
|
|
|
// Call generateObjectService with the CORRECT schema and additional telemetry params
|
|
aiServiceResponse = await generateObjectService({
|
|
role: 'main',
|
|
session: session,
|
|
projectRoot: projectRoot,
|
|
schema: prdResponseSchema,
|
|
objectName: 'tasks_data',
|
|
systemPrompt: systemPrompt,
|
|
prompt: userPrompt,
|
|
commandName: 'parse-prd',
|
|
outputType: isMCP ? 'mcp' : 'cli'
|
|
});
|
|
|
|
// Create the directory if it doesn't exist
|
|
const tasksDir = path.dirname(tasksPath);
|
|
if (!fs.existsSync(tasksDir)) {
|
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
}
|
|
logFn.success('Successfully parsed PRD via AI service.');
|
|
|
|
// Validate and Process Tasks
|
|
const generatedData = aiServiceResponse?.mainResult?.object;
|
|
|
|
if (!generatedData || !Array.isArray(generatedData.tasks)) {
|
|
logFn.error(
|
|
`Internal Error: generateObjectService returned unexpected data structure: ${JSON.stringify(generatedData)}`
|
|
);
|
|
throw new Error(
|
|
'AI service returned unexpected data structure after validation.'
|
|
);
|
|
}
|
|
|
|
let currentId = nextId;
|
|
const taskMap = new Map();
|
|
const processedNewTasks = generatedData.tasks.map((task) => {
|
|
const newId = currentId++;
|
|
taskMap.set(task.id, newId);
|
|
return {
|
|
...task,
|
|
id: newId,
|
|
status: 'pending',
|
|
priority: task.priority || 'medium',
|
|
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
|
subtasks: []
|
|
};
|
|
});
|
|
|
|
// Remap dependencies for the NEWLY processed tasks
|
|
processedNewTasks.forEach((task) => {
|
|
task.dependencies = task.dependencies
|
|
.map((depId) => taskMap.get(depId)) // Map old AI ID to new sequential ID
|
|
.filter(
|
|
(newDepId) =>
|
|
newDepId != null && // Must exist
|
|
newDepId < task.id && // Must be a lower ID (could be existing or newly generated)
|
|
(findTaskById(existingTasks, newDepId) || // Check if it exists in old tasks OR
|
|
processedNewTasks.some((t) => t.id === newDepId)) // check if it exists in new tasks
|
|
);
|
|
});
|
|
|
|
const finalTasks = useAppend
|
|
? [...existingTasks, ...processedNewTasks]
|
|
: processedNewTasks;
|
|
const outputData = { tasks: finalTasks };
|
|
|
|
// Write the final tasks to the file
|
|
writeJSON(tasksPath, outputData);
|
|
report(
|
|
`Successfully ${useAppend ? 'appended' : 'generated'} ${processedNewTasks.length} tasks in ${tasksPath}`,
|
|
'success'
|
|
);
|
|
|
|
// Generate markdown task files after writing tasks.json
|
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
|
|
|
// Handle CLI output (e.g., success message)
|
|
if (outputFormat === 'text') {
|
|
console.log(
|
|
boxen(
|
|
chalk.green(
|
|
`Successfully generated ${processedNewTasks.length} new tasks. Total tasks in ${tasksPath}: ${finalTasks.length}`
|
|
),
|
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
|
)
|
|
);
|
|
|
|
console.log(
|
|
boxen(
|
|
chalk.white.bold('Next Steps:') +
|
|
'\n\n' +
|
|
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
|
|
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
|
|
{
|
|
padding: 1,
|
|
borderColor: 'cyan',
|
|
borderStyle: 'round',
|
|
margin: { top: 1 }
|
|
}
|
|
)
|
|
);
|
|
|
|
if (aiServiceResponse && aiServiceResponse.telemetryData) {
|
|
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
|
}
|
|
}
|
|
|
|
// Return telemetry data
|
|
return {
|
|
success: true,
|
|
tasksPath,
|
|
telemetryData: aiServiceResponse?.telemetryData
|
|
};
|
|
} catch (error) {
|
|
report(`Error parsing PRD: ${error.message}`, 'error');
|
|
|
|
// Only show error UI for text output (CLI)
|
|
if (outputFormat === 'text') {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
|
|
if (getDebugFlag(projectRoot)) {
|
|
// Use projectRoot for debug flag check
|
|
console.error(error);
|
|
}
|
|
|
|
process.exit(1);
|
|
} else {
|
|
throw error; // Re-throw for JSON output
|
|
}
|
|
}
|
|
}
|
|
|
|
export default parsePRD;
|