fix(parse-prd): pass projectRoot and fix schema/logging

Modified parse-prd core, direct function, and tool to pass projectRoot for .env API key fallback. Corrected Zod schema used in generateObjectService call. Fixed logFn reference error in core parsePRD. Updated unit test mock for utils.js.
This commit is contained in:
Eyal Toledano
2025-05-01 17:11:51 -04:00
parent d07f8fddc5
commit ad1c234b4e
5 changed files with 484 additions and 303 deletions

View File

@@ -8,9 +8,11 @@ import fs from 'fs';
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js';
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
@@ -21,177 +23,160 @@ import { createLogWrapper } from '../../tools/utils.js';
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function parsePRDDirect(args, log, context = {}) {
const { session } = context; // Only extract session
const { session } = context;
// Extract projectRoot from args
const {
input: inputArg,
output: outputArg,
numTasks: numTasksArg,
force,
append,
projectRoot
} = args;
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
const logWrapper = createLogWrapper(log);
// Validate required parameters
if (!args.projectRoot) {
const errorMessage = 'Project root is required for parsePRDDirect';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_PROJECT_ROOT', message: errorMessage },
fromCache: false
};
}
if (!args.input) {
const errorMessage = 'Input file path is required for parsePRDDirect';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_INPUT_PATH', message: errorMessage },
fromCache: false
};
}
if (!args.output) {
const errorMessage = 'Output file path is required for parsePRDDirect';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_OUTPUT_PATH', message: errorMessage },
fromCache: false
};
}
// Resolve input path (expecting absolute path or path relative to project root)
const projectRoot = args.projectRoot;
const inputPath = path.isAbsolute(args.input)
? args.input
: path.resolve(projectRoot, args.input);
// Verify input file exists
if (!fs.existsSync(inputPath)) {
const errorMessage = `Input file not found: ${inputPath}`;
log.error(errorMessage);
return {
success: false,
error: {
code: 'INPUT_FILE_NOT_FOUND',
message: errorMessage,
details: `Checked path: ${inputPath}\nProject root: ${projectRoot}\nInput argument: ${args.input}`
},
fromCache: false
};
}
// Resolve output path (expecting absolute path or path relative to project root)
const outputPath = path.isAbsolute(args.output)
? args.output
: path.resolve(projectRoot, args.output);
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
log.info(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
// Parse number of tasks - handle both string and number values
let numTasks = 10; // Default
if (args.numTasks) {
numTasks =
typeof args.numTasks === 'string'
? parseInt(args.numTasks, 10)
: args.numTasks;
if (isNaN(numTasks)) {
numTasks = 10; // Fallback to default if parsing fails
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
}
}
// Extract the append flag from args
const append = Boolean(args.append) === true;
// Log key parameters including append flag
log.info(
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
// --- Input Validation and Path Resolution ---
if (!projectRoot || !path.isAbsolute(projectRoot)) {
logWrapper.error(
'parsePRDDirect requires an absolute projectRoot argument.'
);
// --- Logger Wrapper ---
const mcpLog = createLogWrapper(log);
// Prepare options for the core function
const options = {
mcpLog,
session
};
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
try {
// Make sure the output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
log.info(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
// Execute core parsePRD function with AI client
const tasksDataResult = await parsePRD(
inputPath,
outputPath,
numTasks,
{
mcpLog: logWrapper,
session,
append
},
aiClient,
modelConfig
);
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
// to return it to the caller
if (fs.existsSync(outputPath)) {
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
const actionVerb = append ? 'appended' : 'generated';
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
if (!tasksDataResult || !tasksDataResult.tasks || !tasksData) {
throw new Error(
'Core parsePRD function did not return valid task data.'
);
}
log.info(message);
return {
success: true,
data: {
message,
taskCount: tasksDataResult.tasks?.length || 0,
outputPath,
appended: append
},
fromCache: false // This operation always modifies state and should never be cached
};
} else {
const errorMessage = `Tasks file was not created at ${outputPath}`;
log.error(errorMessage);
return {
success: false,
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
fromCache: false
};
}
} finally {
// Always restore normal logging
disableSilentMode();
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error parsing PRD: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'PARSE_PRD_ERROR', // Use error code if available
message: error.message || 'Unknown error parsing PRD'
},
fromCache: false
code: 'MISSING_ARGUMENT',
message: 'projectRoot is required and must be absolute.'
}
};
}
if (!inputArg) {
logWrapper.error('parsePRDDirect called without input path');
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' }
};
}
// Resolve input and output paths relative to projectRoot if they aren't absolute
const inputPath = path.resolve(projectRoot, inputArg);
const outputPath = outputArg
? path.resolve(projectRoot, outputArg)
: path.resolve(projectRoot, 'tasks', 'tasks.json'); // Default output path
// Check if input file exists
if (!fs.existsSync(inputPath)) {
const errorMsg = `Input PRD file not found at resolved path: ${inputPath}`;
logWrapper.error(errorMsg);
return {
success: false,
error: { code: 'FILE_NOT_FOUND', message: errorMsg }
};
}
const outputDir = path.dirname(outputPath);
try {
if (!fs.existsSync(outputDir)) {
logWrapper.info(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
} catch (dirError) {
logWrapper.error(
`Failed to create output directory ${outputDir}: ${dirError.message}`
);
// Return an error response immediately if dir creation fails
return {
success: false,
error: {
code: 'DIRECTORY_CREATION_ERROR',
message: `Failed to create output directory: ${dirError.message}`
}
};
}
let numTasks = getDefaultNumTasks(projectRoot);
if (numTasksArg) {
numTasks =
typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg;
if (isNaN(numTasks) || numTasks <= 0) {
// Ensure positive number
numTasks = getDefaultNumTasks(projectRoot); // Fallback to default if parsing fails or invalid
logWrapper.warn(
`Invalid numTasks value: ${numTasksArg}. Using default: 10`
);
}
}
const useForce = force === true;
const useAppend = append === true;
if (useAppend) {
logWrapper.info('Append mode enabled.');
if (useForce) {
logWrapper.warn(
'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.'
);
}
}
logWrapper.info(
`Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${useForce}, Append: ${useAppend}, ProjectRoot: ${projectRoot}`
);
const wasSilent = isSilentMode();
if (!wasSilent) {
enableSilentMode();
}
try {
// Call the core parsePRD function
const result = await parsePRD(
inputPath,
outputPath,
numTasks,
{ session, mcpLog: logWrapper, projectRoot, useForce, useAppend },
'json'
);
// parsePRD returns { success: true, tasks: processedTasks } on success
if (result && result.success && Array.isArray(result.tasks)) {
logWrapper.success(
`Successfully parsed PRD. Generated ${result.tasks.length} tasks.`
);
return {
success: true,
data: {
message: `Successfully parsed PRD and generated ${result.tasks.length} tasks.`,
outputPath: outputPath,
taskCount: result.tasks.length
// Optionally include tasks if needed by client: tasks: result.tasks
}
};
} else {
// Handle case where core function didn't return expected success structure
logWrapper.error(
'Core parsePRD function did not return a successful structure.'
);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message:
result?.message ||
'Core function failed to parse PRD or returned unexpected result.'
}
};
}
} catch (error) {
logWrapper.error(`Error executing core parsePRD: ${error.message}`);
return {
success: false,
error: {
code: 'PARSE_PRD_CORE_ERROR',
message: error.message || 'Unknown error parsing PRD'
}
};
} finally {
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
}
}

