refactor(config): Standardize env var access and config getters
This commit focuses on standardizing configuration and API key access patterns across key modules as part of subtask 61.34. Key changes include: - Refactored `ai-services.js` to remove global AI clients and use `resolveEnvVariable` for API key checks. Client instantiation now relies on `getAnthropicClient`/`getPerplexityClient` accepting a session object. - Refactored `task-manager.js` (`analyzeTaskComplexity` function) to use the unified `generateTextService` from `ai-services-unified.js`, removing direct AI client calls. - Replaced direct `process.env` access for model parameters and other configurations (`PERPLEXITY_MODEL`, `CONFIG.*`) in `task-manager.js` with calls to the appropriate getters from `config-manager.js` (e.g., `getResearchModelId(session)`, `getMainMaxTokens(session)`). - Ensured `utils.js` (`resolveEnvVariable`) correctly handles potentially undefined session objects. - Updated function signatures where necessary to propagate the `session` object for correct context-aware configuration/key retrieval. This moves towards the goal of using `ai-client-factory.js` and `ai-services-unified.js` as the standard pattern for AI interactions and centralizing configuration management through `config-manager.js`.
This commit is contained in:
335
scripts/modules/task-manager/expand-all-tasks.js
Normal file
335
scripts/modules/task-manager/expand-all-tasks.js
Normal file
@@ -0,0 +1,335 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
|
||||
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
|
||||
|
||||
import {
|
||||
displayBanner,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator
|
||||
} from '../ui.js';
|
||||
|
||||
import { getDefaultSubtasks } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Expand all pending tasks with subtasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number} numSubtasks - Number of subtasks per task
|
||||
* @param {boolean} useResearch - Whether to use research (Perplexity)
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @param {boolean} forceFlag - Force regeneration for tasks with subtasks
|
||||
* @param {Object} options - Options for expanding tasks
|
||||
* @param {function} options.reportProgress - Function to report progress
|
||||
* @param {Object} options.mcpLog - MCP logger object
|
||||
* @param {Object} options.session - Session object from MCP
|
||||
* @param {string} outputFormat - Output format (text or json)
|
||||
*/
|
||||
async function expandAllTasks(
|
||||
tasksPath,
|
||||
numSubtasks = getDefaultSubtasks(), // Use getter
|
||||
useResearch = false,
|
||||
additionalContext = '',
|
||||
forceFlag = false,
|
||||
{ reportProgress, mcpLog, session } = {},
|
||||
outputFormat = 'text'
|
||||
) {
|
||||
// Create custom reporter that checks for MCP log and silent mode
|
||||
const report = (message, level = 'info') => {
|
||||
if (mcpLog) {
|
||||
mcpLog[level](message);
|
||||
} else if (!isSilentMode() && outputFormat === 'text') {
|
||||
// Only log to console if not in silent mode and outputFormat is 'text'
|
||||
log(level, message);
|
||||
}
|
||||
};
|
||||
|
||||
// Only display banner and UI elements for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
displayBanner();
|
||||
}
|
||||
|
||||
// Parse numSubtasks as integer if it's a string
|
||||
if (typeof numSubtasks === 'string') {
|
||||
numSubtasks = parseInt(numSubtasks, 10);
|
||||
if (isNaN(numSubtasks)) {
|
||||
numSubtasks = getDefaultSubtasks(); // Use getter
|
||||
}
|
||||
}
|
||||
|
||||
report(`Expanding all pending tasks with ${numSubtasks} subtasks each...`);
|
||||
if (useResearch) {
|
||||
report('Using research-backed AI for more detailed subtasks');
|
||||
}
|
||||
|
||||
// Load tasks
|
||||
let data;
|
||||
try {
|
||||
data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error('No valid tasks found');
|
||||
}
|
||||
} catch (error) {
|
||||
report(`Error loading tasks: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Get all tasks that are pending/in-progress and don't have subtasks (or force regeneration)
|
||||
const tasksToExpand = data.tasks.filter(
|
||||
(task) =>
|
||||
(task.status === 'pending' || task.status === 'in-progress') &&
|
||||
(!task.subtasks || task.subtasks.length === 0 || forceFlag)
|
||||
);
|
||||
|
||||
if (tasksToExpand.length === 0) {
|
||||
report(
|
||||
'No tasks eligible for expansion. Tasks should be in pending/in-progress status and not have subtasks already.',
|
||||
'info'
|
||||
);
|
||||
|
||||
// Return structured result for MCP
|
||||
return {
|
||||
success: true,
|
||||
expandedCount: 0,
|
||||
tasksToExpand: 0,
|
||||
message: 'No tasks eligible for expansion'
|
||||
};
|
||||
}
|
||||
|
||||
report(`Found ${tasksToExpand.length} tasks to expand`);
|
||||
|
||||
// Check if we have a complexity report to prioritize complex tasks
|
||||
let complexityReport;
|
||||
const reportPath = path.join(
|
||||
path.dirname(tasksPath),
|
||||
'../scripts/task-complexity-report.json'
|
||||
);
|
||||
if (fs.existsSync(reportPath)) {
|
||||
try {
|
||||
complexityReport = readJSON(reportPath);
|
||||
report('Using complexity analysis to prioritize tasks');
|
||||
} catch (error) {
|
||||
report(`Could not read complexity report: ${error.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
// Only create loading indicator if not in silent mode and outputFormat is 'text'
|
||||
let loadingIndicator = null;
|
||||
if (!isSilentMode() && outputFormat === 'text') {
|
||||
loadingIndicator = startLoadingIndicator(
|
||||
`Expanding ${tasksToExpand.length} tasks with ${numSubtasks} subtasks each`
|
||||
);
|
||||
}
|
||||
|
||||
let expandedCount = 0;
|
||||
let expansionErrors = 0;
|
||||
try {
|
||||
// Sort tasks by complexity if report exists, otherwise by ID
|
||||
if (complexityReport && complexityReport.complexityAnalysis) {
|
||||
report('Sorting tasks by complexity...');
|
||||
|
||||
// Create a map of task IDs to complexity scores
|
||||
const complexityMap = new Map();
|
||||
complexityReport.complexityAnalysis.forEach((analysis) => {
|
||||
complexityMap.set(analysis.taskId, analysis.complexityScore);
|
||||
});
|
||||
|
||||
// Sort tasks by complexity score (high to low)
|
||||
tasksToExpand.sort((a, b) => {
|
||||
const scoreA = complexityMap.get(a.id) || 0;
|
||||
const scoreB = complexityMap.get(b.id) || 0;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
}
|
||||
|
||||
// Process each task
|
||||
for (const task of tasksToExpand) {
|
||||
if (loadingIndicator && outputFormat === 'text') {
|
||||
loadingIndicator.text = `Expanding task ${task.id}: ${truncate(task.title, 30)} (${expandedCount + 1}/${tasksToExpand.length})`;
|
||||
}
|
||||
|
||||
// Report progress to MCP if available
|
||||
if (reportProgress) {
|
||||
reportProgress({
|
||||
status: 'processing',
|
||||
current: expandedCount + 1,
|
||||
total: tasksToExpand.length,
|
||||
message: `Expanding task ${task.id}: ${truncate(task.title, 30)}`
|
||||
});
|
||||
}
|
||||
|
||||
report(`Expanding task ${task.id}: ${truncate(task.title, 50)}`);
|
||||
|
||||
// Check if task already has subtasks and forceFlag is enabled
|
||||
if (task.subtasks && task.subtasks.length > 0 && forceFlag) {
|
||||
report(
|
||||
`Task ${task.id} already has ${task.subtasks.length} subtasks. Clearing them for regeneration.`
|
||||
);
|
||||
task.subtasks = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Get complexity analysis for this task if available
|
||||
let taskAnalysis;
|
||||
if (complexityReport && complexityReport.complexityAnalysis) {
|
||||
taskAnalysis = complexityReport.complexityAnalysis.find(
|
||||
(a) => a.taskId === task.id
|
||||
);
|
||||
}
|
||||
|
||||
let thisNumSubtasks = numSubtasks;
|
||||
|
||||
// Use recommended number of subtasks from complexity analysis if available
|
||||
if (taskAnalysis && taskAnalysis.recommendedSubtasks) {
|
||||
report(
|
||||
`Using recommended ${taskAnalysis.recommendedSubtasks} subtasks based on complexity score ${taskAnalysis.complexityScore}/10 for task ${task.id}`
|
||||
);
|
||||
thisNumSubtasks = taskAnalysis.recommendedSubtasks;
|
||||
}
|
||||
|
||||
// Generate prompt for subtask creation based on task details
|
||||
const prompt = generateSubtaskPrompt(
|
||||
task,
|
||||
thisNumSubtasks,
|
||||
additionalContext,
|
||||
taskAnalysis
|
||||
);
|
||||
|
||||
// Use AI to generate subtasks
|
||||
const aiResponse = await getSubtasksFromAI(
|
||||
prompt,
|
||||
useResearch,
|
||||
session,
|
||||
mcpLog
|
||||
);
|
||||
|
||||
if (
|
||||
aiResponse &&
|
||||
aiResponse.subtasks &&
|
||||
Array.isArray(aiResponse.subtasks) &&
|
||||
aiResponse.subtasks.length > 0
|
||||
) {
|
||||
// Process and add the subtasks to the task
|
||||
task.subtasks = aiResponse.subtasks.map((subtask, index) => ({
|
||||
id: index + 1,
|
||||
title: subtask.title || `Subtask ${index + 1}`,
|
||||
description: subtask.description || 'No description provided',
|
||||
status: 'pending',
|
||||
dependencies: subtask.dependencies || [],
|
||||
details: subtask.details || ''
|
||||
}));
|
||||
|
||||
report(`Added ${task.subtasks.length} subtasks to task ${task.id}`);
|
||||
expandedCount++;
|
||||
} else if (aiResponse && aiResponse.error) {
|
||||
// Handle error response
|
||||
const errorMsg = `Failed to generate subtasks for task ${task.id}: ${aiResponse.error}`;
|
||||
report(errorMsg, 'error');
|
||||
|
||||
// Add task ID to error info and provide actionable guidance
|
||||
const suggestion = aiResponse.suggestion.replace('<id>', task.id);
|
||||
report(`Suggestion: ${suggestion}`, 'info');
|
||||
|
||||
expansionErrors++;
|
||||
} else {
|
||||
report(`Failed to generate subtasks for task ${task.id}`, 'error');
|
||||
report(
|
||||
`Suggestion: Run 'task-master update-task --id=${task.id} --prompt="Generate subtasks for this task"' to manually create subtasks.`,
|
||||
'info'
|
||||
);
|
||||
expansionErrors++;
|
||||
}
|
||||
} catch (error) {
|
||||
report(`Error expanding task ${task.id}: ${error.message}`, 'error');
|
||||
expansionErrors++;
|
||||
}
|
||||
|
||||
// Small delay to prevent rate limiting
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Save the updated tasks
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate task files
|
||||
if (outputFormat === 'text') {
|
||||
// Only perform file generation for CLI (text) mode
|
||||
const outputDir = path.dirname(tasksPath);
|
||||
await generateTaskFiles(tasksPath, outputDir);
|
||||
}
|
||||
|
||||
// Return structured result for MCP
|
||||
return {
|
||||
success: true,
|
||||
expandedCount,
|
||||
tasksToExpand: tasksToExpand.length,
|
||||
expansionErrors,
|
||||
message: `Successfully expanded ${expandedCount} out of ${tasksToExpand.length} tasks${expansionErrors > 0 ? ` (${expansionErrors} errors)` : ''}`
|
||||
};
|
||||
} catch (error) {
|
||||
report(`Error expanding tasks: ${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
// Stop the loading indicator if it was created
|
||||
if (loadingIndicator && outputFormat === 'text') {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
}
|
||||
|
||||
// Final progress report
|
||||
if (reportProgress) {
|
||||
reportProgress({
|
||||
status: 'completed',
|
||||
current: expandedCount,
|
||||
total: tasksToExpand.length,
|
||||
message: `Completed expanding ${expandedCount} out of ${tasksToExpand.length} tasks`
|
||||
});
|
||||
}
|
||||
|
||||
// Display completion message for CLI mode
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`Task Expansion Completed`) +
|
||||
'\n\n' +
|
||||
chalk.white(
|
||||
`Expanded ${expandedCount} out of ${tasksToExpand.length} tasks`
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
`Each task now has detailed subtasks to guide implementation`
|
||||
),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Suggest next actions
|
||||
if (expandedCount > 0) {
|
||||
console.log(chalk.bold('\nNext Steps:'));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`1. Run ${chalk.yellow('task-master list --with-subtasks')} to see all tasks with their subtasks`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`2. Run ${chalk.yellow('task-master next')} to find the next task to work on`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`3. Run ${chalk.yellow('task-master set-status --id=<taskId> --status=in-progress')} to start working on a task`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default expandAllTasks;
|
||||
Reference in New Issue
Block a user