diff --git a/scripts/modules/ai-services-unified.js b/scripts/modules/ai-services-unified.js index 35f15418..6a71459b 100644 --- a/scripts/modules/ai-services-unified.js +++ b/scripts/modules/ai-services-unified.js @@ -27,6 +27,7 @@ import { } from "./config-manager.js"; import { log, findProjectRoot, resolveEnvVariable } from "./utils.js"; import { submitTelemetryData } from "./telemetry-submission.js"; +import { isHostedMode } from "./user-management.js"; // Import provider classes import { @@ -267,6 +268,80 @@ async function _attemptProviderCallWithRetries( ); } +/** + * Makes an AI call through the TaskMaster gateway for hosted users + * @param {string} serviceType - Type of service (generateText, generateObject, streamText) + * @param {object} callParams - Parameters for the AI call + * @param {string} providerName - AI provider name + * @param {string} modelId - Model ID + * @param {string} userId - User ID + * @param {string} commandName - Command name for tracking + * @param {string} outputType - Output type (cli, mcp) + * @param {string} projectRoot - Project root path + * @returns {Promise} AI response with usage data + */ +async function _callGatewayAI( + serviceType, + callParams, + providerName, + modelId, + userId, + commandName, + outputType, + projectRoot +) { + const gatewayUrl = + process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444"; + const endpoint = `${gatewayUrl}/api/v1/ai/${serviceType}`; + + // Get API key from env + const apiKey = resolveEnvVariable("TASKMASTER_API_KEY", null, projectRoot); + if (!apiKey) { + throw new Error("TASKMASTER_API_KEY not found for hosted mode"); + } + + // need to make sure the user is authenticated and has a valid paid user token + enough credits for this call + + const requestBody = { + provider: providerName, + model: modelId, + serviceType, + userId, + commandName, + outputType, + ...callParams, + }; + + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Gateway AI call failed: ${response.status} ${errorText}`); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || "Gateway AI call failed"); + } + + // Return the AI response in the expected format + return { + text: result.data.text, + object: result.data.object, + usage: result.data.usage, + // Include any account info returned from gateway + accountInfo: result.accountInfo, + }; +} + /** * Base logic for unified service functions. * @param {string} serviceType - Type of service ('generateText', 'streamText', 'generateObject'). @@ -295,6 +370,7 @@ async function _unifiedServiceRunner(serviceType, params) { outputType, ...restApiParams } = params; + if (getDebugFlag()) { log("info", `${serviceType}Service called`, { role: initialRole, @@ -307,6 +383,115 @@ async function _unifiedServiceRunner(serviceType, params) { const effectiveProjectRoot = projectRoot || findProjectRoot(); const userId = getUserId(effectiveProjectRoot); + // Check if user is in hosted mode + const hostedMode = isHostedMode(effectiveProjectRoot); + + if (hostedMode) { + // For hosted mode, route through gateway + log("info", "Routing AI call through TaskMaster gateway (hosted mode)"); + + try { + // Get the role configuration for provider/model selection + let providerName, modelId; + if (initialRole === "main") { + providerName = getMainProvider(effectiveProjectRoot); + modelId = getMainModelId(effectiveProjectRoot); + } else if (initialRole === "research") { + providerName = getResearchProvider(effectiveProjectRoot); + modelId = getResearchModelId(effectiveProjectRoot); + } else if (initialRole === "fallback") { + providerName = getFallbackProvider(effectiveProjectRoot); + modelId = getFallbackModelId(effectiveProjectRoot); + } else { + throw new Error(`Unknown AI role: ${initialRole}`); + } + + if (!providerName || !modelId) { + throw new Error( + `Configuration missing for role '${initialRole}'. Provider: ${providerName}, Model: ${modelId}` + ); + } + + // Get role parameters + const roleParams = getParametersForRole( + initialRole, + effectiveProjectRoot + ); + + // Prepare messages + const messages = []; + if (systemPrompt) { + messages.push({ role: "system", content: systemPrompt }); + } + if (prompt) { + messages.push({ role: "user", content: prompt }); + } else { + throw new Error("User prompt content is missing."); + } + + const callParams = { + maxTokens: roleParams.maxTokens, + temperature: roleParams.temperature, + messages, + ...(serviceType === "generateObject" && { schema, objectName }), + ...restApiParams, + }; + + const gatewayResponse = await _callGatewayAI( + serviceType, + callParams, + providerName, + modelId, + userId, + commandName, + outputType, + effectiveProjectRoot + ); + + // For hosted mode, we don't need to submit telemetry separately + // The gateway handles everything and returns account info + let telemetryData = null; + if (gatewayResponse.accountInfo) { + // Convert gateway account info to telemetry format for UI display + telemetryData = { + timestamp: new Date().toISOString(), + userId, + commandName, + modelUsed: modelId, + providerName, + inputTokens: gatewayResponse.usage?.inputTokens || 0, + outputTokens: gatewayResponse.usage?.outputTokens || 0, + totalTokens: gatewayResponse.usage?.totalTokens || 0, + totalCost: 0, // Not used in hosted mode + currency: "USD", + // Include account info for UI display + accountInfo: gatewayResponse.accountInfo, + }; + } + + let finalMainResult; + if (serviceType === "generateText") { + finalMainResult = gatewayResponse.text; + } else if (serviceType === "generateObject") { + finalMainResult = gatewayResponse.object; + } else if (serviceType === "streamText") { + finalMainResult = gatewayResponse; // Streaming through gateway would need special handling + } else { + finalMainResult = gatewayResponse; + } + + return { + mainResult: finalMainResult, + telemetryData: telemetryData, + }; + } catch (error) { + const cleanMessage = _extractErrorMessage(error); + log("error", `Gateway AI call failed: ${cleanMessage}`); + throw new Error(cleanMessage); + } + } + + // For BYOK mode, continue with existing logic... let sequence; if (initialRole === "main") { sequence = ["main", "fallback", "research"]; diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 3f1e1ec2..5895502b 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -3,69 +3,69 @@ * User interface functions for the Task Master CLI */ -import chalk from 'chalk'; -import figlet from 'figlet'; -import boxen from 'boxen'; -import ora from 'ora'; -import Table from 'cli-table3'; -import gradient from 'gradient-string'; +import chalk from "chalk"; +import figlet from "figlet"; +import boxen from "boxen"; +import ora from "ora"; +import Table from "cli-table3"; +import gradient from "gradient-string"; import { - log, - findTaskById, - readJSON, - truncate, - isSilentMode -} from './utils.js'; -import fs from 'fs'; + log, + findTaskById, + readJSON, + truncate, + isSilentMode, +} from "./utils.js"; +import fs from "fs"; import { - findNextTask, - analyzeTaskComplexity, - readComplexityReport -} from './task-manager.js'; -import { getProjectName, getDefaultSubtasks } from './config-manager.js'; -import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; -import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; + findNextTask, + analyzeTaskComplexity, + readComplexityReport, +} from "./task-manager.js"; +import { getProjectName, getDefaultSubtasks } from "./config-manager.js"; +import { TASK_STATUS_OPTIONS } from "../../src/constants/task-status.js"; +import { getTaskMasterVersion } from "../../src/utils/getVersion.js"; // Create a color gradient for the banner -const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']); -const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']); +const coolGradient = gradient(["#00b4d8", "#0077b6", "#03045e"]); +const warmGradient = gradient(["#fb8b24", "#e36414", "#9a031e"]); /** * Display a fancy banner for the CLI */ function displayBanner() { - if (isSilentMode()) return; + if (isSilentMode()) return; - console.clear(); - const bannerText = figlet.textSync('Task Master', { - font: 'Standard', - horizontalLayout: 'default', - verticalLayout: 'default' - }); + console.clear(); + const bannerText = figlet.textSync("Task Master", { + font: "Standard", + horizontalLayout: "default", + verticalLayout: "default", + }); - console.log(coolGradient(bannerText)); + console.log(coolGradient(bannerText)); - // Add creator credit line below the banner - console.log( - chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano') - ); + // Add creator credit line below the banner + console.log( + chalk.dim("by ") + chalk.cyan.underline("https://x.com/eyaltoledano") + ); - // Read version directly from package.json - const version = getTaskMasterVersion(); + // Read version directly from package.json + const version = getTaskMasterVersion(); - console.log( - boxen( - chalk.white( - `${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${getProjectName(null)}` - ), - { - padding: 1, - margin: { top: 0, bottom: 1 }, - borderStyle: 'round', - borderColor: 'cyan' - } - ) - ); + console.log( + boxen( + chalk.white( + `${chalk.bold("Version:")} ${version} ${chalk.bold("Project:")} ${getProjectName(null)}` + ), + { + padding: 1, + margin: { top: 0, bottom: 1 }, + borderStyle: "round", + borderColor: "cyan", + } + ) + ); } /** @@ -74,12 +74,12 @@ function displayBanner() { * @returns {Object} Spinner object */ function startLoadingIndicator(message) { - const spinner = ora({ - text: message, - color: 'cyan' - }).start(); + const spinner = ora({ + text: message, + color: "cyan", + }).start(); - return spinner; + return spinner; } /** @@ -87,9 +87,9 @@ function startLoadingIndicator(message) { * @param {Object} spinner - Spinner object to stop */ function stopLoadingIndicator(spinner) { - if (spinner && spinner.stop) { - spinner.stop(); - } + if (spinner && spinner.stop) { + spinner.stop(); + } } /** @@ -100,120 +100,120 @@ function stopLoadingIndicator(spinner) { * @returns {string} The formatted progress bar */ function createProgressBar(percent, length = 30, statusBreakdown = null) { - // Adjust the percent to treat deferred and cancelled as complete - const effectivePercent = statusBreakdown - ? Math.min( - 100, - percent + - (statusBreakdown.deferred || 0) + - (statusBreakdown.cancelled || 0) - ) - : percent; + // Adjust the percent to treat deferred and cancelled as complete + const effectivePercent = statusBreakdown + ? Math.min( + 100, + percent + + (statusBreakdown.deferred || 0) + + (statusBreakdown.cancelled || 0) + ) + : percent; - // Calculate how many characters to fill for "true completion" - const trueCompletedFilled = Math.round((percent * length) / 100); + // Calculate how many characters to fill for "true completion" + const trueCompletedFilled = Math.round((percent * length) / 100); - // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) - const effectiveCompletedFilled = Math.round( - (effectivePercent * length) / 100 - ); + // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) + const effectiveCompletedFilled = Math.round( + (effectivePercent * length) / 100 + ); - // The "deferred/cancelled" section (difference between true and effective) - const deferredCancelledFilled = - effectiveCompletedFilled - trueCompletedFilled; + // The "deferred/cancelled" section (difference between true and effective) + const deferredCancelledFilled = + effectiveCompletedFilled - trueCompletedFilled; - // Set the empty section (remaining after effective completion) - const empty = length - effectiveCompletedFilled; + // Set the empty section (remaining after effective completion) + const empty = length - effectiveCompletedFilled; - // Determine color based on percentage for the completed section - let completedColor; - if (percent < 25) { - completedColor = chalk.red; - } else if (percent < 50) { - completedColor = chalk.hex('#FFA500'); // Orange - } else if (percent < 75) { - completedColor = chalk.yellow; - } else if (percent < 100) { - completedColor = chalk.green; - } else { - completedColor = chalk.hex('#006400'); // Dark green - } + // Determine color based on percentage for the completed section + let completedColor; + if (percent < 25) { + completedColor = chalk.red; + } else if (percent < 50) { + completedColor = chalk.hex("#FFA500"); // Orange + } else if (percent < 75) { + completedColor = chalk.yellow; + } else if (percent < 100) { + completedColor = chalk.green; + } else { + completedColor = chalk.hex("#006400"); // Dark green + } - // Create colored sections - const completedSection = completedColor('█'.repeat(trueCompletedFilled)); + // Create colored sections + const completedSection = completedColor("█".repeat(trueCompletedFilled)); - // Gray section for deferred/cancelled items - const deferredCancelledSection = chalk.gray( - '█'.repeat(deferredCancelledFilled) - ); + // Gray section for deferred/cancelled items + const deferredCancelledSection = chalk.gray( + "█".repeat(deferredCancelledFilled) + ); - // If we have a status breakdown, create a multi-colored remaining section - let remainingSection = ''; + // If we have a status breakdown, create a multi-colored remaining section + let remainingSection = ""; - if (statusBreakdown && empty > 0) { - // Status colors (matching the statusConfig colors in getStatusWithColor) - const statusColors = { - pending: chalk.yellow, - 'in-progress': chalk.hex('#FFA500'), // Orange - blocked: chalk.red, - review: chalk.magenta - // Deferred and cancelled are treated as part of the completed section - }; + if (statusBreakdown && empty > 0) { + // Status colors (matching the statusConfig colors in getStatusWithColor) + const statusColors = { + pending: chalk.yellow, + "in-progress": chalk.hex("#FFA500"), // Orange + blocked: chalk.red, + review: chalk.magenta, + // Deferred and cancelled are treated as part of the completed section + }; - // Calculate proportions for each status - const totalRemaining = Object.entries(statusBreakdown) - .filter( - ([status]) => - !['deferred', 'cancelled', 'done', 'completed'].includes(status) - ) - .reduce((sum, [_, val]) => sum + val, 0); + // Calculate proportions for each status + const totalRemaining = Object.entries(statusBreakdown) + .filter( + ([status]) => + !["deferred", "cancelled", "done", "completed"].includes(status) + ) + .reduce((sum, [_, val]) => sum + val, 0); - // If no remaining tasks with tracked statuses, just use gray - if (totalRemaining <= 0) { - remainingSection = chalk.gray('░'.repeat(empty)); - } else { - // Track how many characters we've added - let addedChars = 0; + // If no remaining tasks with tracked statuses, just use gray + if (totalRemaining <= 0) { + remainingSection = chalk.gray("░".repeat(empty)); + } else { + // Track how many characters we've added + let addedChars = 0; - // Add each status section proportionally - for (const [status, percentage] of Object.entries(statusBreakdown)) { - // Skip statuses that are considered complete - if (['deferred', 'cancelled', 'done', 'completed'].includes(status)) - continue; + // Add each status section proportionally + for (const [status, percentage] of Object.entries(statusBreakdown)) { + // Skip statuses that are considered complete + if (["deferred", "cancelled", "done", "completed"].includes(status)) + continue; - // Calculate how many characters this status should fill - const statusChars = Math.round((percentage / totalRemaining) * empty); + // Calculate how many characters this status should fill + const statusChars = Math.round((percentage / totalRemaining) * empty); - // Make sure we don't exceed the total length due to rounding - const actualChars = Math.min(statusChars, empty - addedChars); + // Make sure we don't exceed the total length due to rounding + const actualChars = Math.min(statusChars, empty - addedChars); - // Add colored section for this status - const colorFn = statusColors[status] || chalk.gray; - remainingSection += colorFn('░'.repeat(actualChars)); + // Add colored section for this status + const colorFn = statusColors[status] || chalk.gray; + remainingSection += colorFn("░".repeat(actualChars)); - addedChars += actualChars; - } + addedChars += actualChars; + } - // If we have any remaining space due to rounding, fill with gray - if (addedChars < empty) { - remainingSection += chalk.gray('░'.repeat(empty - addedChars)); - } - } - } else { - // Default to gray for the empty section if no breakdown provided - remainingSection = chalk.gray('░'.repeat(empty)); - } + // If we have any remaining space due to rounding, fill with gray + if (addedChars < empty) { + remainingSection += chalk.gray("░".repeat(empty - addedChars)); + } + } + } else { + // Default to gray for the empty section if no breakdown provided + remainingSection = chalk.gray("░".repeat(empty)); + } - // Effective percentage text color should reflect the highest category - const percentTextColor = - percent === 100 - ? chalk.hex('#006400') // Dark green for 100% - : effectivePercent === 100 - ? chalk.gray // Gray for 100% with deferred/cancelled - : completedColor; // Otherwise match the completed color + // Effective percentage text color should reflect the highest category + const percentTextColor = + percent === 100 + ? chalk.hex("#006400") // Dark green for 100% + : effectivePercent === 100 + ? chalk.gray // Gray for 100% with deferred/cancelled + : completedColor; // Otherwise match the completed color - // Build the complete progress bar - return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; + // Build the complete progress bar + return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; } /** @@ -223,44 +223,44 @@ function createProgressBar(percent, length = 30, statusBreakdown = null) { * @returns {string} Colored status string */ function getStatusWithColor(status, forTable = false) { - if (!status) { - return chalk.gray('❓ unknown'); - } + if (!status) { + return chalk.gray("❓ unknown"); + } - const statusConfig = { - done: { color: chalk.green, icon: '✅', tableIcon: '✓' }, - completed: { color: chalk.green, icon: '✅', tableIcon: '✓' }, - pending: { color: chalk.yellow, icon: '⏱️', tableIcon: '⏱' }, - 'in-progress': { color: chalk.hex('#FFA500'), icon: '🔄', tableIcon: '►' }, - deferred: { color: chalk.gray, icon: '⏱️', tableIcon: '⏱' }, - blocked: { color: chalk.red, icon: '❌', tableIcon: '✗' }, - review: { color: chalk.magenta, icon: '👀', tableIcon: '👁' }, - cancelled: { color: chalk.gray, icon: '❌', tableIcon: '✗' } - }; + const statusConfig = { + done: { color: chalk.green, icon: "✅", tableIcon: "✓" }, + completed: { color: chalk.green, icon: "✅", tableIcon: "✓" }, + pending: { color: chalk.yellow, icon: "⏱️", tableIcon: "⏱" }, + "in-progress": { color: chalk.hex("#FFA500"), icon: "🔄", tableIcon: "►" }, + deferred: { color: chalk.gray, icon: "⏱️", tableIcon: "⏱" }, + blocked: { color: chalk.red, icon: "❌", tableIcon: "✗" }, + review: { color: chalk.magenta, icon: "👀", tableIcon: "👁" }, + cancelled: { color: chalk.gray, icon: "❌", tableIcon: "✗" }, + }; - const config = statusConfig[status.toLowerCase()] || { - color: chalk.red, - icon: '❌', - tableIcon: '✗' - }; + const config = statusConfig[status.toLowerCase()] || { + color: chalk.red, + icon: "❌", + tableIcon: "✗", + }; - // Use simpler icons for table display to prevent border issues - if (forTable) { - // Use ASCII characters instead of Unicode for completely stable display - const simpleIcons = { - done: '✓', - completed: '✓', - pending: '○', - 'in-progress': '►', - deferred: 'x', - blocked: '!', // Using plain x character for better compatibility - review: '?' // Using circled dot symbol - }; - const simpleIcon = simpleIcons[status.toLowerCase()] || 'x'; - return config.color(`${simpleIcon} ${status}`); - } + // Use simpler icons for table display to prevent border issues + if (forTable) { + // Use ASCII characters instead of Unicode for completely stable display + const simpleIcons = { + done: "✓", + completed: "✓", + pending: "○", + "in-progress": "►", + deferred: "x", + blocked: "!", // Using plain x character for better compatibility + review: "?", // Using circled dot symbol + }; + const simpleIcon = simpleIcons[status.toLowerCase()] || "x"; + return config.color(`${simpleIcon} ${status}`); + } - return config.color(`${config.icon} ${status}`); + return config.color(`${config.icon} ${status}`); } /** @@ -272,472 +272,472 @@ function getStatusWithColor(status, forTable = false) { * @returns {string} Formatted dependencies string */ function formatDependenciesWithStatus( - dependencies, - allTasks, - forConsole = false, - complexityReport = null // Add complexityReport parameter + dependencies, + allTasks, + forConsole = false, + complexityReport = null // Add complexityReport parameter ) { - if ( - !dependencies || - !Array.isArray(dependencies) || - dependencies.length === 0 - ) { - return forConsole ? chalk.gray('None') : 'None'; - } + if ( + !dependencies || + !Array.isArray(dependencies) || + dependencies.length === 0 + ) { + return forConsole ? chalk.gray("None") : "None"; + } - const formattedDeps = dependencies.map((depId) => { - const depIdStr = depId.toString(); // Ensure string format for display + const formattedDeps = dependencies.map((depId) => { + const depIdStr = depId.toString(); // Ensure string format for display - // Check if it's already a fully qualified subtask ID (like "22.1") - if (depIdStr.includes('.')) { - const [parentId, subtaskId] = depIdStr - .split('.') - .map((id) => parseInt(id, 10)); + // Check if it's already a fully qualified subtask ID (like "22.1") + if (depIdStr.includes(".")) { + const [parentId, subtaskId] = depIdStr + .split(".") + .map((id) => parseInt(id, 10)); - // Find the parent task - const parentTask = allTasks.find((t) => t.id === parentId); - if (!parentTask || !parentTask.subtasks) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + // Find the parent task + const parentTask = allTasks.find((t) => t.id === parentId); + if (!parentTask || !parentTask.subtasks) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Find the subtask - const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); - if (!subtask) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + // Find the subtask + const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); + if (!subtask) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Format with status - const status = subtask.status || 'pending'; - const isDone = - status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; - const isInProgress = status.toLowerCase() === 'in-progress'; + // Format with status + const status = subtask.status || "pending"; + const isDone = + status.toLowerCase() === "done" || status.toLowerCase() === "completed"; + const isInProgress = status.toLowerCase() === "in-progress"; - if (forConsole) { - if (isDone) { - return chalk.green.bold(depIdStr); - } else if (isInProgress) { - return chalk.hex('#FFA500').bold(depIdStr); - } else { - return chalk.red.bold(depIdStr); - } - } + if (forConsole) { + if (isDone) { + return chalk.green.bold(depIdStr); + } else if (isInProgress) { + return chalk.hex("#FFA500").bold(depIdStr); + } else { + return chalk.red.bold(depIdStr); + } + } - // For plain text output (task files), return just the ID without any formatting or emoji - return depIdStr; - } + // For plain text output (task files), return just the ID without any formatting or emoji + return depIdStr; + } - // If depId is a number less than 100, it's likely a reference to a subtask ID in the current task - // This case is typically handled elsewhere (in task-specific code) before calling this function + // If depId is a number less than 100, it's likely a reference to a subtask ID in the current task + // This case is typically handled elsewhere (in task-specific code) before calling this function - // For regular task dependencies (not subtasks) - // Convert string depId to number if needed - const numericDepId = - typeof depId === 'string' ? parseInt(depId, 10) : depId; + // For regular task dependencies (not subtasks) + // Convert string depId to number if needed + const numericDepId = + typeof depId === "string" ? parseInt(depId, 10) : depId; - // Look up the task using the numeric ID - const depTaskResult = findTaskById( - allTasks, - numericDepId, - complexityReport - ); - const depTask = depTaskResult.task; // Access the task object from the result + // Look up the task using the numeric ID + const depTaskResult = findTaskById( + allTasks, + numericDepId, + complexityReport + ); + const depTask = depTaskResult.task; // Access the task object from the result - if (!depTask) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + if (!depTask) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Format with status - const status = depTask.status || 'pending'; - const isDone = - status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; - const isInProgress = status.toLowerCase() === 'in-progress'; + // Format with status + const status = depTask.status || "pending"; + const isDone = + status.toLowerCase() === "done" || status.toLowerCase() === "completed"; + const isInProgress = status.toLowerCase() === "in-progress"; - if (forConsole) { - if (isDone) { - return chalk.green.bold(depIdStr); - } else if (isInProgress) { - return chalk.yellow.bold(depIdStr); - } else { - return chalk.red.bold(depIdStr); - } - } + if (forConsole) { + if (isDone) { + return chalk.green.bold(depIdStr); + } else if (isInProgress) { + return chalk.yellow.bold(depIdStr); + } else { + return chalk.red.bold(depIdStr); + } + } - // For plain text output (task files), return just the ID without any formatting or emoji - return depIdStr; - }); + // For plain text output (task files), return just the ID without any formatting or emoji + return depIdStr; + }); - return formattedDeps.join(', '); + return formattedDeps.join(", "); } /** * Display a comprehensive help guide */ function displayHelp() { - displayBanner(); + displayBanner(); - // Get terminal width - moved to top of function to make it available throughout - const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect + // Get terminal width - moved to top of function to make it available throughout + const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect - console.log( - boxen(chalk.white.bold('Task Master CLI'), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); + console.log( + boxen(chalk.white.bold("Task Master CLI"), { + padding: 1, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + }) + ); - // Command categories - const commandCategories = [ - { - title: 'Project Setup & Configuration', - color: 'blue', - commands: [ - { - name: 'init', - args: '[--name=] [--description=] [-y]', - desc: 'Initialize a new project with Task Master structure' - }, - { - name: 'models', - args: '', - desc: 'View current AI model configuration and available models' - }, - { - name: 'models --setup', - args: '', - desc: 'Run interactive setup to configure AI models' - }, - { - name: 'models --set-main', - args: '', - desc: 'Set the primary model for task generation' - }, - { - name: 'models --set-research', - args: '', - desc: 'Set the model for research operations' - }, - { - name: 'models --set-fallback', - args: '', - desc: 'Set the fallback model (optional)' - } - ] - }, - { - title: 'Task Generation', - color: 'cyan', - commands: [ - { - name: 'parse-prd', - args: '--input= [--num-tasks=10]', - desc: 'Generate tasks from a PRD document' - }, - { - name: 'generate', - args: '', - desc: 'Create individual task files from tasks.json' - } - ] - }, - { - title: 'Task Management', - color: 'green', - commands: [ - { - name: 'list', - args: '[--status=] [--with-subtasks]', - desc: 'List all tasks with their status' - }, - { - name: 'set-status', - args: '--id= --status=', - desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})` - }, - { - name: 'update', - args: '--from= --prompt=""', - desc: 'Update multiple tasks based on new requirements' - }, - { - name: 'update-task', - args: '--id= --prompt=""', - desc: 'Update a single specific task with new information' - }, - { - name: 'update-subtask', - args: '--id= --prompt=""', - desc: 'Append additional information to a subtask' - }, - { - name: 'add-task', - args: '--prompt="" [--dependencies=] [--priority=]', - desc: 'Add a new task using AI' - }, - { - name: 'remove-task', - args: '--id= [-y]', - desc: 'Permanently remove a task or subtask' - } - ] - }, - { - title: 'Subtask Management', - color: 'yellow', - commands: [ - { - name: 'add-subtask', - args: '--parent= --title="" [--description="<desc>"]', - desc: 'Add a new subtask to a parent task' - }, - { - name: 'add-subtask', - args: '--parent=<id> --task-id=<id>', - desc: 'Convert an existing task into a subtask' - }, - { - name: 'remove-subtask', - args: '--id=<parentId.subtaskId> [--convert]', - desc: 'Remove a subtask (optionally convert to standalone task)' - }, - { - name: 'clear-subtasks', - args: '--id=<id>', - desc: 'Remove all subtasks from specified tasks' - }, - { - name: 'clear-subtasks --all', - args: '', - desc: 'Remove subtasks from all tasks' - } - ] - }, - { - title: 'Task Analysis & Breakdown', - color: 'magenta', - commands: [ - { - name: 'analyze-complexity', - args: '[--research] [--threshold=5]', - desc: 'Analyze tasks and generate expansion recommendations' - }, - { - name: 'complexity-report', - args: '[--file=<path>]', - desc: 'Display the complexity analysis report' - }, - { - name: 'expand', - args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]', - desc: 'Break down tasks into detailed subtasks' - }, - { - name: 'expand --all', - args: '[--force] [--research]', - desc: 'Expand all pending tasks with subtasks' - }, - { - name: 'research', - args: '"<prompt>" [-i=<task_ids>] [-f=<file_paths>] [-c="<context>"] [--tree] [-s=<save_file>] [-d=<detail_level>]', - desc: 'Perform AI-powered research queries with project context' - } - ] - }, - { - title: 'Task Navigation & Viewing', - color: 'cyan', - commands: [ - { - name: 'next', - args: '', - desc: 'Show the next task to work on based on dependencies' - }, - { - name: 'show', - args: '<id>', - desc: 'Display detailed information about a specific task' - } - ] - }, - { - title: 'Dependency Management', - color: 'blue', - commands: [ - { - name: 'add-dependency', - args: '--id=<id> --depends-on=<id>', - desc: 'Add a dependency to a task' - }, - { - name: 'remove-dependency', - args: '--id=<id> --depends-on=<id>', - desc: 'Remove a dependency from a task' - }, - { - name: 'validate-dependencies', - args: '', - desc: 'Identify invalid dependencies without fixing them' - }, - { - name: 'fix-dependencies', - args: '', - desc: 'Fix invalid dependencies automatically' - } - ] - } - ]; + // Command categories + const commandCategories = [ + { + title: "Project Setup & Configuration", + color: "blue", + commands: [ + { + name: "init", + args: "[--name=<name>] [--description=<desc>] [-y]", + desc: "Initialize a new project with Task Master structure", + }, + { + name: "models", + args: "", + desc: "View current AI model configuration and available models", + }, + { + name: "models --setup", + args: "", + desc: "Run interactive setup to configure AI models", + }, + { + name: "models --set-main", + args: "<model_id>", + desc: "Set the primary model for task generation", + }, + { + name: "models --set-research", + args: "<model_id>", + desc: "Set the model for research operations", + }, + { + name: "models --set-fallback", + args: "<model_id>", + desc: "Set the fallback model (optional)", + }, + ], + }, + { + title: "Task Generation", + color: "cyan", + commands: [ + { + name: "parse-prd", + args: "--input=<file.txt> [--num-tasks=10]", + desc: "Generate tasks from a PRD document", + }, + { + name: "generate", + args: "", + desc: "Create individual task files from tasks.json", + }, + ], + }, + { + title: "Task Management", + color: "green", + commands: [ + { + name: "list", + args: "[--status=<status>] [--with-subtasks]", + desc: "List all tasks with their status", + }, + { + name: "set-status", + args: "--id=<id> --status=<status>", + desc: `Update task status (${TASK_STATUS_OPTIONS.join(", ")})`, + }, + { + name: "update", + args: '--from=<id> --prompt="<context>"', + desc: "Update multiple tasks based on new requirements", + }, + { + name: "update-task", + args: '--id=<id> --prompt="<context>"', + desc: "Update a single specific task with new information", + }, + { + name: "update-subtask", + args: '--id=<parentId.subtaskId> --prompt="<context>"', + desc: "Append additional information to a subtask", + }, + { + name: "add-task", + args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]', + desc: "Add a new task using AI", + }, + { + name: "remove-task", + args: "--id=<id> [-y]", + desc: "Permanently remove a task or subtask", + }, + ], + }, + { + title: "Subtask Management", + color: "yellow", + commands: [ + { + name: "add-subtask", + args: '--parent=<id> --title="<title>" [--description="<desc>"]', + desc: "Add a new subtask to a parent task", + }, + { + name: "add-subtask", + args: "--parent=<id> --task-id=<id>", + desc: "Convert an existing task into a subtask", + }, + { + name: "remove-subtask", + args: "--id=<parentId.subtaskId> [--convert]", + desc: "Remove a subtask (optionally convert to standalone task)", + }, + { + name: "clear-subtasks", + args: "--id=<id>", + desc: "Remove all subtasks from specified tasks", + }, + { + name: "clear-subtasks --all", + args: "", + desc: "Remove subtasks from all tasks", + }, + ], + }, + { + title: "Task Analysis & Breakdown", + color: "magenta", + commands: [ + { + name: "analyze-complexity", + args: "[--research] [--threshold=5]", + desc: "Analyze tasks and generate expansion recommendations", + }, + { + name: "complexity-report", + args: "[--file=<path>]", + desc: "Display the complexity analysis report", + }, + { + name: "expand", + args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]', + desc: "Break down tasks into detailed subtasks", + }, + { + name: "expand --all", + args: "[--force] [--research]", + desc: "Expand all pending tasks with subtasks", + }, + { + name: "research", + args: '"<prompt>" [-i=<task_ids>] [-f=<file_paths>] [-c="<context>"] [--tree] [-s=<save_file>] [-d=<detail_level>]', + desc: "Perform AI-powered research queries with project context", + }, + ], + }, + { + title: "Task Navigation & Viewing", + color: "cyan", + commands: [ + { + name: "next", + args: "", + desc: "Show the next task to work on based on dependencies", + }, + { + name: "show", + args: "<id>", + desc: "Display detailed information about a specific task", + }, + ], + }, + { + title: "Dependency Management", + color: "blue", + commands: [ + { + name: "add-dependency", + args: "--id=<id> --depends-on=<id>", + desc: "Add a dependency to a task", + }, + { + name: "remove-dependency", + args: "--id=<id> --depends-on=<id>", + desc: "Remove a dependency from a task", + }, + { + name: "validate-dependencies", + args: "", + desc: "Identify invalid dependencies without fixing them", + }, + { + name: "fix-dependencies", + args: "", + desc: "Fix invalid dependencies automatically", + }, + ], + }, + ]; - // Display each category - commandCategories.forEach((category) => { - console.log( - boxen(chalk[category.color].bold(category.title), { - padding: { left: 2, right: 2, top: 0, bottom: 0 }, - margin: { top: 1, bottom: 0 }, - borderColor: category.color, - borderStyle: 'round' - }) - ); + // Display each category + commandCategories.forEach((category) => { + console.log( + boxen(chalk[category.color].bold(category.title), { + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 0 }, + borderColor: category.color, + borderStyle: "round", + }) + ); - // Calculate dynamic column widths - adjust ratios as needed - const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25 - const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40 - const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer + // Calculate dynamic column widths - adjust ratios as needed + const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25 + const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40 + const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer - const commandTable = new Table({ - colWidths: [nameWidth, argsWidth, descWidth], - chars: { - top: '', - 'top-mid': '', - 'top-left': '', - 'top-right': '', - bottom: '', - 'bottom-mid': '', - 'bottom-left': '', - 'bottom-right': '', - left: '', - 'left-mid': '', - mid: '', - 'mid-mid': '', - right: '', - 'right-mid': '', - middle: ' ' - }, - style: { border: [], 'padding-left': 4 }, - wordWrap: true - }); + const commandTable = new Table({ + colWidths: [nameWidth, argsWidth, descWidth], + chars: { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + mid: "", + "mid-mid": "", + right: "", + "right-mid": "", + middle: " ", + }, + style: { border: [], "padding-left": 4 }, + wordWrap: true, + }); - category.commands.forEach((cmd, index) => { - commandTable.push([ - `${chalk.yellow.bold(cmd.name)}${chalk.reset('')}`, - `${chalk.white(cmd.args)}${chalk.reset('')}`, - `${chalk.dim(cmd.desc)}${chalk.reset('')}` - ]); - }); + category.commands.forEach((cmd, index) => { + commandTable.push([ + `${chalk.yellow.bold(cmd.name)}${chalk.reset("")}`, + `${chalk.white(cmd.args)}${chalk.reset("")}`, + `${chalk.dim(cmd.desc)}${chalk.reset("")}`, + ]); + }); - console.log(commandTable.toString()); - console.log(''); - }); + console.log(commandTable.toString()); + console.log(""); + }); - // Display configuration section - console.log( - boxen(chalk.cyan.bold('Configuration'), { - padding: { left: 2, right: 2, top: 0, bottom: 0 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'cyan', - borderStyle: 'round' - }) - ); + // Display configuration section + console.log( + boxen(chalk.cyan.bold("Configuration"), { + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 0 }, + borderColor: "cyan", + borderStyle: "round", + }) + ); - // Get terminal width if not already defined - const configTerminalWidth = terminalWidth || process.stdout.columns || 100; + // Get terminal width if not already defined + const configTerminalWidth = terminalWidth || process.stdout.columns || 100; - // Calculate dynamic column widths for config table - const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25)); - const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45)); - const configValueWidth = Math.max( - 30, - Math.floor(configTerminalWidth * 0.3) - 10 - ); + // Calculate dynamic column widths for config table + const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25)); + const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45)); + const configValueWidth = Math.max( + 30, + Math.floor(configTerminalWidth * 0.3) - 10 + ); - const configTable = new Table({ - colWidths: [configKeyWidth, configDescWidth, configValueWidth], - chars: { - top: '', - 'top-mid': '', - 'top-left': '', - 'top-right': '', - bottom: '', - 'bottom-mid': '', - 'bottom-left': '', - 'bottom-right': '', - left: '', - 'left-mid': '', - mid: '', - 'mid-mid': '', - right: '', - 'right-mid': '', - middle: ' ' - }, - style: { border: [], 'padding-left': 4 }, - wordWrap: true - }); + const configTable = new Table({ + colWidths: [configKeyWidth, configDescWidth, configValueWidth], + chars: { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + mid: "", + "mid-mid": "", + right: "", + "right-mid": "", + middle: " ", + }, + style: { border: [], "padding-left": 4 }, + wordWrap: true, + }); - configTable.push( - [ - `${chalk.yellow('.taskmasterconfig')}${chalk.reset('')}`, - `${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`, - `${chalk.dim('Managed by models cmd')}${chalk.reset('')}` - ], - [ - `${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`, - `${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`, - `${chalk.dim('Required in .env file')}${chalk.reset('')}` - ], - [ - `${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`, - `${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`, - `${chalk.dim('Required in .cursor/')}${chalk.reset('')}` - ] - ); + configTable.push( + [ + `${chalk.yellow(".taskmasterconfig")}${chalk.reset("")}`, + `${chalk.white("AI model configuration file (project root)")}${chalk.reset("")}`, + `${chalk.dim("Managed by models cmd")}${chalk.reset("")}`, + ], + [ + `${chalk.yellow("API Keys (.env)")}${chalk.reset("")}`, + `${chalk.white("API keys for AI providers (ANTHROPIC_API_KEY, etc.)")}${chalk.reset("")}`, + `${chalk.dim("Required in .env file")}${chalk.reset("")}`, + ], + [ + `${chalk.yellow("MCP Keys (mcp.json)")}${chalk.reset("")}`, + `${chalk.white("API keys for Cursor integration")}${chalk.reset("")}`, + `${chalk.dim("Required in .cursor/")}${chalk.reset("")}`, + ] + ); - console.log(configTable.toString()); - console.log(''); + console.log(configTable.toString()); + console.log(""); - // Show helpful hints - console.log( - boxen( - chalk.white.bold('Quick Start:') + - '\n\n' + - chalk.cyan('1. Create Project: ') + - chalk.white('task-master init') + - '\n' + - chalk.cyan('2. Setup Models: ') + - chalk.white('task-master models --setup') + - '\n' + - chalk.cyan('3. Parse PRD: ') + - chalk.white('task-master parse-prd --input=<prd-file>') + - '\n' + - chalk.cyan('4. List Tasks: ') + - chalk.white('task-master list') + - '\n' + - chalk.cyan('5. Find Next Task: ') + - chalk.white('task-master next'), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 }, - width: Math.min(configTerminalWidth - 10, 100) // Limit width to terminal width minus padding, max 100 - } - ) - ); + // Show helpful hints + console.log( + boxen( + chalk.white.bold("Quick Start:") + + "\n\n" + + chalk.cyan("1. Create Project: ") + + chalk.white("task-master init") + + "\n" + + chalk.cyan("2. Setup Models: ") + + chalk.white("task-master models --setup") + + "\n" + + chalk.cyan("3. Parse PRD: ") + + chalk.white("task-master parse-prd --input=<prd-file>") + + "\n" + + chalk.cyan("4. List Tasks: ") + + chalk.white("task-master list") + + "\n" + + chalk.cyan("5. Find Next Task: ") + + chalk.white("task-master next"), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + width: Math.min(configTerminalWidth - 10, 100), // Limit width to terminal width minus padding, max 100 + } + ) + ); } /** @@ -746,9 +746,9 @@ function displayHelp() { * @returns {string} Colored complexity score */ function getComplexityWithColor(score) { - if (score <= 3) return chalk.green(`🟢 ${score}`); - if (score <= 6) return chalk.yellow(`🟡 ${score}`); - return chalk.red(`🔴 ${score}`); + if (score <= 3) return chalk.green(`🟢 ${score}`); + if (score <= 6) return chalk.yellow(`🟡 ${score}`); + return chalk.red(`🔴 ${score}`); } /** @@ -758,9 +758,9 @@ function getComplexityWithColor(score) { * @returns {string} Truncated string */ function truncateString(str, maxLength) { - if (!str) return ''; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength - 3) + '...'; + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength - 3) + "..."; } /** @@ -768,264 +768,264 @@ function truncateString(str, maxLength) { * @param {string} tasksPath - Path to the tasks.json file */ async function displayNextTask(tasksPath, complexityReportPath = null) { - displayBanner(); + displayBanner(); - // Read the tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + // Read the tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Read complexity report once - const complexityReport = readComplexityReport(complexityReportPath); + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); - // Find the next task - const nextTask = findNextTask(data.tasks, complexityReport); + // Find the next task + const nextTask = findNextTask(data.tasks, complexityReport); - if (!nextTask) { - console.log( - boxen( - chalk.yellow('No eligible tasks found!\n\n') + - 'All pending tasks have unsatisfied dependencies, or all tasks are completed.', - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - return; - } + if (!nextTask) { + console.log( + boxen( + chalk.yellow("No eligible tasks found!\n\n") + + "All pending tasks have unsatisfied dependencies, or all tasks are completed.", + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + return; + } - // Display the task in a nice format - console.log( - boxen(chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - }) - ); + // Display the task in a nice format + console.log( + boxen(chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + }) + ); - // Create a table with task details - const taskTable = new Table({ - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); + // Create a table with task details + const taskTable = new Table({ + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); - // Priority with color - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; - const priorityColor = - priorityColors[nextTask.priority || 'medium'] || chalk.white; + // Priority with color + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + low: chalk.gray, + }; + const priorityColor = + priorityColors[nextTask.priority || "medium"] || chalk.white; - // Add task details to table - taskTable.push( - [chalk.cyan.bold('ID:'), nextTask.id.toString()], - [chalk.cyan.bold('Title:'), nextTask.title], - [ - chalk.cyan.bold('Priority:'), - priorityColor(nextTask.priority || 'medium') - ], - [ - chalk.cyan.bold('Dependencies:'), - formatDependenciesWithStatus( - nextTask.dependencies, - data.tasks, - true, - complexityReport - ) - ], - [ - chalk.cyan.bold('Complexity:'), - nextTask.complexityScore - ? getComplexityWithColor(nextTask.complexityScore) - : chalk.gray('N/A') - ], - [chalk.cyan.bold('Description:'), nextTask.description] - ); + // Add task details to table + taskTable.push( + [chalk.cyan.bold("ID:"), nextTask.id.toString()], + [chalk.cyan.bold("Title:"), nextTask.title], + [ + chalk.cyan.bold("Priority:"), + priorityColor(nextTask.priority || "medium"), + ], + [ + chalk.cyan.bold("Dependencies:"), + formatDependenciesWithStatus( + nextTask.dependencies, + data.tasks, + true, + complexityReport + ), + ], + [ + chalk.cyan.bold("Complexity:"), + nextTask.complexityScore + ? getComplexityWithColor(nextTask.complexityScore) + : chalk.gray("N/A"), + ], + [chalk.cyan.bold("Description:"), nextTask.description] + ); - console.log(taskTable.toString()); + console.log(taskTable.toString()); - // If task has details, show them in a separate box - if (nextTask.details && nextTask.details.trim().length > 0) { - console.log( - boxen( - chalk.white.bold('Implementation Details:') + '\n\n' + nextTask.details, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); - } + // If task has details, show them in a separate box + if (nextTask.details && nextTask.details.trim().length > 0) { + console.log( + boxen( + chalk.white.bold("Implementation Details:") + "\n\n" + nextTask.details, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); + } - // Determine if the nextTask is a subtask - const isSubtask = !!nextTask.parentId; + // Determine if the nextTask is a subtask + const isSubtask = !!nextTask.parentId; - // Show subtasks if they exist (only for parent tasks) - if (!isSubtask && nextTask.subtasks && nextTask.subtasks.length > 0) { - console.log( - boxen(chalk.white.bold('Subtasks'), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'magenta', - borderStyle: 'round' - }) - ); + // Show subtasks if they exist (only for parent tasks) + if (!isSubtask && nextTask.subtasks && nextTask.subtasks.length > 0) { + console.log( + boxen(chalk.white.bold("Subtasks"), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "magenta", + borderStyle: "round", + }) + ); - // Calculate available width for the subtask table - const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect + // Calculate available width for the subtask table + const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect - // Define percentage-based column widths - const idWidthPct = 8; - const statusWidthPct = 15; - const depsWidthPct = 25; - const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; + // Define percentage-based column widths + const idWidthPct = 8; + const statusWidthPct = 15; + const depsWidthPct = 25; + const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; - // Calculate actual column widths - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + // Calculate actual column widths + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - // Create a table for subtasks with improved handling - const subtaskTable = new Table({ - head: [ - chalk.magenta.bold('ID'), - chalk.magenta.bold('Status'), - chalk.magenta.bold('Title'), - chalk.magenta.bold('Deps') - ], - colWidths: [idWidth, statusWidth, titleWidth, depsWidth], - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - wordWrap: true - }); + // Create a table for subtasks with improved handling + const subtaskTable = new Table({ + head: [ + chalk.magenta.bold("ID"), + chalk.magenta.bold("Status"), + chalk.magenta.bold("Title"), + chalk.magenta.bold("Deps"), + ], + colWidths: [idWidth, statusWidth, titleWidth, depsWidth], + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + wordWrap: true, + }); - // Add subtasks to table - nextTask.subtasks.forEach((st) => { - const statusColor = - { - done: chalk.green, - completed: chalk.green, - pending: chalk.yellow, - 'in-progress': chalk.blue - }[st.status || 'pending'] || chalk.white; + // Add subtasks to table + nextTask.subtasks.forEach((st) => { + const statusColor = + { + done: chalk.green, + completed: chalk.green, + pending: chalk.yellow, + "in-progress": chalk.blue, + }[st.status || "pending"] || chalk.white; - // Format subtask dependencies - let subtaskDeps = 'None'; - if (st.dependencies && st.dependencies.length > 0) { - // Format dependencies with correct notation - const formattedDeps = st.dependencies.map((depId) => { - if (typeof depId === 'number' && depId < 100) { - const foundSubtask = nextTask.subtasks.find( - (st) => st.id === depId - ); - if (foundSubtask) { - const isDone = - foundSubtask.status === 'done' || - foundSubtask.status === 'completed'; - const isInProgress = foundSubtask.status === 'in-progress'; + // Format subtask dependencies + let subtaskDeps = "None"; + if (st.dependencies && st.dependencies.length > 0) { + // Format dependencies with correct notation + const formattedDeps = st.dependencies.map((depId) => { + if (typeof depId === "number" && depId < 100) { + const foundSubtask = nextTask.subtasks.find( + (st) => st.id === depId + ); + if (foundSubtask) { + const isDone = + foundSubtask.status === "done" || + foundSubtask.status === "completed"; + const isInProgress = foundSubtask.status === "in-progress"; - // Use consistent color formatting instead of emojis - if (isDone) { - return chalk.green.bold(`${nextTask.id}.${depId}`); - } else if (isInProgress) { - return chalk.hex('#FFA500').bold(`${nextTask.id}.${depId}`); - } else { - return chalk.red.bold(`${nextTask.id}.${depId}`); - } - } - return chalk.red(`${nextTask.id}.${depId} (Not found)`); - } - return depId; - }); + // Use consistent color formatting instead of emojis + if (isDone) { + return chalk.green.bold(`${nextTask.id}.${depId}`); + } else if (isInProgress) { + return chalk.hex("#FFA500").bold(`${nextTask.id}.${depId}`); + } else { + return chalk.red.bold(`${nextTask.id}.${depId}`); + } + } + return chalk.red(`${nextTask.id}.${depId} (Not found)`); + } + return depId; + }); - // Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again - subtaskDeps = - formattedDeps.length === 1 - ? formattedDeps[0] - : formattedDeps.join(chalk.white(', ')); - } + // Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again + subtaskDeps = + formattedDeps.length === 1 + ? formattedDeps[0] + : formattedDeps.join(chalk.white(", ")); + } - subtaskTable.push([ - `${nextTask.id}.${st.id}`, - statusColor(st.status || 'pending'), - st.title, - subtaskDeps - ]); - }); + subtaskTable.push([ + `${nextTask.id}.${st.id}`, + statusColor(st.status || "pending"), + st.title, + subtaskDeps, + ]); + }); - console.log(subtaskTable.toString()); - } + console.log(subtaskTable.toString()); + } - // Suggest expanding if no subtasks (only for parent tasks without subtasks) - if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) { - console.log( - boxen( - chalk.yellow('No subtasks found. Consider breaking down this task:') + - '\n' + - chalk.white( - `Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); - } + // Suggest expanding if no subtasks (only for parent tasks without subtasks) + if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) { + console.log( + boxen( + chalk.yellow("No subtasks found. Consider breaking down this task:") + + "\n" + + chalk.white( + `Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); + } - // Show action suggestions - let suggestedActionsContent = chalk.white.bold('Suggested Actions:') + '\n'; - if (isSubtask) { - // Suggested actions for a subtask - suggestedActionsContent += - `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + - `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + - `${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`; - } else { - // Suggested actions for a parent task - suggestedActionsContent += - `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + - `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + - (nextTask.subtasks && nextTask.subtasks.length > 0 - ? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}` // Example: first subtask - : `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`); - } + // Show action suggestions + let suggestedActionsContent = chalk.white.bold("Suggested Actions:") + "\n"; + if (isSubtask) { + // Suggested actions for a subtask + suggestedActionsContent += + `${chalk.cyan("1.")} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + + `${chalk.cyan("2.")} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + + `${chalk.cyan("3.")} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`; + } else { + // Suggested actions for a parent task + suggestedActionsContent += + `${chalk.cyan("1.")} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + + `${chalk.cyan("2.")} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + + (nextTask.subtasks && nextTask.subtasks.length > 0 + ? `${chalk.cyan("3.")} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}` // Example: first subtask + : `${chalk.cyan("3.")} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`); + } - console.log( - boxen(suggestedActionsContent, { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - }) - ); + console.log( + boxen(suggestedActionsContent, { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + }) + ); } /** @@ -1035,460 +1035,460 @@ async function displayNextTask(tasksPath, complexityReportPath = null) { * @param {string} [statusFilter] - Optional status to filter subtasks by */ async function displayTaskById( - tasksPath, - taskId, - complexityReportPath = null, - statusFilter = null + tasksPath, + taskId, + complexityReportPath = null, + statusFilter = null ) { - displayBanner(); + displayBanner(); - // Read the tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + // Read the tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Read complexity report once - const complexityReport = readComplexityReport(complexityReportPath); + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); - // Find the task by ID, applying the status filter if provided - // Returns { task, originalSubtaskCount, originalSubtasks } - const { task, originalSubtaskCount, originalSubtasks } = findTaskById( - data.tasks, - taskId, - complexityReport, - statusFilter - ); + // Find the task by ID, applying the status filter if provided + // Returns { task, originalSubtaskCount, originalSubtasks } + const { task, originalSubtaskCount, originalSubtasks } = findTaskById( + data.tasks, + taskId, + complexityReport, + statusFilter + ); - if (!task) { - console.log( - boxen(chalk.yellow(`Task with ID ${taskId} not found!`), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - }) - ); - return; - } + if (!task) { + console.log( + boxen(chalk.yellow(`Task with ID ${taskId} not found!`), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + }) + ); + return; + } - // Handle subtask display specially (This logic remains the same) - if (task.isSubtask || task.parentTask) { - console.log( - boxen( - chalk.white.bold( - `Subtask: #${task.parentTask.id}.${task.id} - ${task.title}` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'magenta', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); + // Handle subtask display specially (This logic remains the same) + if (task.isSubtask || task.parentTask) { + console.log( + boxen( + chalk.white.bold( + `Subtask: #${task.parentTask.id}.${task.id} - ${task.title}` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "magenta", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); - const subtaskTable = new Table({ - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); - subtaskTable.push( - [chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`], - [ - chalk.cyan.bold('Parent Task:'), - `#${task.parentTask.id} - ${task.parentTask.title}` - ], - [chalk.cyan.bold('Title:'), task.title], - [ - chalk.cyan.bold('Status:'), - getStatusWithColor(task.status || 'pending', true) - ], - [ - chalk.cyan.bold('Complexity:'), - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray('N/A') - ], - [ - chalk.cyan.bold('Description:'), - task.description || 'No description provided.' - ] - ); - console.log(subtaskTable.toString()); + const subtaskTable = new Table({ + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); + subtaskTable.push( + [chalk.cyan.bold("ID:"), `${task.parentTask.id}.${task.id}`], + [ + chalk.cyan.bold("Parent Task:"), + `#${task.parentTask.id} - ${task.parentTask.title}`, + ], + [chalk.cyan.bold("Title:"), task.title], + [ + chalk.cyan.bold("Status:"), + getStatusWithColor(task.status || "pending", true), + ], + [ + chalk.cyan.bold("Complexity:"), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray("N/A"), + ], + [ + chalk.cyan.bold("Description:"), + task.description || "No description provided.", + ] + ); + console.log(subtaskTable.toString()); - if (task.details && task.details.trim().length > 0) { - console.log( - boxen( - chalk.white.bold('Implementation Details:') + '\n\n' + task.details, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); - } + if (task.details && task.details.trim().length > 0) { + console.log( + boxen( + chalk.white.bold("Implementation Details:") + "\n\n" + task.details, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); + } - console.log( - boxen( - chalk.white.bold('Suggested Actions:') + - '\n' + - `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` + - `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` + - `${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - return; // Exit after displaying subtask details - } + console.log( + boxen( + chalk.white.bold("Suggested Actions:") + + "\n" + + `${chalk.cyan("1.")} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` + + `${chalk.cyan("2.")} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` + + `${chalk.cyan("3.")} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + return; // Exit after displaying subtask details + } - // --- Display Regular Task Details --- - console.log( - boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - }) - ); + // --- Display Regular Task Details --- + console.log( + boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + }) + ); - const taskTable = new Table({ - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; - const priorityColor = - priorityColors[task.priority || 'medium'] || chalk.white; - taskTable.push( - [chalk.cyan.bold('ID:'), task.id.toString()], - [chalk.cyan.bold('Title:'), task.title], - [ - chalk.cyan.bold('Status:'), - getStatusWithColor(task.status || 'pending', true) - ], - [chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')], - [ - chalk.cyan.bold('Dependencies:'), - formatDependenciesWithStatus( - task.dependencies, - data.tasks, - true, - complexityReport - ) - ], - [ - chalk.cyan.bold('Complexity:'), - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray('N/A') - ], - [chalk.cyan.bold('Description:'), task.description] - ); - console.log(taskTable.toString()); + const taskTable = new Table({ + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + low: chalk.gray, + }; + const priorityColor = + priorityColors[task.priority || "medium"] || chalk.white; + taskTable.push( + [chalk.cyan.bold("ID:"), task.id.toString()], + [chalk.cyan.bold("Title:"), task.title], + [ + chalk.cyan.bold("Status:"), + getStatusWithColor(task.status || "pending", true), + ], + [chalk.cyan.bold("Priority:"), priorityColor(task.priority || "medium")], + [ + chalk.cyan.bold("Dependencies:"), + formatDependenciesWithStatus( + task.dependencies, + data.tasks, + true, + complexityReport + ), + ], + [ + chalk.cyan.bold("Complexity:"), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray("N/A"), + ], + [chalk.cyan.bold("Description:"), task.description] + ); + console.log(taskTable.toString()); - if (task.details && task.details.trim().length > 0) { - console.log( - boxen( - chalk.white.bold('Implementation Details:') + '\n\n' + task.details, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); - } - if (task.testStrategy && task.testStrategy.trim().length > 0) { - console.log( - boxen(chalk.white.bold('Test Strategy:') + '\n\n' + task.testStrategy, { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - }) - ); - } + if (task.details && task.details.trim().length > 0) { + console.log( + boxen( + chalk.white.bold("Implementation Details:") + "\n\n" + task.details, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); + } + if (task.testStrategy && task.testStrategy.trim().length > 0) { + console.log( + boxen(chalk.white.bold("Test Strategy:") + "\n\n" + task.testStrategy, { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + }) + ); + } - // --- Subtask Table Display (uses filtered list: task.subtasks) --- - if (task.subtasks && task.subtasks.length > 0) { - console.log( - boxen(chalk.white.bold('Subtasks'), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'magenta', - borderStyle: 'round' - }) - ); + // --- Subtask Table Display (uses filtered list: task.subtasks) --- + if (task.subtasks && task.subtasks.length > 0) { + console.log( + boxen(chalk.white.bold("Subtasks"), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "magenta", + borderStyle: "round", + }) + ); - const availableWidth = process.stdout.columns - 10 || 100; - const idWidthPct = 10; - const statusWidthPct = 15; - const depsWidthPct = 25; - const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + const availableWidth = process.stdout.columns - 10 || 100; + const idWidthPct = 10; + const statusWidthPct = 15; + const depsWidthPct = 25; + const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - const subtaskTable = new Table({ - head: [ - chalk.magenta.bold('ID'), - chalk.magenta.bold('Status'), - chalk.magenta.bold('Title'), - chalk.magenta.bold('Deps') - ], - colWidths: [idWidth, statusWidth, titleWidth, depsWidth], - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - wordWrap: true - }); + const subtaskTable = new Table({ + head: [ + chalk.magenta.bold("ID"), + chalk.magenta.bold("Status"), + chalk.magenta.bold("Title"), + chalk.magenta.bold("Deps"), + ], + colWidths: [idWidth, statusWidth, titleWidth, depsWidth], + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + wordWrap: true, + }); - // Populate table with the potentially filtered subtasks - task.subtasks.forEach((st) => { - const statusColorMap = { - done: chalk.green, - completed: chalk.green, - pending: chalk.yellow, - 'in-progress': chalk.blue - }; - const statusColor = statusColorMap[st.status || 'pending'] || chalk.white; - let subtaskDeps = 'None'; - if (st.dependencies && st.dependencies.length > 0) { - const formattedDeps = st.dependencies.map((depId) => { - // Use the original, unfiltered list for dependency status lookup - const sourceListForDeps = originalSubtasks || task.subtasks; - const foundDepSubtask = - typeof depId === 'number' && depId < 100 - ? sourceListForDeps.find((sub) => sub.id === depId) - : null; + // Populate table with the potentially filtered subtasks + task.subtasks.forEach((st) => { + const statusColorMap = { + done: chalk.green, + completed: chalk.green, + pending: chalk.yellow, + "in-progress": chalk.blue, + }; + const statusColor = statusColorMap[st.status || "pending"] || chalk.white; + let subtaskDeps = "None"; + if (st.dependencies && st.dependencies.length > 0) { + const formattedDeps = st.dependencies.map((depId) => { + // Use the original, unfiltered list for dependency status lookup + const sourceListForDeps = originalSubtasks || task.subtasks; + const foundDepSubtask = + typeof depId === "number" && depId < 100 + ? sourceListForDeps.find((sub) => sub.id === depId) + : null; - if (foundDepSubtask) { - const isDone = - foundDepSubtask.status === 'done' || - foundDepSubtask.status === 'completed'; - const isInProgress = foundDepSubtask.status === 'in-progress'; - const color = isDone - ? chalk.green.bold - : isInProgress - ? chalk.hex('#FFA500').bold - : chalk.red.bold; - return color(`${task.id}.${depId}`); - } else if (typeof depId === 'number' && depId < 100) { - return chalk.red(`${task.id}.${depId} (Not found)`); - } - return depId; // Assume it's a top-level task ID if not a number < 100 - }); - subtaskDeps = - formattedDeps.length === 1 - ? formattedDeps[0] - : formattedDeps.join(chalk.white(', ')); - } - subtaskTable.push([ - `${task.id}.${st.id}`, - statusColor(st.status || 'pending'), - st.title, - subtaskDeps - ]); - }); - console.log(subtaskTable.toString()); + if (foundDepSubtask) { + const isDone = + foundDepSubtask.status === "done" || + foundDepSubtask.status === "completed"; + const isInProgress = foundDepSubtask.status === "in-progress"; + const color = isDone + ? chalk.green.bold + : isInProgress + ? chalk.hex("#FFA500").bold + : chalk.red.bold; + return color(`${task.id}.${depId}`); + } else if (typeof depId === "number" && depId < 100) { + return chalk.red(`${task.id}.${depId} (Not found)`); + } + return depId; // Assume it's a top-level task ID if not a number < 100 + }); + subtaskDeps = + formattedDeps.length === 1 + ? formattedDeps[0] + : formattedDeps.join(chalk.white(", ")); + } + subtaskTable.push([ + `${task.id}.${st.id}`, + statusColor(st.status || "pending"), + st.title, + subtaskDeps, + ]); + }); + console.log(subtaskTable.toString()); - // Display filter summary line *immediately after the table* if a filter was applied - if (statusFilter && originalSubtaskCount !== null) { - console.log( - chalk.cyan( - ` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.` - ) - ); - // Add a newline for spacing before the progress bar if the filter line was shown - console.log(); - } - // --- Conditional Messages for No Subtasks Shown --- - } else if (statusFilter && originalSubtaskCount === 0) { - // Case where filter applied, but the parent task had 0 subtasks originally - console.log( - boxen( - chalk.yellow( - `No subtasks found matching status: ${statusFilter} (Task has no subtasks)` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'yellow', - borderStyle: 'round' - } - ) - ); - } else if ( - statusFilter && - originalSubtaskCount > 0 && - task.subtasks.length === 0 - ) { - // Case where filter applied, original subtasks existed, but none matched - console.log( - boxen( - chalk.yellow( - `No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'yellow', - borderStyle: 'round' - } - ) - ); - } else if ( - !statusFilter && - (!originalSubtasks || originalSubtasks.length === 0) - ) { - // Case where NO filter applied AND the task genuinely has no subtasks - // Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks - const actualSubtasks = originalSubtasks || task.subtasks; - if (!actualSubtasks || actualSubtasks.length === 0) { - console.log( - boxen( - chalk.yellow('No subtasks found. Consider breaking down this task:') + - '\n' + - chalk.white( - `Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); - } - } + // Display filter summary line *immediately after the table* if a filter was applied + if (statusFilter && originalSubtaskCount !== null) { + console.log( + chalk.cyan( + ` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.` + ) + ); + // Add a newline for spacing before the progress bar if the filter line was shown + console.log(); + } + // --- Conditional Messages for No Subtasks Shown --- + } else if (statusFilter && originalSubtaskCount === 0) { + // Case where filter applied, but the parent task had 0 subtasks originally + console.log( + boxen( + chalk.yellow( + `No subtasks found matching status: ${statusFilter} (Task has no subtasks)` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "yellow", + borderStyle: "round", + } + ) + ); + } else if ( + statusFilter && + originalSubtaskCount > 0 && + task.subtasks.length === 0 + ) { + // Case where filter applied, original subtasks existed, but none matched + console.log( + boxen( + chalk.yellow( + `No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "yellow", + borderStyle: "round", + } + ) + ); + } else if ( + !statusFilter && + (!originalSubtasks || originalSubtasks.length === 0) + ) { + // Case where NO filter applied AND the task genuinely has no subtasks + // Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks + const actualSubtasks = originalSubtasks || task.subtasks; + if (!actualSubtasks || actualSubtasks.length === 0) { + console.log( + boxen( + chalk.yellow("No subtasks found. Consider breaking down this task:") + + "\n" + + chalk.white( + `Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); + } + } - // --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) --- - // Determine the list to use for progress calculation (always the original if available and filtering happened) - const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks + // --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) --- + // Determine the list to use for progress calculation (always the original if available and filtering happened) + const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks - // Only show progress if there are actually subtasks - if (subtasksForProgress && subtasksForProgress.length > 0) { - const totalSubtasks = subtasksForProgress.length; - const completedSubtasks = subtasksForProgress.filter( - (st) => st.status === 'done' || st.status === 'completed' - ).length; + // Only show progress if there are actually subtasks + if (subtasksForProgress && subtasksForProgress.length > 0) { + const totalSubtasks = subtasksForProgress.length; + const completedSubtasks = subtasksForProgress.filter( + (st) => st.status === "done" || st.status === "completed" + ).length; - // Count other statuses from the original/complete list - const inProgressSubtasks = subtasksForProgress.filter( - (st) => st.status === 'in-progress' - ).length; - const pendingSubtasks = subtasksForProgress.filter( - (st) => st.status === 'pending' - ).length; - const blockedSubtasks = subtasksForProgress.filter( - (st) => st.status === 'blocked' - ).length; - const deferredSubtasks = subtasksForProgress.filter( - (st) => st.status === 'deferred' - ).length; - const cancelledSubtasks = subtasksForProgress.filter( - (st) => st.status === 'cancelled' - ).length; + // Count other statuses from the original/complete list + const inProgressSubtasks = subtasksForProgress.filter( + (st) => st.status === "in-progress" + ).length; + const pendingSubtasks = subtasksForProgress.filter( + (st) => st.status === "pending" + ).length; + const blockedSubtasks = subtasksForProgress.filter( + (st) => st.status === "blocked" + ).length; + const deferredSubtasks = subtasksForProgress.filter( + (st) => st.status === "deferred" + ).length; + const cancelledSubtasks = subtasksForProgress.filter( + (st) => st.status === "cancelled" + ).length; - const statusBreakdown = { - // Calculate breakdown based on the complete list - 'in-progress': (inProgressSubtasks / totalSubtasks) * 100, - pending: (pendingSubtasks / totalSubtasks) * 100, - blocked: (blockedSubtasks / totalSubtasks) * 100, - deferred: (deferredSubtasks / totalSubtasks) * 100, - cancelled: (cancelledSubtasks / totalSubtasks) * 100 - }; - const completionPercentage = (completedSubtasks / totalSubtasks) * 100; + const statusBreakdown = { + // Calculate breakdown based on the complete list + "in-progress": (inProgressSubtasks / totalSubtasks) * 100, + pending: (pendingSubtasks / totalSubtasks) * 100, + blocked: (blockedSubtasks / totalSubtasks) * 100, + deferred: (deferredSubtasks / totalSubtasks) * 100, + cancelled: (cancelledSubtasks / totalSubtasks) * 100, + }; + const completionPercentage = (completedSubtasks / totalSubtasks) * 100; - const availableWidth = process.stdout.columns || 80; - const boxPadding = 2; - const boxBorders = 2; - const percentTextLength = 5; - const progressBarLength = Math.max( - 20, - Math.min( - 60, - availableWidth - boxPadding - boxBorders - percentTextLength - 35 - ) - ); + const availableWidth = process.stdout.columns || 80; + const boxPadding = 2; + const boxBorders = 2; + const percentTextLength = 5; + const progressBarLength = Math.max( + 20, + Math.min( + 60, + availableWidth - boxPadding - boxBorders - percentTextLength - 35 + ) + ); - const statusCounts = - `${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` + - `${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`; + const statusCounts = + `${chalk.green("✓ Done:")} ${completedSubtasks} ${chalk.hex("#FFA500")("► In Progress:")} ${inProgressSubtasks} ${chalk.yellow("○ Pending:")} ${pendingSubtasks}\n` + + `${chalk.red("! Blocked:")} ${blockedSubtasks} ${chalk.gray("⏱ Deferred:")} ${deferredSubtasks} ${chalk.gray("✗ Cancelled:")} ${cancelledSubtasks}`; - console.log( - boxen( - chalk.white.bold('Subtask Progress:') + - '\n\n' + - `${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + - `${statusCounts}\n` + - `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 0 }, - width: Math.min(availableWidth - 10, 100), - textAlignment: 'left' - } - ) - ); - } + console.log( + boxen( + chalk.white.bold("Subtask Progress:") + + "\n\n" + + `${chalk.cyan("Completed:")} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + + `${statusCounts}\n` + + `${chalk.cyan("Progress:")} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + width: Math.min(availableWidth - 10, 100), + textAlignment: "left", + } + ) + ); + } - // --- Suggested Actions --- - console.log( - boxen( - chalk.white.bold('Suggested Actions:') + - '\n' + - `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` + - `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` + - // Determine action 3 based on whether subtasks *exist* (use the source list for progress) - (subtasksForProgress && subtasksForProgress.length > 0 - ? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1 - : `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // --- Suggested Actions --- + console.log( + boxen( + chalk.white.bold("Suggested Actions:") + + "\n" + + `${chalk.cyan("1.")} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` + + `${chalk.cyan("2.")} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` + + // Determine action 3 based on whether subtasks *exist* (use the source list for progress) + (subtasksForProgress && subtasksForProgress.length > 0 + ? `${chalk.cyan("3.")} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1 + : `${chalk.cyan("3.")} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); } /** @@ -1496,243 +1496,243 @@ async function displayTaskById( * @param {string} reportPath - Path to the complexity report file */ async function displayComplexityReport(reportPath) { - displayBanner(); + displayBanner(); - // Check if the report exists - if (!fs.existsSync(reportPath)) { - console.log( - boxen( - chalk.yellow(`No complexity report found at ${reportPath}\n\n`) + - 'Would you like to generate one now?', - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // Check if the report exists + if (!fs.existsSync(reportPath)) { + console.log( + boxen( + chalk.yellow(`No complexity report found at ${reportPath}\n\n`) + + "Would you like to generate one now?", + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - const readline = require('readline').createInterface({ - input: process.stdin, - output: process.stdout - }); + const readline = require("readline").createInterface({ + input: process.stdin, + output: process.stdout, + }); - const answer = await new Promise((resolve) => { - readline.question( - chalk.cyan('Generate complexity report? (y/n): '), - resolve - ); - }); - readline.close(); + const answer = await new Promise((resolve) => { + readline.question( + chalk.cyan("Generate complexity report? (y/n): "), + resolve + ); + }); + readline.close(); - if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { - // Call the analyze-complexity command - console.log(chalk.blue('Generating complexity report...')); - await analyzeTaskComplexity({ - output: reportPath, - research: false, // Default to no research for speed - file: 'tasks/tasks.json' - }); - // Read the newly generated report - return displayComplexityReport(reportPath); - } else { - console.log(chalk.yellow('Report generation cancelled.')); - return; - } - } + if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") { + // Call the analyze-complexity command + console.log(chalk.blue("Generating complexity report...")); + await analyzeTaskComplexity({ + output: reportPath, + research: false, // Default to no research for speed + file: "tasks/tasks.json", + }); + // Read the newly generated report + return displayComplexityReport(reportPath); + } else { + console.log(chalk.yellow("Report generation cancelled.")); + return; + } + } - // Read the report - let report; - try { - report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); - } catch (error) { - log('error', `Error reading complexity report: ${error.message}`); - return; - } + // Read the report + let report; + try { + report = JSON.parse(fs.readFileSync(reportPath, "utf8")); + } catch (error) { + log("error", `Error reading complexity report: ${error.message}`); + return; + } - // Display report header - console.log( - boxen(chalk.white.bold('Task Complexity Analysis Report'), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); + // Display report header + console.log( + boxen(chalk.white.bold("Task Complexity Analysis Report"), { + padding: 1, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + }) + ); - // Display metadata - const metaTable = new Table({ - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { - mid: '', - 'left-mid': '', - 'mid-mid': '', - 'right-mid': '' - }, - colWidths: [20, 50] - }); + // Display metadata + const metaTable = new Table({ + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { + mid: "", + "left-mid": "", + "mid-mid": "", + "right-mid": "", + }, + colWidths: [20, 50], + }); - metaTable.push( - [ - chalk.cyan.bold('Generated:'), - new Date(report.meta.generatedAt).toLocaleString() - ], - [chalk.cyan.bold('Tasks Analyzed:'), report.meta.tasksAnalyzed], - [chalk.cyan.bold('Threshold Score:'), report.meta.thresholdScore], - [chalk.cyan.bold('Project:'), report.meta.projectName], - [ - chalk.cyan.bold('Research-backed:'), - report.meta.usedResearch ? 'Yes' : 'No' - ] - ); + metaTable.push( + [ + chalk.cyan.bold("Generated:"), + new Date(report.meta.generatedAt).toLocaleString(), + ], + [chalk.cyan.bold("Tasks Analyzed:"), report.meta.tasksAnalyzed], + [chalk.cyan.bold("Threshold Score:"), report.meta.thresholdScore], + [chalk.cyan.bold("Project:"), report.meta.projectName], + [ + chalk.cyan.bold("Research-backed:"), + report.meta.usedResearch ? "Yes" : "No", + ] + ); - console.log(metaTable.toString()); + console.log(metaTable.toString()); - // Sort tasks by complexity score (highest first) - const sortedTasks = [...report.complexityAnalysis].sort( - (a, b) => b.complexityScore - a.complexityScore - ); + // Sort tasks by complexity score (highest first) + const sortedTasks = [...report.complexityAnalysis].sort( + (a, b) => b.complexityScore - a.complexityScore + ); - // Determine which tasks need expansion based on threshold - const tasksNeedingExpansion = sortedTasks.filter( - (task) => task.complexityScore >= report.meta.thresholdScore - ); - const simpleTasks = sortedTasks.filter( - (task) => task.complexityScore < report.meta.thresholdScore - ); + // Determine which tasks need expansion based on threshold + const tasksNeedingExpansion = sortedTasks.filter( + (task) => task.complexityScore >= report.meta.thresholdScore + ); + const simpleTasks = sortedTasks.filter( + (task) => task.complexityScore < report.meta.thresholdScore + ); - // Create progress bar to show complexity distribution - const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10) - sortedTasks.forEach((task) => { - if (task.complexityScore < 5) complexityDistribution[0]++; - else if (task.complexityScore < 8) complexityDistribution[1]++; - else complexityDistribution[2]++; - }); + // Create progress bar to show complexity distribution + const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10) + sortedTasks.forEach((task) => { + if (task.complexityScore < 5) complexityDistribution[0]++; + else if (task.complexityScore < 8) complexityDistribution[1]++; + else complexityDistribution[2]++; + }); - const percentLow = Math.round( - (complexityDistribution[0] / sortedTasks.length) * 100 - ); - const percentMedium = Math.round( - (complexityDistribution[1] / sortedTasks.length) * 100 - ); - const percentHigh = Math.round( - (complexityDistribution[2] / sortedTasks.length) * 100 - ); + const percentLow = Math.round( + (complexityDistribution[0] / sortedTasks.length) * 100 + ); + const percentMedium = Math.round( + (complexityDistribution[1] / sortedTasks.length) * 100 + ); + const percentHigh = Math.round( + (complexityDistribution[2] / sortedTasks.length) * 100 + ); - console.log( - boxen( - chalk.white.bold('Complexity Distribution\n\n') + - `${chalk.green.bold('Low (1-4):')} ${complexityDistribution[0]} tasks (${percentLow}%)\n` + - `${chalk.yellow.bold('Medium (5-7):')} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` + - `${chalk.red.bold('High (8-10):')} ${complexityDistribution[2]} tasks (${percentHigh}%)`, - { - padding: 1, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - } - ) - ); + console.log( + boxen( + chalk.white.bold("Complexity Distribution\n\n") + + `${chalk.green.bold("Low (1-4):")} ${complexityDistribution[0]} tasks (${percentLow}%)\n` + + `${chalk.yellow.bold("Medium (5-7):")} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` + + `${chalk.red.bold("High (8-10):")} ${complexityDistribution[2]} tasks (${percentHigh}%)`, + { + padding: 1, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + } + ) + ); - // Get terminal width - const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect + // Get terminal width + const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect - // Calculate dynamic column widths - const idWidth = 12; - const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width - const scoreWidth = 8; - const subtasksWidth = 8; - // Command column gets the remaining space (minus some buffer for borders) - const commandWidth = - terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10; + // Calculate dynamic column widths + const idWidth = 12; + const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width + const scoreWidth = 8; + const subtasksWidth = 8; + // Command column gets the remaining space (minus some buffer for borders) + const commandWidth = + terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10; - // Create table with new column widths and word wrapping - const complexTable = new Table({ - head: [ - chalk.yellow.bold('ID'), - chalk.yellow.bold('Title'), - chalk.yellow.bold('Score'), - chalk.yellow.bold('Subtasks'), - chalk.yellow.bold('Expansion Command') - ], - colWidths: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth], - style: { head: [], border: [] }, - wordWrap: true, - wrapOnWordBoundary: true - }); + // Create table with new column widths and word wrapping + const complexTable = new Table({ + head: [ + chalk.yellow.bold("ID"), + chalk.yellow.bold("Title"), + chalk.yellow.bold("Score"), + chalk.yellow.bold("Subtasks"), + chalk.yellow.bold("Expansion Command"), + ], + colWidths: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth], + style: { head: [], border: [] }, + wordWrap: true, + wrapOnWordBoundary: true, + }); - // When adding rows, don't truncate the expansion command - tasksNeedingExpansion.forEach((task) => { - const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ''}`; + // When adding rows, don't truncate the expansion command + tasksNeedingExpansion.forEach((task) => { + const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ""}`; - complexTable.push([ - task.taskId, - truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability - getComplexityWithColor(task.complexityScore), - task.recommendedSubtasks, - chalk.cyan(expansionCommand) // Don't truncate - allow wrapping - ]); - }); + complexTable.push([ + task.taskId, + truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability + getComplexityWithColor(task.complexityScore), + task.recommendedSubtasks, + chalk.cyan(expansionCommand), // Don't truncate - allow wrapping + ]); + }); - console.log(complexTable.toString()); + console.log(complexTable.toString()); - // Create table for simple tasks - if (simpleTasks.length > 0) { - console.log( - boxen(chalk.green.bold(`Simple Tasks (${simpleTasks.length})`), { - padding: { left: 2, right: 2, top: 0, bottom: 0 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'green', - borderStyle: 'round' - }) - ); + // Create table for simple tasks + if (simpleTasks.length > 0) { + console.log( + boxen(chalk.green.bold(`Simple Tasks (${simpleTasks.length})`), { + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 0 }, + borderColor: "green", + borderStyle: "round", + }) + ); - const simpleTable = new Table({ - head: [ - chalk.green.bold('ID'), - chalk.green.bold('Title'), - chalk.green.bold('Score'), - chalk.green.bold('Reasoning') - ], - colWidths: [5, 40, 8, 50], - style: { head: [], border: [] } - }); + const simpleTable = new Table({ + head: [ + chalk.green.bold("ID"), + chalk.green.bold("Title"), + chalk.green.bold("Score"), + chalk.green.bold("Reasoning"), + ], + colWidths: [5, 40, 8, 50], + style: { head: [], border: [] }, + }); - simpleTasks.forEach((task) => { - simpleTable.push([ - task.taskId, - truncate(task.taskTitle, 37), - getComplexityWithColor(task.complexityScore), - truncate(task.reasoning, 47) - ]); - }); + simpleTasks.forEach((task) => { + simpleTable.push([ + task.taskId, + truncate(task.taskTitle, 37), + getComplexityWithColor(task.complexityScore), + truncate(task.reasoning, 47), + ]); + }); - console.log(simpleTable.toString()); - } + console.log(simpleTable.toString()); + } - // Show action suggestions - console.log( - boxen( - chalk.white.bold('Suggested Actions:') + - '\n\n' + - `${chalk.cyan('1.')} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` + - `${chalk.cyan('2.')} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` + - `${chalk.cyan('3.')} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`, - { - padding: 1, - borderColor: 'cyan', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // Show action suggestions + console.log( + boxen( + chalk.white.bold("Suggested Actions:") + + "\n\n" + + `${chalk.cyan("1.")} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` + + `${chalk.cyan("2.")} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` + + `${chalk.cyan("3.")} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`, + { + padding: 1, + borderColor: "cyan", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); } /** @@ -1741,21 +1741,21 @@ async function displayComplexityReport(reportPath) { * @returns {string} Generated prompt */ function generateComplexityAnalysisPrompt(tasksData) { - const defaultSubtasks = getDefaultSubtasks(null); // Use the getter - return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: + const defaultSubtasks = getDefaultSubtasks(null); // Use the getter + return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: ${tasksData.tasks - .map( - (task) => ` + .map( + (task) => ` Task ID: ${task.id} Title: ${task.title} Description: ${task.description} Details: ${task.details} Dependencies: ${JSON.stringify(task.dependencies || [])} -Priority: ${task.priority || 'medium'} +Priority: ${task.priority || "medium"} ` - ) - .join('\n---\n')} + ) + .join("\n---\n")} Analyze each task and return a JSON array with the following structure for each task: [ @@ -1780,39 +1780,39 @@ IMPORTANT: Make sure to include an analysis for EVERY task listed above, with th * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise */ async function confirmTaskOverwrite(tasksPath) { - console.log( - boxen( - chalk.yellow( - "It looks like you've already generated tasks for this project.\n" - ) + - chalk.yellow( - 'Executing this command will overwrite any existing tasks.' - ), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + console.log( + boxen( + chalk.yellow( + "It looks like you've already generated tasks for this project.\n" + ) + + chalk.yellow( + "Executing this command will overwrite any existing tasks." + ), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - // Use dynamic import to get the readline module - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + // Use dynamic import to get the readline module + const readline = await import("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const answer = await new Promise((resolve) => { - rl.question( - chalk.cyan('Are you sure you wish to continue? (y/N): '), - resolve - ); - }); - rl.close(); + const answer = await new Promise((resolve) => { + rl.question( + chalk.cyan("Are you sure you wish to continue? (y/N): "), + resolve + ); + }); + rl.close(); - return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"; } /** @@ -1820,75 +1820,75 @@ async function confirmTaskOverwrite(tasksPath) { * @param {Array<{provider: string, cli: boolean, mcp: boolean}>} statusReport - The report generated by getApiKeyStatusReport. */ function displayApiKeyStatus(statusReport) { - if (!statusReport || statusReport.length === 0) { - console.log(chalk.yellow('No API key status information available.')); - return; - } + if (!statusReport || statusReport.length === 0) { + console.log(chalk.yellow("No API key status information available.")); + return; + } - const table = new Table({ - head: [ - chalk.cyan('Provider'), - chalk.cyan('CLI Key (.env)'), - chalk.cyan('MCP Key (mcp.json)') - ], - colWidths: [15, 20, 25], - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' } - }); + const table = new Table({ + head: [ + chalk.cyan("Provider"), + chalk.cyan("CLI Key (.env)"), + chalk.cyan("MCP Key (mcp.json)"), + ], + colWidths: [15, 20, 25], + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + }); - statusReport.forEach(({ provider, cli, mcp }) => { - const cliStatus = cli ? chalk.green('✅ Found') : chalk.red('❌ Missing'); - const mcpStatus = mcp ? chalk.green('✅ Found') : chalk.red('❌ Missing'); - // Capitalize provider name for display - const providerName = provider.charAt(0).toUpperCase() + provider.slice(1); - table.push([providerName, cliStatus, mcpStatus]); - }); + statusReport.forEach(({ provider, cli, mcp }) => { + const cliStatus = cli ? chalk.green("✅ Found") : chalk.red("❌ Missing"); + const mcpStatus = mcp ? chalk.green("✅ Found") : chalk.red("❌ Missing"); + // Capitalize provider name for display + const providerName = provider.charAt(0).toUpperCase() + provider.slice(1); + table.push([providerName, cliStatus, mcpStatus]); + }); - console.log(chalk.bold('\n🔑 API Key Status:')); - console.log(table.toString()); - console.log( - chalk.gray( - ' Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in .taskmasterconfig.' - ) - ); + console.log(chalk.bold("\n🔑 API Key Status:")); + console.log(table.toString()); + console.log( + chalk.gray( + " Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in .taskmasterconfig." + ) + ); } // --- Formatting Helpers (Potentially move some to utils.js if reusable) --- const formatSweScoreWithTertileStars = (score, allModels) => { - // ... (Implementation from previous version or refine) ... - if (score === null || score === undefined || score <= 0) return 'N/A'; - const formattedPercentage = `${(score * 100).toFixed(1)}%`; + // ... (Implementation from previous version or refine) ... + if (score === null || score === undefined || score <= 0) return "N/A"; + const formattedPercentage = `${(score * 100).toFixed(1)}%`; - const validScores = allModels - .map((m) => m.sweScore) - .filter((s) => s !== null && s !== undefined && s > 0); - const sortedScores = [...validScores].sort((a, b) => b - a); - const n = sortedScores.length; - let stars = chalk.gray('☆☆☆'); + const validScores = allModels + .map((m) => m.sweScore) + .filter((s) => s !== null && s !== undefined && s > 0); + const sortedScores = [...validScores].sort((a, b) => b - a); + const n = sortedScores.length; + let stars = chalk.gray("☆☆☆"); - if (n > 0) { - const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); - const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); - if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow('★★★'); - else if (score >= sortedScores[midThirdIndex]) - stars = chalk.yellow('★★') + chalk.gray('☆'); - else stars = chalk.yellow('★') + chalk.gray('☆☆'); - } - return `${formattedPercentage} ${stars}`; + if (n > 0) { + const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); + const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); + if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow("★★★"); + else if (score >= sortedScores[midThirdIndex]) + stars = chalk.yellow("★★") + chalk.gray("☆"); + else stars = chalk.yellow("★") + chalk.gray("☆☆"); + } + return `${formattedPercentage} ${stars}`; }; const formatCost = (costObj) => { - // ... (Implementation from previous version or refine) ... - if (!costObj) return 'N/A'; - if (costObj.input === 0 && costObj.output === 0) { - return chalk.green('Free'); - } - const formatSingleCost = (costValue) => { - if (costValue === null || costValue === undefined) return 'N/A'; - const isInteger = Number.isInteger(costValue); - return `$${costValue.toFixed(isInteger ? 0 : 2)}`; - }; - return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`; + // ... (Implementation from previous version or refine) ... + if (!costObj) return "N/A"; + if (costObj.input === 0 && costObj.output === 0) { + return chalk.green("Free"); + } + const formatSingleCost = (costValue) => { + if (costValue === null || costValue === undefined) return "N/A"; + const isInteger = Number.isInteger(costValue); + return `$${costValue.toFixed(isInteger ? 0 : 2)}`; + }; + return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`; }; // --- Display Functions --- @@ -1899,63 +1899,63 @@ const formatCost = (costObj) => { * @param {AvailableModel[]} allAvailableModels - Needed for SWE score tertiles. */ function displayModelConfiguration(configData, allAvailableModels = []) { - console.log(chalk.cyan.bold('\nActive Model Configuration:')); - const active = configData.activeModels; - const activeTable = new Table({ - head: [ - 'Role', - 'Provider', - 'Model ID', - 'SWE Score', - 'Cost ($/1M tkns)' - // 'API Key Status' // Removed, handled by separate displayApiKeyStatus - ].map((h) => chalk.cyan.bold(h)), - colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths - style: { head: ['cyan', 'bold'] } - }); + console.log(chalk.cyan.bold("\nActive Model Configuration:")); + const active = configData.activeModels; + const activeTable = new Table({ + head: [ + "Role", + "Provider", + "Model ID", + "SWE Score", + "Cost ($/1M tkns)", + // 'API Key Status' // Removed, handled by separate displayApiKeyStatus + ].map((h) => chalk.cyan.bold(h)), + colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths + style: { head: ["cyan", "bold"] }, + }); - activeTable.push([ - chalk.white('Main'), - active.main.provider, - active.main.modelId, - formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels), - formatCost(active.main.cost) - // getCombinedStatus(active.main.keyStatus) // Removed - ]); - activeTable.push([ - chalk.white('Research'), - active.research.provider, - active.research.modelId, - formatSweScoreWithTertileStars( - active.research.sweScore, - allAvailableModels - ), - formatCost(active.research.cost) - // getCombinedStatus(active.research.keyStatus) // Removed - ]); - if (active.fallback && active.fallback.provider && active.fallback.modelId) { - activeTable.push([ - chalk.white('Fallback'), - active.fallback.provider, - active.fallback.modelId, - formatSweScoreWithTertileStars( - active.fallback.sweScore, - allAvailableModels - ), - formatCost(active.fallback.cost) - // getCombinedStatus(active.fallback.keyStatus) // Removed - ]); - } else { - activeTable.push([ - chalk.white('Fallback'), - chalk.gray('-'), - chalk.gray('(Not Set)'), - chalk.gray('-'), - chalk.gray('-') - // chalk.gray('-') // Removed - ]); - } - console.log(activeTable.toString()); + activeTable.push([ + chalk.white("Main"), + active.main.provider, + active.main.modelId, + formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels), + formatCost(active.main.cost), + // getCombinedStatus(active.main.keyStatus) // Removed + ]); + activeTable.push([ + chalk.white("Research"), + active.research.provider, + active.research.modelId, + formatSweScoreWithTertileStars( + active.research.sweScore, + allAvailableModels + ), + formatCost(active.research.cost), + // getCombinedStatus(active.research.keyStatus) // Removed + ]); + if (active.fallback && active.fallback.provider && active.fallback.modelId) { + activeTable.push([ + chalk.white("Fallback"), + active.fallback.provider, + active.fallback.modelId, + formatSweScoreWithTertileStars( + active.fallback.sweScore, + allAvailableModels + ), + formatCost(active.fallback.cost), + // getCombinedStatus(active.fallback.keyStatus) // Removed + ]); + } else { + activeTable.push([ + chalk.white("Fallback"), + chalk.gray("-"), + chalk.gray("(Not Set)"), + chalk.gray("-"), + chalk.gray("-"), + // chalk.gray('-') // Removed + ]); + } + console.log(activeTable.toString()); } /** @@ -1963,64 +1963,64 @@ function displayModelConfiguration(configData, allAvailableModels = []) { * @param {AvailableModel[]} availableModels - List of available models. */ function displayAvailableModels(availableModels) { - if (!availableModels || availableModels.length === 0) { - console.log( - chalk.gray('\n(No other models available or all are configured)') - ); - return; - } + if (!availableModels || availableModels.length === 0) { + console.log( + chalk.gray("\n(No other models available or all are configured)") + ); + return; + } - console.log(chalk.cyan.bold('\nOther Available Models:')); - const availableTable = new Table({ - head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map((h) => - chalk.cyan.bold(h) - ), - colWidths: [15, 40, 18, 25], - style: { head: ['cyan', 'bold'] } - }); + console.log(chalk.cyan.bold("\nOther Available Models:")); + const availableTable = new Table({ + head: ["Provider", "Model ID", "SWE Score", "Cost ($/1M tkns)"].map((h) => + chalk.cyan.bold(h) + ), + colWidths: [15, 40, 18, 25], + style: { head: ["cyan", "bold"] }, + }); - availableModels.forEach((model) => { - availableTable.push([ - model.provider, - model.modelId, - formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison - formatCost(model.cost) - ]); - }); - console.log(availableTable.toString()); + availableModels.forEach((model) => { + availableTable.push([ + model.provider, + model.modelId, + formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison + formatCost(model.cost), + ]); + }); + console.log(availableTable.toString()); - // --- Suggested Actions Section (moved here from models command) --- - console.log( - boxen( - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}` - ) + - '\n' + - chalk.cyan( - `2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}` - ) + - '\n' + - chalk.cyan( - `3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}` - ) + - '\n' + - chalk.cyan( - `4. Run interactive setup: ${chalk.yellow('task-master models --setup')}` - ) + - '\n' + - chalk.cyan( - `5. Use custom ollama/openrouter models: ${chalk.yellow('task-master models --openrouter|ollama --set-main|research|fallback <model_id>')}` - ), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // --- Suggested Actions Section (moved here from models command) --- + console.log( + boxen( + chalk.white.bold("Next Steps:") + + "\n" + + chalk.cyan( + `1. Set main model: ${chalk.yellow("task-master models --set-main <model_id>")}` + ) + + "\n" + + chalk.cyan( + `2. Set research model: ${chalk.yellow("task-master models --set-research <model_id>")}` + ) + + "\n" + + chalk.cyan( + `3. Set fallback model: ${chalk.yellow("task-master models --set-fallback <model_id>")}` + ) + + "\n" + + chalk.cyan( + `4. Run interactive setup: ${chalk.yellow("task-master models --setup")}` + ) + + "\n" + + chalk.cyan( + `5. Use custom ollama/openrouter models: ${chalk.yellow("task-master models --openrouter|ollama --set-main|research|fallback <model_id>")}` + ), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); } /** @@ -2028,44 +2028,44 @@ function displayAvailableModels(availableModels) { * @param {object} telemetryData - The telemetry data object. * @param {string} outputType - 'cli' or 'mcp' (though typically only called for 'cli'). */ -function displayAiUsageSummary(telemetryData, outputType = 'cli') { - if ( - (outputType !== 'cli' && outputType !== 'text') || - !telemetryData || - isSilentMode() - ) { - return; // Only display for CLI and if data exists and not in silent mode - } +function displayAiUsageSummary(telemetryData, outputType = "cli") { + if ( + (outputType !== "cli" && outputType !== "text") || + !telemetryData || + isSilentMode() + ) { + return; // Only display for CLI and if data exists and not in silent mode + } - const { - modelUsed, - providerName, - inputTokens, - outputTokens, - totalTokens, - totalCost, - commandName - } = telemetryData; + const { + modelUsed, + providerName, + inputTokens, + outputTokens, + totalTokens, + totalCost, + commandName, + } = telemetryData; - let summary = chalk.bold.blue('AI Usage Summary:') + '\n'; - summary += chalk.gray(` Command: ${commandName}\n`); - summary += chalk.gray(` Provider: ${providerName}\n`); - summary += chalk.gray(` Model: ${modelUsed}\n`); - summary += chalk.gray( - ` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n` - ); - summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`); + let summary = chalk.bold.blue("AI Usage Summary:") + "\n"; + summary += chalk.gray(` Command: ${commandName}\n`); + summary += chalk.gray(` Provider: ${providerName}\n`); + summary += chalk.gray(` Model: ${modelUsed}\n`); + summary += chalk.gray( + ` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n` + ); + summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`); - console.log( - boxen(summary, { - padding: 1, - margin: { top: 1 }, - borderColor: 'blue', - borderStyle: 'round', - title: '💡 Telemetry', - titleAlignment: 'center' - }) - ); + console.log( + boxen(summary, { + padding: 1, + margin: { top: 1 }, + borderColor: "blue", + borderStyle: "round", + title: "💡 Telemetry", + titleAlignment: "center", + }) + ); } /** @@ -2076,389 +2076,389 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') { * @param {string} statusFilter - Optional status filter for subtasks */ async function displayMultipleTasksSummary( - tasksPath, - taskIds, - complexityReportPath = null, - statusFilter = null + tasksPath, + taskIds, + complexityReportPath = null, + statusFilter = null ) { - displayBanner(); + displayBanner(); - // Read the tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + // Read the tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Read complexity report once - const complexityReport = readComplexityReport(complexityReportPath); + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); - // Find all requested tasks - const foundTasks = []; - const notFoundIds = []; + // Find all requested tasks + const foundTasks = []; + const notFoundIds = []; - taskIds.forEach((id) => { - const { task } = findTaskById( - data.tasks, - id, - complexityReport, - statusFilter - ); - if (task) { - foundTasks.push(task); - } else { - notFoundIds.push(id); - } - }); + taskIds.forEach((id) => { + const { task } = findTaskById( + data.tasks, + id, + complexityReport, + statusFilter + ); + if (task) { + foundTasks.push(task); + } else { + notFoundIds.push(id); + } + }); - // Show not found tasks - if (notFoundIds.length > 0) { - console.log( - boxen(chalk.yellow(`Tasks not found: ${notFoundIds.join(', ')}`), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); - } + // Show not found tasks + if (notFoundIds.length > 0) { + console.log( + boxen(chalk.yellow(`Tasks not found: ${notFoundIds.join(", ")}`), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1, bottom: 1 }, + }) + ); + } - if (foundTasks.length === 0) { - console.log( - boxen(chalk.red('No valid tasks found to display'), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'red', - borderStyle: 'round', - margin: { top: 1 } - }) - ); - return; - } + if (foundTasks.length === 0) { + console.log( + boxen(chalk.red("No valid tasks found to display"), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "red", + borderStyle: "round", + margin: { top: 1 }, + }) + ); + return; + } - // Display header - console.log( - boxen( - chalk.white.bold( - `Task Summary (${foundTasks.length} task${foundTasks.length === 1 ? '' : 's'})` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 0 } - } - ) - ); + // Display header + console.log( + boxen( + chalk.white.bold( + `Task Summary (${foundTasks.length} task${foundTasks.length === 1 ? "" : "s"})` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + } + ) + ); - // Calculate terminal width for responsive layout - const terminalWidth = process.stdout.columns || 100; - const availableWidth = terminalWidth - 10; + // Calculate terminal width for responsive layout + const terminalWidth = process.stdout.columns || 100; + const availableWidth = terminalWidth - 10; - // Create compact summary table - const summaryTable = new Table({ - head: [ - chalk.cyan.bold('ID'), - chalk.cyan.bold('Title'), - chalk.cyan.bold('Status'), - chalk.cyan.bold('Priority'), - chalk.cyan.bold('Subtasks'), - chalk.cyan.bold('Progress') - ], - colWidths: [ - Math.floor(availableWidth * 0.08), // ID: 8% - Math.floor(availableWidth * 0.35), // Title: 35% - Math.floor(availableWidth * 0.12), // Status: 12% - Math.floor(availableWidth * 0.1), // Priority: 10% - Math.floor(availableWidth * 0.15), // Subtasks: 15% - Math.floor(availableWidth * 0.2) // Progress: 20% - ], - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - wordWrap: true - }); + // Create compact summary table + const summaryTable = new Table({ + head: [ + chalk.cyan.bold("ID"), + chalk.cyan.bold("Title"), + chalk.cyan.bold("Status"), + chalk.cyan.bold("Priority"), + chalk.cyan.bold("Subtasks"), + chalk.cyan.bold("Progress"), + ], + colWidths: [ + Math.floor(availableWidth * 0.08), // ID: 8% + Math.floor(availableWidth * 0.35), // Title: 35% + Math.floor(availableWidth * 0.12), // Status: 12% + Math.floor(availableWidth * 0.1), // Priority: 10% + Math.floor(availableWidth * 0.15), // Subtasks: 15% + Math.floor(availableWidth * 0.2), // Progress: 20% + ], + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + wordWrap: true, + }); - // Add each task to the summary table - foundTasks.forEach((task) => { - // Handle subtask case - if (task.isSubtask || task.parentTask) { - const parentId = task.parentTask ? task.parentTask.id : 'Unknown'; - summaryTable.push([ - `${parentId}.${task.id}`, - truncate(task.title, Math.floor(availableWidth * 0.35) - 3), - getStatusWithColor(task.status || 'pending', true), - chalk.gray('(subtask)'), - chalk.gray('N/A'), - chalk.gray('N/A') - ]); - return; - } + // Add each task to the summary table + foundTasks.forEach((task) => { + // Handle subtask case + if (task.isSubtask || task.parentTask) { + const parentId = task.parentTask ? task.parentTask.id : "Unknown"; + summaryTable.push([ + `${parentId}.${task.id}`, + truncate(task.title, Math.floor(availableWidth * 0.35) - 3), + getStatusWithColor(task.status || "pending", true), + chalk.gray("(subtask)"), + chalk.gray("N/A"), + chalk.gray("N/A"), + ]); + return; + } - // Handle regular task - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; - const priorityColor = - priorityColors[task.priority || 'medium'] || chalk.white; + // Handle regular task + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + low: chalk.gray, + }; + const priorityColor = + priorityColors[task.priority || "medium"] || chalk.white; - // Calculate subtask summary - let subtaskSummary = chalk.gray('None'); - let progressBar = chalk.gray('N/A'); + // Calculate subtask summary + let subtaskSummary = chalk.gray("None"); + let progressBar = chalk.gray("N/A"); - if (task.subtasks && task.subtasks.length > 0) { - const total = task.subtasks.length; - const completed = task.subtasks.filter( - (st) => st.status === 'done' || st.status === 'completed' - ).length; - const inProgress = task.subtasks.filter( - (st) => st.status === 'in-progress' - ).length; - const pending = task.subtasks.filter( - (st) => st.status === 'pending' - ).length; + if (task.subtasks && task.subtasks.length > 0) { + const total = task.subtasks.length; + const completed = task.subtasks.filter( + (st) => st.status === "done" || st.status === "completed" + ).length; + const inProgress = task.subtasks.filter( + (st) => st.status === "in-progress" + ).length; + const pending = task.subtasks.filter( + (st) => st.status === "pending" + ).length; - // Compact subtask count with status indicators - subtaskSummary = `${chalk.green(completed)}/${total}`; - if (inProgress > 0) - subtaskSummary += ` ${chalk.hex('#FFA500')(`+${inProgress}`)}`; - if (pending > 0) subtaskSummary += ` ${chalk.yellow(`(${pending})`)}`; + // Compact subtask count with status indicators + subtaskSummary = `${chalk.green(completed)}/${total}`; + if (inProgress > 0) + subtaskSummary += ` ${chalk.hex("#FFA500")(`+${inProgress}`)}`; + if (pending > 0) subtaskSummary += ` ${chalk.yellow(`(${pending})`)}`; - // Mini progress bar (shorter than usual) - const completionPercentage = (completed / total) * 100; - const barLength = 8; // Compact bar - const statusBreakdown = { - 'in-progress': (inProgress / total) * 100, - pending: (pending / total) * 100 - }; - progressBar = createProgressBar( - completionPercentage, - barLength, - statusBreakdown - ); - } + // Mini progress bar (shorter than usual) + const completionPercentage = (completed / total) * 100; + const barLength = 8; // Compact bar + const statusBreakdown = { + "in-progress": (inProgress / total) * 100, + pending: (pending / total) * 100, + }; + progressBar = createProgressBar( + completionPercentage, + barLength, + statusBreakdown + ); + } - summaryTable.push([ - task.id.toString(), - truncate(task.title, Math.floor(availableWidth * 0.35) - 3), - getStatusWithColor(task.status || 'pending', true), - priorityColor(task.priority || 'medium'), - subtaskSummary, - progressBar - ]); - }); + summaryTable.push([ + task.id.toString(), + truncate(task.title, Math.floor(availableWidth * 0.35) - 3), + getStatusWithColor(task.status || "pending", true), + priorityColor(task.priority || "medium"), + subtaskSummary, + progressBar, + ]); + }); - console.log(summaryTable.toString()); + console.log(summaryTable.toString()); - // Interactive drill-down prompt - if (foundTasks.length > 1) { - console.log( - boxen( - chalk.white.bold('Interactive Options:') + - '\n' + - chalk.cyan('• Press Enter to view available actions for all tasks') + - '\n' + - chalk.cyan( - '• Type a task ID (e.g., "3" or "3.2") to view that specific task' - ) + - '\n' + - chalk.cyan('• Type "q" to quit'), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // Interactive drill-down prompt + if (foundTasks.length > 1) { + console.log( + boxen( + chalk.white.bold("Interactive Options:") + + "\n" + + chalk.cyan("• Press Enter to view available actions for all tasks") + + "\n" + + chalk.cyan( + '• Type a task ID (e.g., "3" or "3.2") to view that specific task' + ) + + "\n" + + chalk.cyan('• Type "q" to quit'), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - // Use dynamic import for readline - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + // Use dynamic import for readline + const readline = await import("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const choice = await new Promise((resolve) => { - rl.question(chalk.cyan('Your choice: '), resolve); - }); - rl.close(); + const choice = await new Promise((resolve) => { + rl.question(chalk.cyan("Your choice: "), resolve); + }); + rl.close(); - if (choice.toLowerCase() === 'q') { - return; - } else if (choice.trim() === '') { - // Show action menu for selected tasks - console.log( - boxen( - chalk.white.bold('Available Actions for Selected Tasks:') + - '\n' + - chalk.cyan('1.') + - ' Mark all as in-progress' + - '\n' + - chalk.cyan('2.') + - ' Mark all as done' + - '\n' + - chalk.cyan('3.') + - ' Show next available task' + - '\n' + - chalk.cyan('4.') + - ' Expand all tasks (generate subtasks)' + - '\n' + - chalk.cyan('5.') + - ' View dependency relationships' + - '\n' + - chalk.cyan('6.') + - ' Generate task files' + - '\n' + - chalk.gray('Or type a task ID to view details'), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + if (choice.toLowerCase() === "q") { + return; + } else if (choice.trim() === "") { + // Show action menu for selected tasks + console.log( + boxen( + chalk.white.bold("Available Actions for Selected Tasks:") + + "\n" + + chalk.cyan("1.") + + " Mark all as in-progress" + + "\n" + + chalk.cyan("2.") + + " Mark all as done" + + "\n" + + chalk.cyan("3.") + + " Show next available task" + + "\n" + + chalk.cyan("4.") + + " Expand all tasks (generate subtasks)" + + "\n" + + chalk.cyan("5.") + + " View dependency relationships" + + "\n" + + chalk.cyan("6.") + + " Generate task files" + + "\n" + + chalk.gray("Or type a task ID to view details"), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - const rl2 = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + const rl2 = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const actionChoice = await new Promise((resolve) => { - rl2.question(chalk.cyan('Choose action (1-6): '), resolve); - }); - rl2.close(); + const actionChoice = await new Promise((resolve) => { + rl2.question(chalk.cyan("Choose action (1-6): "), resolve); + }); + rl2.close(); - const taskIdList = foundTasks.map((t) => t.id).join(','); + const taskIdList = foundTasks.map((t) => t.id).join(","); - switch (actionChoice.trim()) { - case '1': - console.log( - chalk.blue( - `\n→ Command: task-master set-status --id=${taskIdList} --status=in-progress` - ) - ); - console.log( - chalk.green( - '✓ Copy and run this command to mark all tasks as in-progress' - ) - ); - break; - case '2': - console.log( - chalk.blue( - `\n→ Command: task-master set-status --id=${taskIdList} --status=done` - ) - ); - console.log( - chalk.green('✓ Copy and run this command to mark all tasks as done') - ); - break; - case '3': - console.log(chalk.blue(`\n→ Command: task-master next`)); - console.log( - chalk.green( - '✓ Copy and run this command to see the next available task' - ) - ); - break; - case '4': - console.log( - chalk.blue( - `\n→ Command: task-master expand --id=${taskIdList} --research` - ) - ); - console.log( - chalk.green( - '✓ Copy and run this command to expand all selected tasks into subtasks' - ) - ); - break; - case '5': - // Show dependency visualization - console.log(chalk.white.bold('\nDependency Relationships:')); - let hasDependencies = false; - foundTasks.forEach((task) => { - if (task.dependencies && task.dependencies.length > 0) { - console.log( - chalk.cyan( - `Task ${task.id} depends on: ${task.dependencies.join(', ')}` - ) - ); - hasDependencies = true; - } - }); - if (!hasDependencies) { - console.log(chalk.gray('No dependencies found for selected tasks')); - } - break; - case '6': - console.log(chalk.blue(`\n→ Command: task-master generate`)); - console.log( - chalk.green('✓ Copy and run this command to generate task files') - ); - break; - default: - if (actionChoice.trim().length > 0) { - console.log(chalk.yellow(`Invalid choice: ${actionChoice.trim()}`)); - console.log(chalk.gray('Please choose 1-6 or type a task ID')); - } - } - } else { - // Show specific task - await displayTaskById( - tasksPath, - choice.trim(), - complexityReportPath, - statusFilter - ); - } - } else { - // Single task - show suggested actions - const task = foundTasks[0]; - console.log( - boxen( - chalk.white.bold('Suggested Actions:') + - '\n' + - `${chalk.cyan('1.')} View full details: ${chalk.yellow(`task-master show ${task.id}`)}\n` + - `${chalk.cyan('2.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` + - `${chalk.cyan('3.')} Mark as done: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}`, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } + switch (actionChoice.trim()) { + case "1": + console.log( + chalk.blue( + `\n→ Command: task-master set-status --id=${taskIdList} --status=in-progress` + ) + ); + console.log( + chalk.green( + "✓ Copy and run this command to mark all tasks as in-progress" + ) + ); + break; + case "2": + console.log( + chalk.blue( + `\n→ Command: task-master set-status --id=${taskIdList} --status=done` + ) + ); + console.log( + chalk.green("✓ Copy and run this command to mark all tasks as done") + ); + break; + case "3": + console.log(chalk.blue(`\n→ Command: task-master next`)); + console.log( + chalk.green( + "✓ Copy and run this command to see the next available task" + ) + ); + break; + case "4": + console.log( + chalk.blue( + `\n→ Command: task-master expand --id=${taskIdList} --research` + ) + ); + console.log( + chalk.green( + "✓ Copy and run this command to expand all selected tasks into subtasks" + ) + ); + break; + case "5": + // Show dependency visualization + console.log(chalk.white.bold("\nDependency Relationships:")); + let hasDependencies = false; + foundTasks.forEach((task) => { + if (task.dependencies && task.dependencies.length > 0) { + console.log( + chalk.cyan( + `Task ${task.id} depends on: ${task.dependencies.join(", ")}` + ) + ); + hasDependencies = true; + } + }); + if (!hasDependencies) { + console.log(chalk.gray("No dependencies found for selected tasks")); + } + break; + case "6": + console.log(chalk.blue(`\n→ Command: task-master generate`)); + console.log( + chalk.green("✓ Copy and run this command to generate task files") + ); + break; + default: + if (actionChoice.trim().length > 0) { + console.log(chalk.yellow(`Invalid choice: ${actionChoice.trim()}`)); + console.log(chalk.gray("Please choose 1-6 or type a task ID")); + } + } + } else { + // Show specific task + await displayTaskById( + tasksPath, + choice.trim(), + complexityReportPath, + statusFilter + ); + } + } else { + // Single task - show suggested actions + const task = foundTasks[0]; + console.log( + boxen( + chalk.white.bold("Suggested Actions:") + + "\n" + + `${chalk.cyan("1.")} View full details: ${chalk.yellow(`task-master show ${task.id}`)}\n` + + `${chalk.cyan("2.")} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` + + `${chalk.cyan("3.")} Mark as done: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } } // Export UI functions export { - displayBanner, - startLoadingIndicator, - stopLoadingIndicator, - createProgressBar, - getStatusWithColor, - formatDependenciesWithStatus, - displayHelp, - getComplexityWithColor, - displayNextTask, - displayTaskById, - displayComplexityReport, - generateComplexityAnalysisPrompt, - confirmTaskOverwrite, - displayApiKeyStatus, - displayModelConfiguration, - displayAvailableModels, - displayAiUsageSummary, - displayMultipleTasksSummary + displayBanner, + startLoadingIndicator, + stopLoadingIndicator, + createProgressBar, + getStatusWithColor, + formatDependenciesWithStatus, + displayHelp, + getComplexityWithColor, + displayNextTask, + displayTaskById, + displayComplexityReport, + generateComplexityAnalysisPrompt, + confirmTaskOverwrite, + displayApiKeyStatus, + displayModelConfiguration, + displayAvailableModels, + displayAiUsageSummary, + displayMultipleTasksSummary, }; diff --git a/tasks/task_090.txt b/tasks/task_090.txt index ca408357..7d344bc9 100644 --- a/tasks/task_090.txt +++ b/tasks/task_090.txt @@ -190,6 +190,44 @@ Successfully tested telemetry submission against live gateway at localhost:4444/ Implementation Complete - Gateway Integration Finalized: Hardcoded gateway endpoint to http://localhost:4444/api/v1/telemetry with config-based credential handling replacing environment variables. Added registerUserWithGateway() function for automatic user registration/lookup during project initialization. Enhanced init.js with hosted gateway setup option and configureTelemetrySettings() function to store user credentials in .taskmasterconfig under telemetry section. Updated all 10 tests to reflect new architecture - all passing. Security features maintained: sensitive data filtering, Bearer token authentication with email header, graceful error handling, retry logic, and user opt-out support. Module fully integrated and ready for ai-services-unified.js integration in subtask 90.3. </info added on 2025-05-29T01:04:27.886Z> +<info added on 2025-05-30T23:36:58.010Z> +Subtask 90.2 COMPLETED successfully! ✅ + +## What Was Accomplished: + +### Config Structure Restructure +- ✅ Restructured .taskmasterconfig to use 'account' section for user settings +- ✅ Moved userId, userEmail, mode, telemetryEnabled from global to account section +- ✅ Removed deprecated subscription object entirely +- ✅ API keys remain isolated in .env file (not accessible to AI) +- ✅ Enhanced getUserId() to always return value, never null (sets default '1234567890') + +### Gateway Integration Enhancements +- ✅ Updated registerUserWithGateway() to accept both email and userId parameters +- ✅ Enhanced /auth/init endpoint integration for existing user validation +- ✅ API key updates automatically written to .env during registration + +### Code Updates +- ✅ Updated config-manager.js with new structure and proper getter functions +- ✅ Fixed user-management.js to use config.account structure +- ✅ Updated telemetry-submission.js to read from account section +- ✅ Enhanced init.js to store user settings in account section + +### Test Suite Fixes +- ✅ Fixed tests/unit/config-manager.test.js for new structure +- ✅ Updated tests/integration/init-config.test.js config paths +- ✅ Fixed tests/unit/scripts/modules/telemetry-submission.test.js +- ✅ Updated tests/unit/ai-services-unified.test.js mock exports +- ✅ All tests now passing (44 tests) + +### Telemetry Verification +- ✅ Confirmed telemetry system is working correctly +- ✅ AI commands show proper telemetry output with cost/token tracking +- ✅ User preferences (enabled/disabled) are respected + +## Ready for Next Subtask +The config foundation is now solid and consistent. Ready to move to subtask 90.3 for the next phase of telemetry improvements. +</info added on 2025-05-30T23:36:58.010Z> ## 3. Implement DAU and active user tracking [done] ### Dependencies: None diff --git a/tasks/tasks.json b/tasks/tasks.json index b48a5a31..a05076e3 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -6073,7 +6073,7 @@ "id": 2, "title": "Send telemetry data to remote database endpoint", "description": "Implement POST requests to gateway.task-master.dev/telemetry endpoint to send all telemetry data including new fields (args, output) for analysis and future AI model training", - "details": "Create a telemetry submission service that POSTs to gateway.task-master.dev/telemetry. Include all existing telemetry fields plus commandArgs and fullOutput. Implement retry logic and handle failures gracefully without blocking command execution. Respect user opt-out preferences.\n<info added on 2025-05-28T18:27:30.207Z>\nTDD Progress - Red Phase Complete:\n- Created test file: tests/unit/scripts/modules/telemetry-submission.test.js\n- Written 6 failing tests for telemetry submission functionality:\n 1. Successfully submit telemetry data to gateway endpoint\n 2. Implement retry logic for failed requests\n 3. Handle failures gracefully without blocking execution\n 4. Respect user opt-out preferences\n 5. Validate telemetry data before submission\n 6. Handle HTTP error responses appropriately\n- All tests failing as expected (module doesn't exist yet)\n- Ready to implement minimum code to make tests pass\n\nNext: Create scripts/modules/telemetry-submission.js with submitTelemetryData function\n</info added on 2025-05-28T18:27:30.207Z>\n<info added on 2025-05-28T18:43:47.334Z>\nTDD Green Phase Complete:\n- Implemented scripts/modules/telemetry-submission.js with submitTelemetryData function\n- All 6 tests now passing with full functionality implemented\n- Security measures in place: commandArgs and fullOutput filtered out before remote submission\n- Reliability features: exponential backoff retry logic (3 attempts max), graceful error handling\n- Gateway integration: configured for https://gateway.task-master.dev/telemetry endpoint\n- Zod schema validation ensures data integrity before submission\n- User privacy protected through telemetryEnabled config option\n- Smart retry logic avoids retries for 429/401/403 status codes\n- Service never throws errors and always returns result object to prevent blocking command execution\n\nImplementation ready for integration into ai-services-unified.js in subtask 90.3\n</info added on 2025-05-28T18:43:47.334Z>\n<info added on 2025-05-28T18:59:16.039Z>\nIntegration Testing Complete - Live Gateway Verification:\nSuccessfully tested telemetry submission against live gateway at localhost:4444/api/v1/telemetry. Confirmed proper authentication using Bearer token and X-User-Email headers (not X-API-Key as initially assumed). Security filtering verified working correctly - sensitive data like commandArgs, fullOutput, apiKey, and internalDebugData properly removed before submission. Gateway responded with success confirmation and assigned telemetry ID. Service handles missing GATEWAY_USER_EMAIL environment variable gracefully. All functionality validated end-to-end including retry logic, error handling, and data validation. Module ready for integration into ai-services-unified.js.\n</info added on 2025-05-28T18:59:16.039Z>\n<info added on 2025-05-29T01:04:27.886Z>\nImplementation Complete - Gateway Integration Finalized:\nHardcoded gateway endpoint to http://localhost:4444/api/v1/telemetry with config-based credential handling replacing environment variables. Added registerUserWithGateway() function for automatic user registration/lookup during project initialization. Enhanced init.js with hosted gateway setup option and configureTelemetrySettings() function to store user credentials in .taskmasterconfig under telemetry section. Updated all 10 tests to reflect new architecture - all passing. Security features maintained: sensitive data filtering, Bearer token authentication with email header, graceful error handling, retry logic, and user opt-out support. Module fully integrated and ready for ai-services-unified.js integration in subtask 90.3.\n</info added on 2025-05-29T01:04:27.886Z>", + "details": "Create a telemetry submission service that POSTs to gateway.task-master.dev/telemetry. Include all existing telemetry fields plus commandArgs and fullOutput. Implement retry logic and handle failures gracefully without blocking command execution. Respect user opt-out preferences.\n<info added on 2025-05-28T18:27:30.207Z>\nTDD Progress - Red Phase Complete:\n- Created test file: tests/unit/scripts/modules/telemetry-submission.test.js\n- Written 6 failing tests for telemetry submission functionality:\n 1. Successfully submit telemetry data to gateway endpoint\n 2. Implement retry logic for failed requests\n 3. Handle failures gracefully without blocking execution\n 4. Respect user opt-out preferences\n 5. Validate telemetry data before submission\n 6. Handle HTTP error responses appropriately\n- All tests failing as expected (module doesn't exist yet)\n- Ready to implement minimum code to make tests pass\n\nNext: Create scripts/modules/telemetry-submission.js with submitTelemetryData function\n</info added on 2025-05-28T18:27:30.207Z>\n<info added on 2025-05-28T18:43:47.334Z>\nTDD Green Phase Complete:\n- Implemented scripts/modules/telemetry-submission.js with submitTelemetryData function\n- All 6 tests now passing with full functionality implemented\n- Security measures in place: commandArgs and fullOutput filtered out before remote submission\n- Reliability features: exponential backoff retry logic (3 attempts max), graceful error handling\n- Gateway integration: configured for https://gateway.task-master.dev/telemetry endpoint\n- Zod schema validation ensures data integrity before submission\n- User privacy protected through telemetryEnabled config option\n- Smart retry logic avoids retries for 429/401/403 status codes\n- Service never throws errors and always returns result object to prevent blocking command execution\n\nImplementation ready for integration into ai-services-unified.js in subtask 90.3\n</info added on 2025-05-28T18:43:47.334Z>\n<info added on 2025-05-28T18:59:16.039Z>\nIntegration Testing Complete - Live Gateway Verification:\nSuccessfully tested telemetry submission against live gateway at localhost:4444/api/v1/telemetry. Confirmed proper authentication using Bearer token and X-User-Email headers (not X-API-Key as initially assumed). Security filtering verified working correctly - sensitive data like commandArgs, fullOutput, apiKey, and internalDebugData properly removed before submission. Gateway responded with success confirmation and assigned telemetry ID. Service handles missing GATEWAY_USER_EMAIL environment variable gracefully. All functionality validated end-to-end including retry logic, error handling, and data validation. Module ready for integration into ai-services-unified.js.\n</info added on 2025-05-28T18:59:16.039Z>\n<info added on 2025-05-29T01:04:27.886Z>\nImplementation Complete - Gateway Integration Finalized:\nHardcoded gateway endpoint to http://localhost:4444/api/v1/telemetry with config-based credential handling replacing environment variables. Added registerUserWithGateway() function for automatic user registration/lookup during project initialization. Enhanced init.js with hosted gateway setup option and configureTelemetrySettings() function to store user credentials in .taskmasterconfig under telemetry section. Updated all 10 tests to reflect new architecture - all passing. Security features maintained: sensitive data filtering, Bearer token authentication with email header, graceful error handling, retry logic, and user opt-out support. Module fully integrated and ready for ai-services-unified.js integration in subtask 90.3.\n</info added on 2025-05-29T01:04:27.886Z>\n<info added on 2025-05-30T23:36:58.010Z>\nSubtask 90.2 COMPLETED successfully! ✅\n\n## What Was Accomplished:\n\n### Config Structure Restructure\n- ✅ Restructured .taskmasterconfig to use 'account' section for user settings\n- ✅ Moved userId, userEmail, mode, telemetryEnabled from global to account section\n- ✅ Removed deprecated subscription object entirely\n- ✅ API keys remain isolated in .env file (not accessible to AI)\n- ✅ Enhanced getUserId() to always return value, never null (sets default '1234567890')\n\n### Gateway Integration Enhancements\n- ✅ Updated registerUserWithGateway() to accept both email and userId parameters\n- ✅ Enhanced /auth/init endpoint integration for existing user validation\n- ✅ API key updates automatically written to .env during registration\n\n### Code Updates\n- ✅ Updated config-manager.js with new structure and proper getter functions\n- ✅ Fixed user-management.js to use config.account structure\n- ✅ Updated telemetry-submission.js to read from account section\n- ✅ Enhanced init.js to store user settings in account section\n\n### Test Suite Fixes\n- ✅ Fixed tests/unit/config-manager.test.js for new structure\n- ✅ Updated tests/integration/init-config.test.js config paths\n- ✅ Fixed tests/unit/scripts/modules/telemetry-submission.test.js\n- ✅ Updated tests/unit/ai-services-unified.test.js mock exports\n- ✅ All tests now passing (44 tests)\n\n### Telemetry Verification\n- ✅ Confirmed telemetry system is working correctly\n- ✅ AI commands show proper telemetry output with cost/token tracking\n- ✅ User preferences (enabled/disabled) are respected\n\n## Ready for Next Subtask\nThe config foundation is now solid and consistent. Ready to move to subtask 90.3 for the next phase of telemetry improvements.\n</info added on 2025-05-30T23:36:58.010Z>", "status": "done", "dependencies": [], "parentTaskId": 90 @@ -6278,37 +6278,6 @@ ], "priority": "medium", "subtasks": [] - }, - { - "id": 94, - "title": "Implement Smart Task Dependency Analyzer and Auto-Suggestion System", - "description": "Create an intelligent system that analyzes task relationships and automatically suggests optimal dependencies when creating new tasks, leveraging AI to understand semantic connections and project context.", - "details": "This task implements a sophisticated dependency analysis system that enhances TaskMaster's task creation workflow:\n\n1. **Dependency Analysis Engine**:\n - Create `scripts/modules/dependency-analyzer.js` with semantic analysis capabilities\n - Implement task similarity scoring using natural language processing\n - Build a dependency graph analyzer that identifies potential circular dependencies\n - Add pattern recognition for common task relationship types (foundation, enhancement, integration)\n\n2. **AI-Powered Suggestion System**:\n - Integrate with existing AI services to analyze task descriptions and suggest dependencies\n - Implement context-aware suggestions based on project history and task patterns\n - Create a confidence scoring system for dependency suggestions\n - Add support for explaining why specific dependencies are recommended\n\n3. **Interactive Dependency Selection**:\n - Enhance the task creation CLI with an interactive dependency selection interface\n - Implement fuzzy search for finding related tasks by title, description, or tags\n - Add visual dependency tree preview before task creation\n - Create dependency validation with warnings for potential issues\n\n4. **Smart Context Integration**:\n - Leverage existing ContextManager system for richer task analysis\n - Implement code-aware dependency detection for technical tasks\n - Add project timeline analysis to suggest logical task ordering\n - Create dependency templates for common task patterns\n\n5. **Performance Optimization**:\n - Implement caching for dependency analysis results\n - Add incremental analysis for large task sets\n - Create background processing for complex dependency calculations\n - Optimize memory usage for projects with hundreds of tasks\n\n6. **Configuration and Customization**:\n - Add user preferences for suggestion aggressiveness and types\n - Implement project-specific dependency rules and patterns\n - Create export/import functionality for dependency templates\n - Add integration with existing telemetry system for usage analytics", - "testStrategy": "Verification approach includes multiple testing layers:\n\n1. **Unit Testing**:\n - Test dependency analysis algorithms with known task sets\n - Verify semantic similarity scoring accuracy with sample task descriptions\n - Test circular dependency detection with various graph configurations\n - Validate AI suggestion integration with mock responses\n\n2. **Integration Testing**:\n - Test end-to-end task creation workflow with dependency suggestions\n - Verify integration with existing ContextManager and AI services\n - Test performance with large task datasets (100+ tasks)\n - Validate telemetry integration and data collection\n\n3. **User Experience Testing**:\n - Test interactive CLI interface with various user input scenarios\n - Verify suggestion quality and relevance through manual review\n - Test fuzzy search functionality with partial and misspelled queries\n - Validate dependency tree visualization accuracy\n\n4. **Performance Testing**:\n - Benchmark analysis speed with increasing task set sizes\n - Test memory usage during complex dependency calculations\n - Verify caching effectiveness and cache invalidation\n - Test background processing reliability\n\n5. **Edge Case Testing**:\n - Test with projects having no existing tasks\n - Verify behavior with malformed or incomplete task data\n - Test with tasks having unusual or complex dependency patterns\n - Validate graceful degradation when AI services are unavailable\n\n6. **Acceptance Criteria**:\n - Dependency suggestions show 80%+ relevance in manual review\n - Analysis completes within 2 seconds for projects with <50 tasks\n - Interactive interface provides clear, actionable suggestions\n - System integrates seamlessly with existing task creation workflow", - "status": "pending", - "dependencies": [ - 1, - 3, - 28, - 90 - ], - "priority": "medium", - "subtasks": [] - }, - { - "id": 95, - "title": "Create Project Status Song/Lyrics Generator", - "description": "Develop a creative feature that generates songs or lyrics based on the current project status, task completion rates, and overall progress metrics.", - "details": "Implement a song/lyrics generation system that includes:\n- Create a lyrics template engine with multiple song styles (rap, ballad, folk, rock, etc.)\n- Implement project status analysis to extract key metrics (completion percentage, active tasks, blockers, recent achievements)\n- Design rhyme scheme generators and syllable counting for proper song structure\n- Create mood detection based on project health (upbeat for good progress, melancholy for delays)\n- Implement verse/chorus/bridge structure with project-specific content\n- Add task-specific verses highlighting major milestones and current challenges\n- Include team member mentions and their contributions in lyrics\n- Create CLI command `taskmaster sing` or `taskmaster lyrics` to generate and display songs\n- Support different output formats (plain text, with ASCII art, or even MIDI-like notation)\n- Add configuration options for song style preferences and content filtering\n- Implement caching to avoid regenerating identical songs for unchanged project states\n- Include Easter eggs and humor based on common development scenarios (merge conflicts, debugging sessions, etc.)", - "testStrategy": "Verify implementation by:\n- Testing song generation with various project states (empty project, partially complete, fully done, blocked tasks)\n- Validating that lyrics properly rhyme and follow chosen song structures\n- Confirming that project metrics are accurately reflected in song content\n- Testing different song styles produce appropriately different outputs\n- Verifying CLI integration works correctly with proper error handling\n- Testing with edge cases (no tasks, all tasks complete, circular dependencies)\n- Ensuring generated content is appropriate and maintains professional tone while being creative\n- Validating that song content updates when project status changes\n- Testing performance with large task sets to ensure reasonable generation times", - "status": "pending", - "dependencies": [ - 1, - 3, - 18 - ], - "priority": "medium", - "subtasks": [] } ] } \ No newline at end of file