View File

@@ -4,16 +4,12 @@
*/
import { z } from 'zod';
import {
getProjectRootFromSession,
handleApiResult,
createErrorResponse
} from './utils.js';
import path from 'path';
import { handleApiResult, createErrorResponse } from './utils.js';
import { parsePRDDirect } from '../core/task-master-core.js';
import { resolveProjectPaths } from '../core/utils/path-utils.js';
/**
* Register the parsePRD tool with the MCP server
* Register the parse_prd tool
* @param {Object} server - FastMCP server instance
*/
export function registerParsePRDTool(server) {
@@ -42,71 +38,64 @@ export function registerParsePRDTool(server) {
force: z
.boolean()
.optional()
.describe('Allow overwriting an existing tasks.json file.'),
.default(false)
.describe('Overwrite existing output file without prompting.'),
append: z
.boolean()
.optional()
.describe(
'Append new tasks to existing tasks.json instead of overwriting'
),
.default(false)
.describe('Append generated tasks to existing file.'),
projectRoot: z
.string()
.describe('The directory of the project. Must be absolute path.')
.describe('The directory of the project. Must be an absolute path.')
}),
execute: async (args, { log, session }) => {
const toolName = 'parse_prd';
try {
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
// Get project root from args or session
const rootFolder =
args.projectRoot || getProjectRootFromSession(session, log);
if (!rootFolder) {
return createErrorResponse(
'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.'
);
}
// Resolve input (PRD) and output (tasks.json) paths using the utility
const { projectRoot, prdPath, tasksJsonPath } = resolveProjectPaths(
rootFolder,
args,
log
log.info(
`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
);
// Check if PRD path was found (resolveProjectPaths returns null if not found and not provided)
if (!prdPath) {
// 1. Get Project Root
const rootFolder = args.projectRoot;
if (!rootFolder || !path.isAbsolute(rootFolder)) {
log.error(
`${toolName}: projectRoot is required and must be absolute.`
);
return createErrorResponse(
'No PRD document found or provided. Please ensure a PRD file exists (e.g., PRD.md) or provide a valid input file path.'
'projectRoot is required and must be absolute.'
);
}
log.info(`${toolName}: Project root: ${rootFolder}`);
// Call the direct function with fully resolved paths
// 2. Call Direct Function - Pass relevant args including projectRoot
// Path resolution (input/output) is handled within the direct function now
const result = await parsePRDDirect(
{
projectRoot: projectRoot,
input: prdPath,
output: tasksJsonPath,
numTasks: args.numTasks,
// Pass args directly needed by the direct function
input: args.input, // Pass relative or absolute path
output: args.output, // Pass relative or absolute path
numTasks: args.numTasks, // Pass number (direct func handles default)
force: args.force,
append: args.append
append: args.append,
projectRoot: rootFolder
},
log,
{ session }
{ session } // Pass context object with session
);
if (result.success) {
log.info(`Successfully parsed PRD: ${result.data.message}`);
} else {
log.error(
`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`
);
}
// 3. Handle Result
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(result, log, 'Error parsing PRD');
} catch (error) {
log.error(`Error in parse-prd tool: ${error.message}`);
return createErrorResponse(error.message);
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`
);
return createErrorResponse(
`Internal tool error (${toolName}): ${error.message}`
);
}
}
});