From faae0b419d451304957d845b551d36c9c157073e Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Sat, 7 Jun 2025 20:30:51 -0400 Subject: [PATCH] chore: passes tests and linting --- .taskmaster/config.json | 64 +- scripts/modules/ai-services-unified.js | 1064 ++--- scripts/modules/dependency-manager.js | 1976 ++++----- scripts/modules/supported-models.json | 850 ++-- scripts/modules/task-manager/add-task.js | 2190 +++++----- .../modules/task-manager/clear-subtasks.js | 252 +- scripts/modules/task-manager/list-tasks.js | 1332 +++--- scripts/modules/task-manager/models.js | 1038 ++--- .../modules/task-manager/set-task-status.js | 186 +- scripts/modules/ui.js | 3568 ++++++++--------- src/ai-providers/base-provider.js | 364 +- .../modules/task-manager/add-task.test.js | 668 +-- tests/unit/ui.test.js | 6 +- 13 files changed, 6781 insertions(+), 6777 deletions(-) diff --git a/.taskmaster/config.json b/.taskmaster/config.json index 442dfc1c..a61d10d5 100644 --- a/.taskmaster/config.json +++ b/.taskmaster/config.json @@ -1,33 +1,33 @@ { - "models": { - "main": { - "provider": "anthropic", - "modelId": "claude-sonnet-4-20250514", - "maxTokens": 50000, - "temperature": 0.2 - }, - "research": { - "provider": "perplexity", - "modelId": "sonar-pro", - "maxTokens": 8700, - "temperature": 0.1 - }, - "fallback": { - "provider": "anthropic", - "modelId": "claude-3-7-sonnet-20250219", - "maxTokens": 128000, - "temperature": 0.2 - } - }, - "global": { - "logLevel": "info", - "debug": false, - "defaultSubtasks": 5, - "defaultPriority": "medium", - "projectName": "Taskmaster", - "ollamaBaseURL": "http://localhost:11434/api", - "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", - "userId": "1234567890", - "azureBaseURL": "https://your-endpoint.azure.com/" - } -} \ No newline at end of file + "models": { + "main": { + "provider": "anthropic", + "modelId": "claude-sonnet-4-20250514", + "maxTokens": 50000, + "temperature": 0.2 + }, + "research": { + "provider": "perplexity", + "modelId": "sonar-pro", + "maxTokens": 8700, + "temperature": 0.1 + }, + "fallback": { + "provider": "anthropic", + "modelId": "claude-3-7-sonnet-20250219", + "maxTokens": 128000, + "temperature": 0.2 + } + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Taskmaster", + "ollamaBaseURL": "http://localhost:11434/api", + "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "userId": "1234567890", + "azureBaseURL": "https://your-endpoint.azure.com/" + } +} diff --git a/scripts/modules/ai-services-unified.js b/scripts/modules/ai-services-unified.js index adcad88d..cd16682a 100644 --- a/scripts/modules/ai-services-unified.js +++ b/scripts/modules/ai-services-unified.js @@ -8,82 +8,82 @@ // --- Core Dependencies --- import { - getMainProvider, - getMainModelId, - getResearchProvider, - getResearchModelId, - getFallbackProvider, - getFallbackModelId, - getParametersForRole, - getUserId, - MODEL_MAP, - getDebugFlag, - getBaseUrlForRole, - isApiKeySet, - getOllamaBaseURL, - getAzureBaseURL, - getBedrockBaseURL, - getVertexProjectId, - getVertexLocation, -} from "./config-manager.js"; -import { log, findProjectRoot, resolveEnvVariable } from "./utils.js"; + getMainProvider, + getMainModelId, + getResearchProvider, + getResearchModelId, + getFallbackProvider, + getFallbackModelId, + getParametersForRole, + getUserId, + MODEL_MAP, + getDebugFlag, + getBaseUrlForRole, + isApiKeySet, + getOllamaBaseURL, + getAzureBaseURL, + getBedrockBaseURL, + getVertexProjectId, + getVertexLocation +} from './config-manager.js'; +import { log, findProjectRoot, resolveEnvVariable } from './utils.js'; // Import provider classes import { - AnthropicAIProvider, - PerplexityAIProvider, - GoogleAIProvider, - OpenAIProvider, - XAIProvider, - OpenRouterAIProvider, - OllamaAIProvider, - BedrockAIProvider, - AzureProvider, - VertexAIProvider, -} from "../../src/ai-providers/index.js"; + AnthropicAIProvider, + PerplexityAIProvider, + GoogleAIProvider, + OpenAIProvider, + XAIProvider, + OpenRouterAIProvider, + OllamaAIProvider, + BedrockAIProvider, + AzureProvider, + VertexAIProvider +} from '../../src/ai-providers/index.js'; // Create provider instances const PROVIDERS = { - anthropic: new AnthropicAIProvider(), - perplexity: new PerplexityAIProvider(), - google: new GoogleAIProvider(), - openai: new OpenAIProvider(), - xai: new XAIProvider(), - openrouter: new OpenRouterAIProvider(), - ollama: new OllamaAIProvider(), - bedrock: new BedrockAIProvider(), - azure: new AzureProvider(), - vertex: new VertexAIProvider(), + anthropic: new AnthropicAIProvider(), + perplexity: new PerplexityAIProvider(), + google: new GoogleAIProvider(), + openai: new OpenAIProvider(), + xai: new XAIProvider(), + openrouter: new OpenRouterAIProvider(), + ollama: new OllamaAIProvider(), + bedrock: new BedrockAIProvider(), + azure: new AzureProvider(), + vertex: new VertexAIProvider() }; // Helper function to get cost for a specific model function _getCostForModel(providerName, modelId) { - if (!MODEL_MAP || !MODEL_MAP[providerName]) { - log( - "warn", - `Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.` - ); - return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost - } + if (!MODEL_MAP || !MODEL_MAP[providerName]) { + log( + 'warn', + `Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.` + ); + return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost + } - const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId); + const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId); - if (!modelData || !modelData.cost_per_1m_tokens) { - log( - "debug", - `Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.` - ); - return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost - } + if (!modelData || !modelData.cost_per_1m_tokens) { + log( + 'debug', + `Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.` + ); + return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost + } - // Ensure currency is part of the returned object, defaulting if not present - const currency = modelData.cost_per_1m_tokens.currency || "USD"; + // Ensure currency is part of the returned object, defaulting if not present + const currency = modelData.cost_per_1m_tokens.currency || 'USD'; - return { - inputCost: modelData.cost_per_1m_tokens.input || 0, - outputCost: modelData.cost_per_1m_tokens.output || 0, - currency: currency, - }; + return { + inputCost: modelData.cost_per_1m_tokens.input || 0, + outputCost: modelData.cost_per_1m_tokens.output || 0, + currency: currency + }; } // --- Configuration for Retries --- @@ -92,16 +92,16 @@ const INITIAL_RETRY_DELAY_MS = 1000; // Helper function to check if an error is retryable function isRetryableError(error) { - const errorMessage = error.message?.toLowerCase() || ""; - return ( - errorMessage.includes("rate limit") || - errorMessage.includes("overloaded") || - errorMessage.includes("service temporarily unavailable") || - errorMessage.includes("timeout") || - errorMessage.includes("network error") || - error.status === 429 || - error.status >= 500 - ); + const errorMessage = error.message?.toLowerCase() || ''; + return ( + errorMessage.includes('rate limit') || + errorMessage.includes('overloaded') || + errorMessage.includes('service temporarily unavailable') || + errorMessage.includes('timeout') || + errorMessage.includes('network error') || + error.status === 429 || + error.status >= 500 + ); } /** @@ -111,45 +111,45 @@ function isRetryableError(error) { * @returns {string} A concise error message. */ function _extractErrorMessage(error) { - try { - // Attempt 1: Look for Vercel SDK specific nested structure (common) - if (error?.data?.error?.message) { - return error.data.error.message; - } + try { + // Attempt 1: Look for Vercel SDK specific nested structure (common) + if (error?.data?.error?.message) { + return error.data.error.message; + } - // Attempt 2: Look for nested error message directly in the error object - if (error?.error?.message) { - return error.error.message; - } + // Attempt 2: Look for nested error message directly in the error object + if (error?.error?.message) { + return error.error.message; + } - // Attempt 3: Look for nested error message in response body if it's JSON string - if (typeof error?.responseBody === "string") { - try { - const body = JSON.parse(error.responseBody); - if (body?.error?.message) { - return body.error.message; - } - } catch (parseError) { - // Ignore if responseBody is not valid JSON - } - } + // Attempt 3: Look for nested error message in response body if it's JSON string + if (typeof error?.responseBody === 'string') { + try { + const body = JSON.parse(error.responseBody); + if (body?.error?.message) { + return body.error.message; + } + } catch (parseError) { + // Ignore if responseBody is not valid JSON + } + } - // Attempt 4: Use the top-level message if it exists - if (typeof error?.message === "string" && error.message) { - return error.message; - } + // Attempt 4: Use the top-level message if it exists + if (typeof error?.message === 'string' && error.message) { + return error.message; + } - // Attempt 5: Handle simple string errors - if (typeof error === "string") { - return error; - } + // Attempt 5: Handle simple string errors + if (typeof error === 'string') { + return error; + } - // Fallback - return "An unknown AI service error occurred."; - } catch (e) { - // Safety net - return "Failed to extract error message."; - } + // Fallback + return 'An unknown AI service error occurred.'; + } catch (e) { + // Safety net + return 'Failed to extract error message.'; + } } /** @@ -161,40 +161,40 @@ function _extractErrorMessage(error) { * @throws {Error} If a required API key is missing. */ function _resolveApiKey(providerName, session, projectRoot = null) { - const keyMap = { - openai: "OPENAI_API_KEY", - anthropic: "ANTHROPIC_API_KEY", - google: "GOOGLE_API_KEY", - perplexity: "PERPLEXITY_API_KEY", - mistral: "MISTRAL_API_KEY", - azure: "AZURE_OPENAI_API_KEY", - openrouter: "OPENROUTER_API_KEY", - xai: "XAI_API_KEY", - ollama: "OLLAMA_API_KEY", - bedrock: "AWS_ACCESS_KEY_ID", - vertex: "GOOGLE_API_KEY", - }; + const keyMap = { + openai: 'OPENAI_API_KEY', + anthropic: 'ANTHROPIC_API_KEY', + google: 'GOOGLE_API_KEY', + perplexity: 'PERPLEXITY_API_KEY', + mistral: 'MISTRAL_API_KEY', + azure: 'AZURE_OPENAI_API_KEY', + openrouter: 'OPENROUTER_API_KEY', + xai: 'XAI_API_KEY', + ollama: 'OLLAMA_API_KEY', + bedrock: 'AWS_ACCESS_KEY_ID', + vertex: 'GOOGLE_API_KEY' + }; - const envVarName = keyMap[providerName]; - if (!envVarName) { - throw new Error( - `Unknown provider '${providerName}' for API key resolution.` - ); - } + const envVarName = keyMap[providerName]; + if (!envVarName) { + throw new Error( + `Unknown provider '${providerName}' for API key resolution.` + ); + } - const apiKey = resolveEnvVariable(envVarName, session, projectRoot); + const apiKey = resolveEnvVariable(envVarName, session, projectRoot); - // Special handling for providers that can use alternative auth - if (providerName === "ollama" || providerName === "bedrock") { - return apiKey || null; - } + // Special handling for providers that can use alternative auth + if (providerName === 'ollama' || providerName === 'bedrock') { + return apiKey || null; + } - if (!apiKey) { - throw new Error( - `Required API key ${envVarName} for provider '${providerName}' is not set in environment, session, or .env file.` - ); - } - return apiKey; + if (!apiKey) { + throw new Error( + `Required API key ${envVarName} for provider '${providerName}' is not set in environment, session, or .env file.` + ); + } + return apiKey; } /** @@ -209,62 +209,62 @@ function _resolveApiKey(providerName, session, projectRoot = null) { * @throws {Error} If the call fails after all retries. */ async function _attemptProviderCallWithRetries( - provider, - serviceType, - callParams, - providerName, - modelId, - attemptRole + provider, + serviceType, + callParams, + providerName, + modelId, + attemptRole ) { - let retries = 0; - const fnName = serviceType; + let retries = 0; + const fnName = serviceType; - while (retries <= MAX_RETRIES) { - try { - if (getDebugFlag()) { - log( - "info", - `Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})` - ); - } + while (retries <= MAX_RETRIES) { + try { + if (getDebugFlag()) { + log( + 'info', + `Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})` + ); + } - // Call the appropriate method on the provider instance - const result = await provider[serviceType](callParams); + // Call the appropriate method on the provider instance + const result = await provider[serviceType](callParams); - if (getDebugFlag()) { - log( - "info", - `${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}` - ); - } - return result; - } catch (error) { - log( - "warn", - `Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}` - ); + if (getDebugFlag()) { + log( + 'info', + `${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}` + ); + } + return result; + } catch (error) { + log( + 'warn', + `Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}` + ); - if (isRetryableError(error) && retries < MAX_RETRIES) { - retries++; - const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1); - log( - "info", - `Something went wrong on the provider side. Retrying in ${delay / 1000}s...` - ); - await new Promise((resolve) => setTimeout(resolve, delay)); - } else { - log( - "error", - `Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).` - ); - throw error; - } - } - } - // Should not be reached due to throw in the else block - throw new Error( - `Exhausted all retries for role ${attemptRole} (${fnName} / ${providerName})` - ); + if (isRetryableError(error) && retries < MAX_RETRIES) { + retries++; + const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1); + log( + 'info', + `Something went wrong on the provider side. Retrying in ${delay / 1000}s...` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + log( + 'error', + `Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).` + ); + throw error; + } + } + } + // Should not be reached due to throw in the else block + throw new Error( + `Exhausted all retries for role ${attemptRole} (${fnName} / ${providerName})` + ); } /** @@ -283,313 +283,313 @@ async function _attemptProviderCallWithRetries( * @returns {Promise} Result from the underlying provider call. */ async function _unifiedServiceRunner(serviceType, params) { - const { - role: initialRole, - session, - projectRoot, - systemPrompt, - prompt, - schema, - objectName, - commandName, - outputType, - ...restApiParams - } = params; - if (getDebugFlag()) { - log("info", `${serviceType}Service called`, { - role: initialRole, - commandName, - outputType, - projectRoot, - }); - } + const { + role: initialRole, + session, + projectRoot, + systemPrompt, + prompt, + schema, + objectName, + commandName, + outputType, + ...restApiParams + } = params; + if (getDebugFlag()) { + log('info', `${serviceType}Service called`, { + role: initialRole, + commandName, + outputType, + projectRoot + }); + } - const effectiveProjectRoot = projectRoot || findProjectRoot(); - const userId = getUserId(effectiveProjectRoot); + const effectiveProjectRoot = projectRoot || findProjectRoot(); + const userId = getUserId(effectiveProjectRoot); - let sequence; - if (initialRole === "main") { - sequence = ["main", "fallback", "research"]; - } else if (initialRole === "research") { - sequence = ["research", "fallback", "main"]; - } else if (initialRole === "fallback") { - sequence = ["fallback", "main", "research"]; - } else { - log( - "warn", - `Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.` - ); - sequence = ["main", "fallback", "research"]; - } + let sequence; + if (initialRole === 'main') { + sequence = ['main', 'fallback', 'research']; + } else if (initialRole === 'research') { + sequence = ['research', 'fallback', 'main']; + } else if (initialRole === 'fallback') { + sequence = ['fallback', 'main', 'research']; + } else { + log( + 'warn', + `Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.` + ); + sequence = ['main', 'fallback', 'research']; + } - let lastError = null; - let lastCleanErrorMessage = - "AI service call failed for all configured roles."; + let lastError = null; + let lastCleanErrorMessage = + 'AI service call failed for all configured roles.'; - for (const currentRole of sequence) { - let providerName, - modelId, - apiKey, - roleParams, - provider, - baseURL, - providerResponse, - telemetryData = null; + for (const currentRole of sequence) { + let providerName, + modelId, + apiKey, + roleParams, + provider, + baseURL, + providerResponse, + telemetryData = null; - try { - log("info", `New AI service call with role: ${currentRole}`); + try { + log('info', `New AI service call with role: ${currentRole}`); - if (currentRole === "main") { - providerName = getMainProvider(effectiveProjectRoot); - modelId = getMainModelId(effectiveProjectRoot); - } else if (currentRole === "research") { - providerName = getResearchProvider(effectiveProjectRoot); - modelId = getResearchModelId(effectiveProjectRoot); - } else if (currentRole === "fallback") { - providerName = getFallbackProvider(effectiveProjectRoot); - modelId = getFallbackModelId(effectiveProjectRoot); - } else { - log( - "error", - `Unknown role encountered in _unifiedServiceRunner: ${currentRole}` - ); - lastError = - lastError || new Error(`Unknown AI role specified: ${currentRole}`); - continue; - } + if (currentRole === 'main') { + providerName = getMainProvider(effectiveProjectRoot); + modelId = getMainModelId(effectiveProjectRoot); + } else if (currentRole === 'research') { + providerName = getResearchProvider(effectiveProjectRoot); + modelId = getResearchModelId(effectiveProjectRoot); + } else if (currentRole === 'fallback') { + providerName = getFallbackProvider(effectiveProjectRoot); + modelId = getFallbackModelId(effectiveProjectRoot); + } else { + log( + 'error', + `Unknown role encountered in _unifiedServiceRunner: ${currentRole}` + ); + lastError = + lastError || new Error(`Unknown AI role specified: ${currentRole}`); + continue; + } - if (!providerName || !modelId) { - log( - "warn", - `Skipping role '${currentRole}': Provider or Model ID not configured.` - ); - lastError = - lastError || - new Error( - `Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}` - ); - continue; - } + if (!providerName || !modelId) { + log( + 'warn', + `Skipping role '${currentRole}': Provider or Model ID not configured.` + ); + lastError = + lastError || + new Error( + `Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}` + ); + continue; + } - // Get provider instance - provider = PROVIDERS[providerName?.toLowerCase()]; - if (!provider) { - log( - "warn", - `Skipping role '${currentRole}': Provider '${providerName}' not supported.` - ); - lastError = - lastError || - new Error(`Unsupported provider configured: ${providerName}`); - continue; - } + // Get provider instance + provider = PROVIDERS[providerName?.toLowerCase()]; + if (!provider) { + log( + 'warn', + `Skipping role '${currentRole}': Provider '${providerName}' not supported.` + ); + lastError = + lastError || + new Error(`Unsupported provider configured: ${providerName}`); + continue; + } - // Check API key if needed - if (providerName?.toLowerCase() !== "ollama") { - if (!isApiKeySet(providerName, session, effectiveProjectRoot)) { - log( - "warn", - `Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.` - ); - lastError = - lastError || - new Error( - `API key for provider '${providerName}' (role: ${currentRole}) is not set.` - ); - continue; // Skip to the next role in the sequence - } - } + // Check API key if needed + if (providerName?.toLowerCase() !== 'ollama') { + if (!isApiKeySet(providerName, session, effectiveProjectRoot)) { + log( + 'warn', + `Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.` + ); + lastError = + lastError || + new Error( + `API key for provider '${providerName}' (role: ${currentRole}) is not set.` + ); + continue; // Skip to the next role in the sequence + } + } - // Get base URL if configured (optional for most providers) - baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot); + // Get base URL if configured (optional for most providers) + baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot); - // For Azure, use the global Azure base URL if role-specific URL is not configured - if (providerName?.toLowerCase() === "azure" && !baseURL) { - baseURL = getAzureBaseURL(effectiveProjectRoot); - log("debug", `Using global Azure base URL: ${baseURL}`); - } else if (providerName?.toLowerCase() === "ollama" && !baseURL) { - // For Ollama, use the global Ollama base URL if role-specific URL is not configured - baseURL = getOllamaBaseURL(effectiveProjectRoot); - log("debug", `Using global Ollama base URL: ${baseURL}`); - } else if (providerName?.toLowerCase() === "bedrock" && !baseURL) { - // For Bedrock, use the global Bedrock base URL if role-specific URL is not configured - baseURL = getBedrockBaseURL(effectiveProjectRoot); - log("debug", `Using global Bedrock base URL: ${baseURL}`); - } + // For Azure, use the global Azure base URL if role-specific URL is not configured + if (providerName?.toLowerCase() === 'azure' && !baseURL) { + baseURL = getAzureBaseURL(effectiveProjectRoot); + log('debug', `Using global Azure base URL: ${baseURL}`); + } else if (providerName?.toLowerCase() === 'ollama' && !baseURL) { + // For Ollama, use the global Ollama base URL if role-specific URL is not configured + baseURL = getOllamaBaseURL(effectiveProjectRoot); + log('debug', `Using global Ollama base URL: ${baseURL}`); + } else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) { + // For Bedrock, use the global Bedrock base URL if role-specific URL is not configured + baseURL = getBedrockBaseURL(effectiveProjectRoot); + log('debug', `Using global Bedrock base URL: ${baseURL}`); + } - // Get AI parameters for the current role - roleParams = getParametersForRole(currentRole, effectiveProjectRoot); - apiKey = _resolveApiKey( - providerName?.toLowerCase(), - session, - effectiveProjectRoot - ); + // Get AI parameters for the current role + roleParams = getParametersForRole(currentRole, effectiveProjectRoot); + apiKey = _resolveApiKey( + providerName?.toLowerCase(), + session, + effectiveProjectRoot + ); - // Prepare provider-specific configuration - let providerSpecificParams = {}; + // Prepare provider-specific configuration + let providerSpecificParams = {}; - // Handle Vertex AI specific configuration - if (providerName?.toLowerCase() === "vertex") { - // Get Vertex project ID and location - const projectId = - getVertexProjectId(effectiveProjectRoot) || - resolveEnvVariable( - "VERTEX_PROJECT_ID", - session, - effectiveProjectRoot - ); + // Handle Vertex AI specific configuration + if (providerName?.toLowerCase() === 'vertex') { + // Get Vertex project ID and location + const projectId = + getVertexProjectId(effectiveProjectRoot) || + resolveEnvVariable( + 'VERTEX_PROJECT_ID', + session, + effectiveProjectRoot + ); - const location = - getVertexLocation(effectiveProjectRoot) || - resolveEnvVariable( - "VERTEX_LOCATION", - session, - effectiveProjectRoot - ) || - "us-central1"; + const location = + getVertexLocation(effectiveProjectRoot) || + resolveEnvVariable( + 'VERTEX_LOCATION', + session, + effectiveProjectRoot + ) || + 'us-central1'; - // Get credentials path if available - const credentialsPath = resolveEnvVariable( - "GOOGLE_APPLICATION_CREDENTIALS", - session, - effectiveProjectRoot - ); + // Get credentials path if available + const credentialsPath = resolveEnvVariable( + 'GOOGLE_APPLICATION_CREDENTIALS', + session, + effectiveProjectRoot + ); - // Add Vertex-specific parameters - providerSpecificParams = { - projectId, - location, - ...(credentialsPath && { credentials: { credentialsFromEnv: true } }), - }; + // Add Vertex-specific parameters + providerSpecificParams = { + projectId, + location, + ...(credentialsPath && { credentials: { credentialsFromEnv: true } }) + }; - log( - "debug", - `Using Vertex AI configuration: Project ID=${projectId}, Location=${location}` - ); - } + log( + 'debug', + `Using Vertex AI configuration: Project ID=${projectId}, Location=${location}` + ); + } - const messages = []; - if (systemPrompt) { - messages.push({ role: "system", content: systemPrompt }); - } + const messages = []; + if (systemPrompt) { + messages.push({ role: 'system', content: systemPrompt }); + } - // IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS - // { - // type: 'text', - // text: 'Large cached context here like a tasks json', - // providerOptions: { - // anthropic: { cacheControl: { type: 'ephemeral' } } - // } - // } + // IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS + // { + // type: 'text', + // text: 'Large cached context here like a tasks json', + // providerOptions: { + // anthropic: { cacheControl: { type: 'ephemeral' } } + // } + // } - // Example - // if (params.context) { // context is a json string of a tasks object or some other stu - // messages.push({ - // type: 'text', - // text: params.context, - // providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } } - // }); - // } + // Example + // if (params.context) { // context is a json string of a tasks object or some other stu + // messages.push({ + // type: 'text', + // text: params.context, + // providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } } + // }); + // } - if (prompt) { - messages.push({ role: "user", content: prompt }); - } else { - throw new Error("User prompt content is missing."); - } + if (prompt) { + messages.push({ role: 'user', content: prompt }); + } else { + throw new Error('User prompt content is missing.'); + } - const callParams = { - apiKey, - modelId, - maxTokens: roleParams.maxTokens, - temperature: roleParams.temperature, - messages, - ...(baseURL && { baseURL }), - ...(serviceType === "generateObject" && { schema, objectName }), - ...providerSpecificParams, - ...restApiParams, - }; + const callParams = { + apiKey, + modelId, + maxTokens: roleParams.maxTokens, + temperature: roleParams.temperature, + messages, + ...(baseURL && { baseURL }), + ...(serviceType === 'generateObject' && { schema, objectName }), + ...providerSpecificParams, + ...restApiParams + }; - providerResponse = await _attemptProviderCallWithRetries( - provider, - serviceType, - callParams, - providerName, - modelId, - currentRole - ); + providerResponse = await _attemptProviderCallWithRetries( + provider, + serviceType, + callParams, + providerName, + modelId, + currentRole + ); - if (userId && providerResponse && providerResponse.usage) { - try { - telemetryData = await logAiUsage({ - userId, - commandName, - providerName, - modelId, - inputTokens: providerResponse.usage.inputTokens, - outputTokens: providerResponse.usage.outputTokens, - outputType, - }); - } catch (telemetryError) { - // logAiUsage already logs its own errors and returns null on failure - // No need to log again here, telemetryData will remain null - } - } else if (userId && providerResponse && !providerResponse.usage) { - log( - "warn", - `Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)` - ); - } + if (userId && providerResponse && providerResponse.usage) { + try { + telemetryData = await logAiUsage({ + userId, + commandName, + providerName, + modelId, + inputTokens: providerResponse.usage.inputTokens, + outputTokens: providerResponse.usage.outputTokens, + outputType + }); + } catch (telemetryError) { + // logAiUsage already logs its own errors and returns null on failure + // No need to log again here, telemetryData will remain null + } + } else if (userId && providerResponse && !providerResponse.usage) { + log( + 'warn', + `Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)` + ); + } - let finalMainResult; - if (serviceType === "generateText") { - finalMainResult = providerResponse.text; - } else if (serviceType === "generateObject") { - finalMainResult = providerResponse.object; - } else if (serviceType === "streamText") { - finalMainResult = providerResponse; - } else { - log( - "error", - `Unknown serviceType in _unifiedServiceRunner: ${serviceType}` - ); - finalMainResult = providerResponse; - } + let finalMainResult; + if (serviceType === 'generateText') { + finalMainResult = providerResponse.text; + } else if (serviceType === 'generateObject') { + finalMainResult = providerResponse.object; + } else if (serviceType === 'streamText') { + finalMainResult = providerResponse; + } else { + log( + 'error', + `Unknown serviceType in _unifiedServiceRunner: ${serviceType}` + ); + finalMainResult = providerResponse; + } - return { - mainResult: finalMainResult, - telemetryData: telemetryData, - }; - } catch (error) { - const cleanMessage = _extractErrorMessage(error); - log( - "error", - `Service call failed for role ${currentRole} (Provider: ${providerName || "unknown"}, Model: ${modelId || "unknown"}): ${cleanMessage}` - ); - lastError = error; - lastCleanErrorMessage = cleanMessage; + return { + mainResult: finalMainResult, + telemetryData: telemetryData + }; + } catch (error) { + const cleanMessage = _extractErrorMessage(error); + log( + 'error', + `Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}` + ); + lastError = error; + lastCleanErrorMessage = cleanMessage; - if (serviceType === "generateObject") { - const lowerCaseMessage = cleanMessage.toLowerCase(); - if ( - lowerCaseMessage.includes( - "no endpoints found that support tool use" - ) || - lowerCaseMessage.includes("does not support tool_use") || - lowerCaseMessage.includes("tool use is not supported") || - lowerCaseMessage.includes("tools are not supported") || - lowerCaseMessage.includes("function calling is not supported") || - lowerCaseMessage.includes("tool use is not supported") - ) { - const specificErrorMsg = `Model '${modelId || "unknown"}' via provider '${providerName || "unknown"}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`; - log("error", `[Tool Support Error] ${specificErrorMsg}`); - throw new Error(specificErrorMsg); - } - } - } - } + if (serviceType === 'generateObject') { + const lowerCaseMessage = cleanMessage.toLowerCase(); + if ( + lowerCaseMessage.includes( + 'no endpoints found that support tool use' + ) || + lowerCaseMessage.includes('does not support tool_use') || + lowerCaseMessage.includes('tool use is not supported') || + lowerCaseMessage.includes('tools are not supported') || + lowerCaseMessage.includes('function calling is not supported') || + lowerCaseMessage.includes('tool use is not supported') + ) { + const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`; + log('error', `[Tool Support Error] ${specificErrorMsg}`); + throw new Error(specificErrorMsg); + } + } + } + } - log("error", `All roles in the sequence [${sequence.join(", ")}] failed.`); - throw new Error(lastCleanErrorMessage); + log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`); + throw new Error(lastCleanErrorMessage); } /** @@ -607,11 +607,11 @@ async function _unifiedServiceRunner(serviceType, params) { * @returns {Promise} Result object containing generated text and usage data. */ async function generateTextService(params) { - // Ensure default outputType if not provided - const defaults = { outputType: "cli" }; - const combinedParams = { ...defaults, ...params }; - // TODO: Validate commandName exists? - return _unifiedServiceRunner("generateText", combinedParams); + // Ensure default outputType if not provided + const defaults = { outputType: 'cli' }; + const combinedParams = { ...defaults, ...params }; + // TODO: Validate commandName exists? + return _unifiedServiceRunner('generateText', combinedParams); } /** @@ -629,13 +629,13 @@ async function generateTextService(params) { * @returns {Promise} Result object containing the stream and usage data. */ async function streamTextService(params) { - const defaults = { outputType: "cli" }; - const combinedParams = { ...defaults, ...params }; - // TODO: Validate commandName exists? - // NOTE: Telemetry for streaming might be tricky as usage data often comes at the end. - // The current implementation logs *after* the stream is returned. - // We might need to adjust how usage is captured/logged for streams. - return _unifiedServiceRunner("streamText", combinedParams); + const defaults = { outputType: 'cli' }; + const combinedParams = { ...defaults, ...params }; + // TODO: Validate commandName exists? + // NOTE: Telemetry for streaming might be tricky as usage data often comes at the end. + // The current implementation logs *after* the stream is returned. + // We might need to adjust how usage is captured/logged for streams. + return _unifiedServiceRunner('streamText', combinedParams); } /** @@ -656,14 +656,14 @@ async function streamTextService(params) { * @returns {Promise} Result object containing the generated object and usage data. */ async function generateObjectService(params) { - const defaults = { - objectName: "generated_object", - maxRetries: 3, - outputType: "cli", - }; - const combinedParams = { ...defaults, ...params }; - // TODO: Validate commandName exists? - return _unifiedServiceRunner("generateObject", combinedParams); + const defaults = { + objectName: 'generated_object', + maxRetries: 3, + outputType: 'cli' + }; + const combinedParams = { ...defaults, ...params }; + // TODO: Validate commandName exists? + return _unifiedServiceRunner('generateObject', combinedParams); } // --- Telemetry Function --- @@ -679,61 +679,61 @@ async function generateObjectService(params) { * @param {number} params.outputTokens - Number of output tokens. */ async function logAiUsage({ - userId, - commandName, - providerName, - modelId, - inputTokens, - outputTokens, - outputType, + userId, + commandName, + providerName, + modelId, + inputTokens, + outputTokens, + outputType }) { - try { - const isMCP = outputType === "mcp"; - const timestamp = new Date().toISOString(); - const totalTokens = (inputTokens || 0) + (outputTokens || 0); + try { + const isMCP = outputType === 'mcp'; + const timestamp = new Date().toISOString(); + const totalTokens = (inputTokens || 0) + (outputTokens || 0); - // Destructure currency along with costs - const { inputCost, outputCost, currency } = _getCostForModel( - providerName, - modelId - ); + // Destructure currency along with costs + const { inputCost, outputCost, currency } = _getCostForModel( + providerName, + modelId + ); - const totalCost = - ((inputTokens || 0) / 1_000_000) * inputCost + - ((outputTokens || 0) / 1_000_000) * outputCost; + const totalCost = + ((inputTokens || 0) / 1_000_000) * inputCost + + ((outputTokens || 0) / 1_000_000) * outputCost; - const telemetryData = { - timestamp, - userId, - commandName, - modelUsed: modelId, // Consistent field name from requirements - providerName, // Keep provider name for context - inputTokens: inputTokens || 0, - outputTokens: outputTokens || 0, - totalTokens, - totalCost: parseFloat(totalCost.toFixed(6)), - currency, // Add currency to the telemetry data - }; + const telemetryData = { + timestamp, + userId, + commandName, + modelUsed: modelId, // Consistent field name from requirements + providerName, // Keep provider name for context + inputTokens: inputTokens || 0, + outputTokens: outputTokens || 0, + totalTokens, + totalCost: parseFloat(totalCost.toFixed(6)), + currency // Add currency to the telemetry data + }; - if (getDebugFlag()) { - log("info", "AI Usage Telemetry:", telemetryData); - } + if (getDebugFlag()) { + log('info', 'AI Usage Telemetry:', telemetryData); + } - // TODO (Subtask 77.2): Send telemetryData securely to the external endpoint. + // TODO (Subtask 77.2): Send telemetryData securely to the external endpoint. - return telemetryData; - } catch (error) { - log("error", `Failed to log AI usage telemetry: ${error.message}`, { - error, - }); - // Don't re-throw; telemetry failure shouldn't block core functionality. - return null; - } + return telemetryData; + } catch (error) { + log('error', `Failed to log AI usage telemetry: ${error.message}`, { + error + }); + // Don't re-throw; telemetry failure shouldn't block core functionality. + return null; + } } export { - generateTextService, - streamTextService, - generateObjectService, - logAiUsage, + generateTextService, + streamTextService, + generateObjectService, + logAiUsage }; diff --git a/scripts/modules/dependency-manager.js b/scripts/modules/dependency-manager.js index a6417780..9b556143 100644 --- a/scripts/modules/dependency-manager.js +++ b/scripts/modules/dependency-manager.js @@ -3,23 +3,23 @@ * Manages task dependencies and relationships */ -import path from "path"; -import chalk from "chalk"; -import boxen from "boxen"; +import path from 'path'; +import chalk from 'chalk'; +import boxen from 'boxen'; import { - log, - readJSON, - writeJSON, - taskExists, - formatTaskId, - findCycles, - isSilentMode, -} from "./utils.js"; + log, + readJSON, + writeJSON, + taskExists, + formatTaskId, + findCycles, + isSilentMode +} from './utils.js'; -import { displayBanner } from "./ui.js"; +import { displayBanner } from './ui.js'; -import { generateTaskFiles } from "./task-manager.js"; +import { generateTaskFiles } from './task-manager.js'; /** * Add a dependency to a task @@ -28,183 +28,183 @@ import { generateTaskFiles } from "./task-manager.js"; * @param {number|string} dependencyId - ID of the task to add as dependency */ async function addDependency(tasksPath, taskId, dependencyId) { - log("info", `Adding dependency ${dependencyId} to task ${taskId}...`); + log('info', `Adding dependency ${dependencyId} to task ${taskId}...`); - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log("error", "No valid tasks found in tasks.json"); - process.exit(1); - } + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log('error', 'No valid tasks found in tasks.json'); + process.exit(1); + } - // Format the task and dependency IDs correctly - const formattedTaskId = - typeof taskId === "string" && taskId.includes(".") - ? taskId - : parseInt(taskId, 10); + // Format the task and dependency IDs correctly + const formattedTaskId = + typeof taskId === 'string' && taskId.includes('.') + ? taskId + : parseInt(taskId, 10); - const formattedDependencyId = formatTaskId(dependencyId); + const formattedDependencyId = formatTaskId(dependencyId); - // Check if the dependency task or subtask actually exists - if (!taskExists(data.tasks, formattedDependencyId)) { - log( - "error", - `Dependency target ${formattedDependencyId} does not exist in tasks.json` - ); - process.exit(1); - } + // Check if the dependency task or subtask actually exists + if (!taskExists(data.tasks, formattedDependencyId)) { + log( + 'error', + `Dependency target ${formattedDependencyId} does not exist in tasks.json` + ); + process.exit(1); + } - // Find the task to update - let targetTask = null; - let isSubtask = false; + // Find the task to update + let targetTask = null; + let isSubtask = false; - if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) { - // Handle dot notation for subtasks (e.g., "1.2") - const [parentId, subtaskId] = formattedTaskId - .split(".") - .map((id) => parseInt(id, 10)); - const parentTask = data.tasks.find((t) => t.id === parentId); + if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { + // Handle dot notation for subtasks (e.g., "1.2") + const [parentId, subtaskId] = formattedTaskId + .split('.') + .map((id) => parseInt(id, 10)); + const parentTask = data.tasks.find((t) => t.id === parentId); - if (!parentTask) { - log("error", `Parent task ${parentId} not found.`); - process.exit(1); - } + if (!parentTask) { + log('error', `Parent task ${parentId} not found.`); + process.exit(1); + } - if (!parentTask.subtasks) { - log("error", `Parent task ${parentId} has no subtasks.`); - process.exit(1); - } + if (!parentTask.subtasks) { + log('error', `Parent task ${parentId} has no subtasks.`); + process.exit(1); + } - targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); - isSubtask = true; + targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); + isSubtask = true; - if (!targetTask) { - log("error", `Subtask ${formattedTaskId} not found.`); - process.exit(1); - } - } else { - // Regular task (not a subtask) - targetTask = data.tasks.find((t) => t.id === formattedTaskId); + if (!targetTask) { + log('error', `Subtask ${formattedTaskId} not found.`); + process.exit(1); + } + } else { + // Regular task (not a subtask) + targetTask = data.tasks.find((t) => t.id === formattedTaskId); - if (!targetTask) { - log("error", `Task ${formattedTaskId} not found.`); - process.exit(1); - } - } + if (!targetTask) { + log('error', `Task ${formattedTaskId} not found.`); + process.exit(1); + } + } - // Initialize dependencies array if it doesn't exist - if (!targetTask.dependencies) { - targetTask.dependencies = []; - } + // Initialize dependencies array if it doesn't exist + if (!targetTask.dependencies) { + targetTask.dependencies = []; + } - // Check if dependency already exists - if ( - targetTask.dependencies.some((d) => { - // Convert both to strings for comparison to handle both numeric and string IDs - return String(d) === String(formattedDependencyId); - }) - ) { - log( - "warn", - `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.` - ); - return; - } + // Check if dependency already exists + if ( + targetTask.dependencies.some((d) => { + // Convert both to strings for comparison to handle both numeric and string IDs + return String(d) === String(formattedDependencyId); + }) + ) { + log( + 'warn', + `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.` + ); + return; + } - // Check if the task is trying to depend on itself - compare full IDs (including subtask parts) - if (String(formattedTaskId) === String(formattedDependencyId)) { - log("error", `Task ${formattedTaskId} cannot depend on itself.`); - process.exit(1); - } + // Check if the task is trying to depend on itself - compare full IDs (including subtask parts) + if (String(formattedTaskId) === String(formattedDependencyId)) { + log('error', `Task ${formattedTaskId} cannot depend on itself.`); + process.exit(1); + } - // For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency - // Check if we're dealing with subtasks with the same parent task - let isSelfDependency = false; + // For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency + // Check if we're dealing with subtasks with the same parent task + let isSelfDependency = false; - if ( - typeof formattedTaskId === "string" && - typeof formattedDependencyId === "string" && - formattedTaskId.includes(".") && - formattedDependencyId.includes(".") - ) { - const [taskParentId] = formattedTaskId.split("."); - const [depParentId] = formattedDependencyId.split("."); + if ( + typeof formattedTaskId === 'string' && + typeof formattedDependencyId === 'string' && + formattedTaskId.includes('.') && + formattedDependencyId.includes('.') + ) { + const [taskParentId] = formattedTaskId.split('.'); + const [depParentId] = formattedDependencyId.split('.'); - // Only treat it as a self-dependency if both the parent ID and subtask ID are identical - isSelfDependency = formattedTaskId === formattedDependencyId; + // Only treat it as a self-dependency if both the parent ID and subtask ID are identical + isSelfDependency = formattedTaskId === formattedDependencyId; - // Log for debugging - log( - "debug", - `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}` - ); - log( - "debug", - `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}` - ); - } + // Log for debugging + log( + 'debug', + `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}` + ); + log( + 'debug', + `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}` + ); + } - if (isSelfDependency) { - log("error", `Subtask ${formattedTaskId} cannot depend on itself.`); - process.exit(1); - } + if (isSelfDependency) { + log('error', `Subtask ${formattedTaskId} cannot depend on itself.`); + process.exit(1); + } - // Check for circular dependencies - let dependencyChain = [formattedTaskId]; - if ( - !isCircularDependency(data.tasks, formattedDependencyId, dependencyChain) - ) { - // Add the dependency - targetTask.dependencies.push(formattedDependencyId); + // Check for circular dependencies + let dependencyChain = [formattedTaskId]; + if ( + !isCircularDependency(data.tasks, formattedDependencyId, dependencyChain) + ) { + // Add the dependency + targetTask.dependencies.push(formattedDependencyId); - // Sort dependencies numerically or by parent task ID first, then subtask ID - targetTask.dependencies.sort((a, b) => { - if (typeof a === "number" && typeof b === "number") { - return a - b; - } else if (typeof a === "string" && typeof b === "string") { - const [aParent, aChild] = a.split(".").map(Number); - const [bParent, bChild] = b.split(".").map(Number); - return aParent !== bParent ? aParent - bParent : aChild - bChild; - } else if (typeof a === "number") { - return -1; // Numbers come before strings - } else { - return 1; // Strings come after numbers - } - }); + // Sort dependencies numerically or by parent task ID first, then subtask ID + targetTask.dependencies.sort((a, b) => { + if (typeof a === 'number' && typeof b === 'number') { + return a - b; + } else if (typeof a === 'string' && typeof b === 'string') { + const [aParent, aChild] = a.split('.').map(Number); + const [bParent, bChild] = b.split('.').map(Number); + return aParent !== bParent ? aParent - bParent : aChild - bChild; + } else if (typeof a === 'number') { + return -1; // Numbers come before strings + } else { + return 1; // Strings come after numbers + } + }); - // Save changes - writeJSON(tasksPath, data); - log( - "success", - `Added dependency ${formattedDependencyId} to task ${formattedTaskId}` - ); + // Save changes + writeJSON(tasksPath, data); + log( + 'success', + `Added dependency ${formattedDependencyId} to task ${formattedTaskId}` + ); - // Display a more visually appealing success message - if (!isSilentMode()) { - console.log( - boxen( - chalk.green(`Successfully added dependency:\n\n`) + - `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`, - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1 }, - } - ) - ); - } + // Display a more visually appealing success message + if (!isSilentMode()) { + console.log( + boxen( + chalk.green(`Successfully added dependency:\n\n`) + + `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`, + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1 } + } + ) + ); + } - // Generate updated task files - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Generate updated task files + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); - log("info", "Task files regenerated with updated dependencies."); - } else { - log( - "error", - `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.` - ); - process.exit(1); - } + log('info', 'Task files regenerated with updated dependencies.'); + } else { + log( + 'error', + `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.` + ); + process.exit(1); + } } /** @@ -214,127 +214,127 @@ async function addDependency(tasksPath, taskId, dependencyId) { * @param {number|string} dependencyId - ID of the task to remove as dependency */ async function removeDependency(tasksPath, taskId, dependencyId) { - log("info", `Removing dependency ${dependencyId} from task ${taskId}...`); + log('info', `Removing dependency ${dependencyId} from task ${taskId}...`); - // Read tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log("error", "No valid tasks found."); - process.exit(1); - } + // Read tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log('error', 'No valid tasks found.'); + process.exit(1); + } - // Format the task and dependency IDs correctly - const formattedTaskId = - typeof taskId === "string" && taskId.includes(".") - ? taskId - : parseInt(taskId, 10); + // Format the task and dependency IDs correctly + const formattedTaskId = + typeof taskId === 'string' && taskId.includes('.') + ? taskId + : parseInt(taskId, 10); - const formattedDependencyId = formatTaskId(dependencyId); + const formattedDependencyId = formatTaskId(dependencyId); - // Find the task to update - let targetTask = null; - let isSubtask = false; + // Find the task to update + let targetTask = null; + let isSubtask = false; - if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) { - // Handle dot notation for subtasks (e.g., "1.2") - const [parentId, subtaskId] = formattedTaskId - .split(".") - .map((id) => parseInt(id, 10)); - const parentTask = data.tasks.find((t) => t.id === parentId); + if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { + // Handle dot notation for subtasks (e.g., "1.2") + const [parentId, subtaskId] = formattedTaskId + .split('.') + .map((id) => parseInt(id, 10)); + const parentTask = data.tasks.find((t) => t.id === parentId); - if (!parentTask) { - log("error", `Parent task ${parentId} not found.`); - process.exit(1); - } + if (!parentTask) { + log('error', `Parent task ${parentId} not found.`); + process.exit(1); + } - if (!parentTask.subtasks) { - log("error", `Parent task ${parentId} has no subtasks.`); - process.exit(1); - } + if (!parentTask.subtasks) { + log('error', `Parent task ${parentId} has no subtasks.`); + process.exit(1); + } - targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); - isSubtask = true; + targetTask = parentTask.subtasks.find((s) => s.id === subtaskId); + isSubtask = true; - if (!targetTask) { - log("error", `Subtask ${formattedTaskId} not found.`); - process.exit(1); - } - } else { - // Regular task (not a subtask) - targetTask = data.tasks.find((t) => t.id === formattedTaskId); + if (!targetTask) { + log('error', `Subtask ${formattedTaskId} not found.`); + process.exit(1); + } + } else { + // Regular task (not a subtask) + targetTask = data.tasks.find((t) => t.id === formattedTaskId); - if (!targetTask) { - log("error", `Task ${formattedTaskId} not found.`); - process.exit(1); - } - } + if (!targetTask) { + log('error', `Task ${formattedTaskId} not found.`); + process.exit(1); + } + } - // Check if the task has any dependencies - if (!targetTask.dependencies || targetTask.dependencies.length === 0) { - log( - "info", - `Task ${formattedTaskId} has no dependencies, nothing to remove.` - ); - return; - } + // Check if the task has any dependencies + if (!targetTask.dependencies || targetTask.dependencies.length === 0) { + log( + 'info', + `Task ${formattedTaskId} has no dependencies, nothing to remove.` + ); + return; + } - // Normalize the dependency ID for comparison to handle different formats - const normalizedDependencyId = String(formattedDependencyId); + // Normalize the dependency ID for comparison to handle different formats + const normalizedDependencyId = String(formattedDependencyId); - // Check if the dependency exists by comparing string representations - const dependencyIndex = targetTask.dependencies.findIndex((dep) => { - // Convert both to strings for comparison - let depStr = String(dep); + // Check if the dependency exists by comparing string representations + const dependencyIndex = targetTask.dependencies.findIndex((dep) => { + // Convert both to strings for comparison + let depStr = String(dep); - // Special handling for numeric IDs that might be subtask references - if (typeof dep === "number" && dep < 100 && isSubtask) { - // It's likely a reference to another subtask in the same parent task - // Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1) - const [parentId] = formattedTaskId.split("."); - depStr = `${parentId}.${dep}`; - } + // Special handling for numeric IDs that might be subtask references + if (typeof dep === 'number' && dep < 100 && isSubtask) { + // It's likely a reference to another subtask in the same parent task + // Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1) + const [parentId] = formattedTaskId.split('.'); + depStr = `${parentId}.${dep}`; + } - return depStr === normalizedDependencyId; - }); + return depStr === normalizedDependencyId; + }); - if (dependencyIndex === -1) { - log( - "info", - `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.` - ); - return; - } + if (dependencyIndex === -1) { + log( + 'info', + `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.` + ); + return; + } - // Remove the dependency - targetTask.dependencies.splice(dependencyIndex, 1); + // Remove the dependency + targetTask.dependencies.splice(dependencyIndex, 1); - // Save the updated tasks - writeJSON(tasksPath, data); + // Save the updated tasks + writeJSON(tasksPath, data); - // Success message - log( - "success", - `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}` - ); + // Success message + log( + 'success', + `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}` + ); - if (!isSilentMode()) { - // Display a more visually appealing success message - console.log( - boxen( - chalk.green(`Successfully removed dependency:\n\n`) + - `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`, - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1 }, - } - ) - ); - } + if (!isSilentMode()) { + // Display a more visually appealing success message + console.log( + boxen( + chalk.green(`Successfully removed dependency:\n\n`) + + `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`, + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1 } + } + ) + ); + } - // Regenerate task files - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Regenerate task files + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); } /** @@ -345,54 +345,54 @@ async function removeDependency(tasksPath, taskId, dependencyId) { * @returns {boolean} True if circular dependency would be created */ function isCircularDependency(tasks, taskId, chain = []) { - // Convert taskId to string for comparison - const taskIdStr = String(taskId); + // Convert taskId to string for comparison + const taskIdStr = String(taskId); - // If we've seen this task before in the chain, we have a circular dependency - if (chain.some((id) => String(id) === taskIdStr)) { - return true; - } + // If we've seen this task before in the chain, we have a circular dependency + if (chain.some((id) => String(id) === taskIdStr)) { + return true; + } - // Find the task or subtask - let task = null; - let parentIdForSubtask = null; + // Find the task or subtask + let task = null; + let parentIdForSubtask = null; - // Check if this is a subtask reference (e.g., "1.2") - if (taskIdStr.includes(".")) { - const [parentId, subtaskId] = taskIdStr.split(".").map(Number); - const parentTask = tasks.find((t) => t.id === parentId); - parentIdForSubtask = parentId; // Store parent ID if it's a subtask + // Check if this is a subtask reference (e.g., "1.2") + if (taskIdStr.includes('.')) { + const [parentId, subtaskId] = taskIdStr.split('.').map(Number); + const parentTask = tasks.find((t) => t.id === parentId); + parentIdForSubtask = parentId; // Store parent ID if it's a subtask - if (parentTask && parentTask.subtasks) { - task = parentTask.subtasks.find((st) => st.id === subtaskId); - } - } else { - // Regular task - task = tasks.find((t) => String(t.id) === taskIdStr); - } + if (parentTask && parentTask.subtasks) { + task = parentTask.subtasks.find((st) => st.id === subtaskId); + } + } else { + // Regular task + task = tasks.find((t) => String(t.id) === taskIdStr); + } - if (!task) { - return false; // Task doesn't exist, can't create circular dependency - } + if (!task) { + return false; // Task doesn't exist, can't create circular dependency + } - // No dependencies, can't create circular dependency - if (!task.dependencies || task.dependencies.length === 0) { - return false; - } + // No dependencies, can't create circular dependency + if (!task.dependencies || task.dependencies.length === 0) { + return false; + } - // Check each dependency recursively - const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency - return task.dependencies.some((depId) => { - let normalizedDepId = String(depId); - // Normalize relative subtask dependencies - if (typeof depId === "number" && parentIdForSubtask !== null) { - // If the current task is a subtask AND the dependency is a number, - // assume it refers to a sibling subtask. - normalizedDepId = `${parentIdForSubtask}.${depId}`; - } - // Pass the normalized ID to the recursive call - return isCircularDependency(tasks, normalizedDepId, newChain); - }); + // Check each dependency recursively + const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency + return task.dependencies.some((depId) => { + let normalizedDepId = String(depId); + // Normalize relative subtask dependencies + if (typeof depId === 'number' && parentIdForSubtask !== null) { + // If the current task is a subtask AND the dependency is a number, + // assume it refers to a sibling subtask. + normalizedDepId = `${parentIdForSubtask}.${depId}`; + } + // Pass the normalized ID to the recursive call + return isCircularDependency(tasks, normalizedDepId, newChain); + }); } /** @@ -401,96 +401,96 @@ function isCircularDependency(tasks, taskId, chain = []) { * @returns {Object} Validation result with valid flag and issues array */ function validateTaskDependencies(tasks) { - const issues = []; + const issues = []; - // Check each task's dependencies - tasks.forEach((task) => { - if (!task.dependencies) { - return; // No dependencies to validate - } + // Check each task's dependencies + tasks.forEach((task) => { + if (!task.dependencies) { + return; // No dependencies to validate + } - task.dependencies.forEach((depId) => { - // Check for self-dependencies - if (String(depId) === String(task.id)) { - issues.push({ - type: "self", - taskId: task.id, - message: `Task ${task.id} depends on itself`, - }); - return; - } + task.dependencies.forEach((depId) => { + // Check for self-dependencies + if (String(depId) === String(task.id)) { + issues.push({ + type: 'self', + taskId: task.id, + message: `Task ${task.id} depends on itself` + }); + return; + } - // Check if dependency exists - if (!taskExists(tasks, depId)) { - issues.push({ - type: "missing", - taskId: task.id, - dependencyId: depId, - message: `Task ${task.id} depends on non-existent task ${depId}`, - }); - } - }); + // Check if dependency exists + if (!taskExists(tasks, depId)) { + issues.push({ + type: 'missing', + taskId: task.id, + dependencyId: depId, + message: `Task ${task.id} depends on non-existent task ${depId}` + }); + } + }); - // Check for circular dependencies - if (isCircularDependency(tasks, task.id)) { - issues.push({ - type: "circular", - taskId: task.id, - message: `Task ${task.id} is part of a circular dependency chain`, - }); - } + // Check for circular dependencies + if (isCircularDependency(tasks, task.id)) { + issues.push({ + type: 'circular', + taskId: task.id, + message: `Task ${task.id} is part of a circular dependency chain` + }); + } - // Check subtask dependencies if they exist - if (task.subtasks && task.subtasks.length > 0) { - task.subtasks.forEach((subtask) => { - if (!subtask.dependencies) { - return; // No dependencies to validate - } + // Check subtask dependencies if they exist + if (task.subtasks && task.subtasks.length > 0) { + task.subtasks.forEach((subtask) => { + if (!subtask.dependencies) { + return; // No dependencies to validate + } - // Create a full subtask ID for reference - const fullSubtaskId = `${task.id}.${subtask.id}`; + // Create a full subtask ID for reference + const fullSubtaskId = `${task.id}.${subtask.id}`; - subtask.dependencies.forEach((depId) => { - // Check for self-dependencies in subtasks - if ( - String(depId) === String(fullSubtaskId) || - (typeof depId === "number" && depId === subtask.id) - ) { - issues.push({ - type: "self", - taskId: fullSubtaskId, - message: `Subtask ${fullSubtaskId} depends on itself`, - }); - return; - } + subtask.dependencies.forEach((depId) => { + // Check for self-dependencies in subtasks + if ( + String(depId) === String(fullSubtaskId) || + (typeof depId === 'number' && depId === subtask.id) + ) { + issues.push({ + type: 'self', + taskId: fullSubtaskId, + message: `Subtask ${fullSubtaskId} depends on itself` + }); + return; + } - // Check if dependency exists - if (!taskExists(tasks, depId)) { - issues.push({ - type: "missing", - taskId: fullSubtaskId, - dependencyId: depId, - message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`, - }); - } - }); + // Check if dependency exists + if (!taskExists(tasks, depId)) { + issues.push({ + type: 'missing', + taskId: fullSubtaskId, + dependencyId: depId, + message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}` + }); + } + }); - // Check for circular dependencies in subtasks - if (isCircularDependency(tasks, fullSubtaskId)) { - issues.push({ - type: "circular", - taskId: fullSubtaskId, - message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`, - }); - } - }); - } - }); + // Check for circular dependencies in subtasks + if (isCircularDependency(tasks, fullSubtaskId)) { + issues.push({ + type: 'circular', + taskId: fullSubtaskId, + message: `Subtask ${fullSubtaskId} is part of a circular dependency chain` + }); + } + }); + } + }); - return { - valid: issues.length === 0, - issues, - }; + return { + valid: issues.length === 0, + issues + }; } /** @@ -499,23 +499,23 @@ function validateTaskDependencies(tasks) { * @returns {Object} Updated tasks data with duplicates removed */ function removeDuplicateDependencies(tasksData) { - const tasks = tasksData.tasks.map((task) => { - if (!task.dependencies) { - return task; - } + const tasks = tasksData.tasks.map((task) => { + if (!task.dependencies) { + return task; + } - // Convert to Set and back to array to remove duplicates - const uniqueDeps = [...new Set(task.dependencies)]; - return { - ...task, - dependencies: uniqueDeps, - }; - }); + // Convert to Set and back to array to remove duplicates + const uniqueDeps = [...new Set(task.dependencies)]; + return { + ...task, + dependencies: uniqueDeps + }; + }); - return { - ...tasksData, - tasks, - }; + return { + ...tasksData, + tasks + }; } /** @@ -524,38 +524,38 @@ function removeDuplicateDependencies(tasksData) { * @returns {Object} Updated tasks data with invalid subtask dependencies removed */ function cleanupSubtaskDependencies(tasksData) { - const tasks = tasksData.tasks.map((task) => { - // Handle task's own dependencies - if (task.dependencies) { - task.dependencies = task.dependencies.filter((depId) => { - // Keep only dependencies that exist - return taskExists(tasksData.tasks, depId); - }); - } + const tasks = tasksData.tasks.map((task) => { + // Handle task's own dependencies + if (task.dependencies) { + task.dependencies = task.dependencies.filter((depId) => { + // Keep only dependencies that exist + return taskExists(tasksData.tasks, depId); + }); + } - // Handle subtask dependencies - if (task.subtasks) { - task.subtasks = task.subtasks.map((subtask) => { - if (!subtask.dependencies) { - return subtask; - } + // Handle subtask dependencies + if (task.subtasks) { + task.subtasks = task.subtasks.map((subtask) => { + if (!subtask.dependencies) { + return subtask; + } - // Filter out dependencies to non-existent subtasks - subtask.dependencies = subtask.dependencies.filter((depId) => { - return taskExists(tasksData.tasks, depId); - }); + // Filter out dependencies to non-existent subtasks + subtask.dependencies = subtask.dependencies.filter((depId) => { + return taskExists(tasksData.tasks, depId); + }); - return subtask; - }); - } + return subtask; + }); + } - return task; - }); + return task; + }); - return { - ...tasksData, - tasks, - }; + return { + ...tasksData, + tasks + }; } /** @@ -563,94 +563,94 @@ function cleanupSubtaskDependencies(tasksData) { * @param {string} tasksPath - Path to tasks.json */ async function validateDependenciesCommand(tasksPath, options = {}) { - log("info", "Checking for invalid dependencies in task files..."); + log('info', 'Checking for invalid dependencies in task files...'); - // Read tasks data - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log("error", "No valid tasks found in tasks.json"); - process.exit(1); - } + // Read tasks data + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log('error', 'No valid tasks found in tasks.json'); + process.exit(1); + } - // Count of tasks and subtasks for reporting - const taskCount = data.tasks.length; - let subtaskCount = 0; - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - subtaskCount += task.subtasks.length; - } - }); + // Count of tasks and subtasks for reporting + const taskCount = data.tasks.length; + let subtaskCount = 0; + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + subtaskCount += task.subtasks.length; + } + }); - log( - "info", - `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...` - ); + log( + 'info', + `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...` + ); - try { - // Directly call the validation function - const validationResult = validateTaskDependencies(data.tasks); + try { + // Directly call the validation function + const validationResult = validateTaskDependencies(data.tasks); - if (!validationResult.valid) { - log( - "error", - `Dependency validation failed. Found ${validationResult.issues.length} issue(s):` - ); - validationResult.issues.forEach((issue) => { - let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`; - if (issue.dependencyId) { - errorMsg += ` (Dependency: ${issue.dependencyId})`; - } - log("error", errorMsg); // Log each issue as an error - }); + if (!validationResult.valid) { + log( + 'error', + `Dependency validation failed. Found ${validationResult.issues.length} issue(s):` + ); + validationResult.issues.forEach((issue) => { + let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`; + if (issue.dependencyId) { + errorMsg += ` (Dependency: ${issue.dependencyId})`; + } + log('error', errorMsg); // Log each issue as an error + }); - // Optionally exit if validation fails, depending on desired behavior - // process.exit(1); // Uncomment if validation failure should stop the process + // Optionally exit if validation fails, depending on desired behavior + // process.exit(1); // Uncomment if validation failure should stop the process - // Display summary box even on failure, showing issues found - if (!isSilentMode()) { - console.log( - boxen( - chalk.red(`Dependency Validation FAILED\n\n`) + - `${chalk.cyan("Tasks checked:")} ${taskCount}\n` + - `${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` + - `${chalk.red("Issues found:")} ${validationResult.issues.length}`, // Display count from result - { - padding: 1, - borderColor: "red", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - } - ) - ); - } - } else { - log( - "success", - "No invalid dependencies found - all dependencies are valid" - ); + // Display summary box even on failure, showing issues found + if (!isSilentMode()) { + console.log( + boxen( + chalk.red(`Dependency Validation FAILED\n\n`) + + `${chalk.cyan('Tasks checked:')} ${taskCount}\n` + + `${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` + + `${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result + { + padding: 1, + borderColor: 'red', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + } + ) + ); + } + } else { + log( + 'success', + 'No invalid dependencies found - all dependencies are valid' + ); - // Show validation summary - only if not in silent mode - if (!isSilentMode()) { - console.log( - boxen( - chalk.green(`All Dependencies Are Valid\n\n`) + - `${chalk.cyan("Tasks checked:")} ${taskCount}\n` + - `${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` + - `${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`, - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - } - ) - ); - } - } - } catch (error) { - log("error", "Error validating dependencies:", error); - process.exit(1); - } + // Show validation summary - only if not in silent mode + if (!isSilentMode()) { + console.log( + boxen( + chalk.green(`All Dependencies Are Valid\n\n`) + + `${chalk.cyan('Tasks checked:')} ${taskCount}\n` + + `${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` + + `${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`, + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + } + ) + ); + } + } + } catch (error) { + log('error', 'Error validating dependencies:', error); + process.exit(1); + } } /** @@ -659,25 +659,25 @@ async function validateDependenciesCommand(tasksPath, options = {}) { * @returns {number} - Total number of dependencies */ function countAllDependencies(tasks) { - let count = 0; + let count = 0; - tasks.forEach((task) => { - // Count main task dependencies - if (task.dependencies && Array.isArray(task.dependencies)) { - count += task.dependencies.length; - } + tasks.forEach((task) => { + // Count main task dependencies + if (task.dependencies && Array.isArray(task.dependencies)) { + count += task.dependencies.length; + } - // Count subtask dependencies - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - count += subtask.dependencies.length; - } - }); - } - }); + // Count subtask dependencies + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + count += subtask.dependencies.length; + } + }); + } + }); - return count; + return count; } /** @@ -686,387 +686,387 @@ function countAllDependencies(tasks) { * @param {Object} options - Options object */ async function fixDependenciesCommand(tasksPath, options = {}) { - log("info", "Checking for and fixing invalid dependencies in tasks.json..."); + log('info', 'Checking for and fixing invalid dependencies in tasks.json...'); - try { - // Read tasks data - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log("error", "No valid tasks found in tasks.json"); - process.exit(1); - } + try { + // Read tasks data + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log('error', 'No valid tasks found in tasks.json'); + process.exit(1); + } - // Create a deep copy of the original data for comparison - const originalData = JSON.parse(JSON.stringify(data)); + // Create a deep copy of the original data for comparison + const originalData = JSON.parse(JSON.stringify(data)); - // Track fixes for reporting - const stats = { - nonExistentDependenciesRemoved: 0, - selfDependenciesRemoved: 0, - duplicateDependenciesRemoved: 0, - circularDependenciesFixed: 0, - tasksFixed: 0, - subtasksFixed: 0, - }; + // Track fixes for reporting + const stats = { + nonExistentDependenciesRemoved: 0, + selfDependenciesRemoved: 0, + duplicateDependenciesRemoved: 0, + circularDependenciesFixed: 0, + tasksFixed: 0, + subtasksFixed: 0 + }; - // First phase: Remove duplicate dependencies in tasks - data.tasks.forEach((task) => { - if (task.dependencies && Array.isArray(task.dependencies)) { - const uniqueDeps = new Set(); - const originalLength = task.dependencies.length; - task.dependencies = task.dependencies.filter((depId) => { - const depIdStr = String(depId); - if (uniqueDeps.has(depIdStr)) { - log( - "info", - `Removing duplicate dependency from task ${task.id}: ${depId}` - ); - stats.duplicateDependenciesRemoved++; - return false; - } - uniqueDeps.add(depIdStr); - return true; - }); - if (task.dependencies.length < originalLength) { - stats.tasksFixed++; - } - } + // First phase: Remove duplicate dependencies in tasks + data.tasks.forEach((task) => { + if (task.dependencies && Array.isArray(task.dependencies)) { + const uniqueDeps = new Set(); + const originalLength = task.dependencies.length; + task.dependencies = task.dependencies.filter((depId) => { + const depIdStr = String(depId); + if (uniqueDeps.has(depIdStr)) { + log( + 'info', + `Removing duplicate dependency from task ${task.id}: ${depId}` + ); + stats.duplicateDependenciesRemoved++; + return false; + } + uniqueDeps.add(depIdStr); + return true; + }); + if (task.dependencies.length < originalLength) { + stats.tasksFixed++; + } + } - // Check for duplicates in subtasks - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const uniqueDeps = new Set(); - const originalLength = subtask.dependencies.length; - subtask.dependencies = subtask.dependencies.filter((depId) => { - let depIdStr = String(depId); - if (typeof depId === "number" && depId < 100) { - depIdStr = `${task.id}.${depId}`; - } - if (uniqueDeps.has(depIdStr)) { - log( - "info", - `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` - ); - stats.duplicateDependenciesRemoved++; - return false; - } - uniqueDeps.add(depIdStr); - return true; - }); - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - }); - } - }); + // Check for duplicates in subtasks + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const uniqueDeps = new Set(); + const originalLength = subtask.dependencies.length; + subtask.dependencies = subtask.dependencies.filter((depId) => { + let depIdStr = String(depId); + if (typeof depId === 'number' && depId < 100) { + depIdStr = `${task.id}.${depId}`; + } + if (uniqueDeps.has(depIdStr)) { + log( + 'info', + `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` + ); + stats.duplicateDependenciesRemoved++; + return false; + } + uniqueDeps.add(depIdStr); + return true; + }); + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + }); + } + }); - // Create validity maps for tasks and subtasks - const validTaskIds = new Set(data.tasks.map((t) => t.id)); - const validSubtaskIds = new Set(); - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - validSubtaskIds.add(`${task.id}.${subtask.id}`); - }); - } - }); + // Create validity maps for tasks and subtasks + const validTaskIds = new Set(data.tasks.map((t) => t.id)); + const validSubtaskIds = new Set(); + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + validSubtaskIds.add(`${task.id}.${subtask.id}`); + }); + } + }); - // Second phase: Remove invalid task dependencies (non-existent tasks) - data.tasks.forEach((task) => { - if (task.dependencies && Array.isArray(task.dependencies)) { - const originalLength = task.dependencies.length; - task.dependencies = task.dependencies.filter((depId) => { - const isSubtask = typeof depId === "string" && depId.includes("."); + // Second phase: Remove invalid task dependencies (non-existent tasks) + data.tasks.forEach((task) => { + if (task.dependencies && Array.isArray(task.dependencies)) { + const originalLength = task.dependencies.length; + task.dependencies = task.dependencies.filter((depId) => { + const isSubtask = typeof depId === 'string' && depId.includes('.'); - if (isSubtask) { - // Check if the subtask exists - if (!validSubtaskIds.has(depId)) { - log( - "info", - `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } else { - // Check if the task exists - const numericId = - typeof depId === "string" ? parseInt(depId, 10) : depId; - if (!validTaskIds.has(numericId)) { - log( - "info", - `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } - }); + if (isSubtask) { + // Check if the subtask exists + if (!validSubtaskIds.has(depId)) { + log( + 'info', + `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } else { + // Check if the task exists + const numericId = + typeof depId === 'string' ? parseInt(depId, 10) : depId; + if (!validTaskIds.has(numericId)) { + log( + 'info', + `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } + }); - if (task.dependencies.length < originalLength) { - stats.tasksFixed++; - } - } + if (task.dependencies.length < originalLength) { + stats.tasksFixed++; + } + } - // Check subtask dependencies for invalid references - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const originalLength = subtask.dependencies.length; - const subtaskId = `${task.id}.${subtask.id}`; + // Check subtask dependencies for invalid references + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const originalLength = subtask.dependencies.length; + const subtaskId = `${task.id}.${subtask.id}`; - // First check for self-dependencies - const hasSelfDependency = subtask.dependencies.some((depId) => { - if (typeof depId === "string" && depId.includes(".")) { - return depId === subtaskId; - } else if (typeof depId === "number" && depId < 100) { - return depId === subtask.id; - } - return false; - }); + // First check for self-dependencies + const hasSelfDependency = subtask.dependencies.some((depId) => { + if (typeof depId === 'string' && depId.includes('.')) { + return depId === subtaskId; + } else if (typeof depId === 'number' && depId < 100) { + return depId === subtask.id; + } + return false; + }); - if (hasSelfDependency) { - subtask.dependencies = subtask.dependencies.filter((depId) => { - const normalizedDepId = - typeof depId === "number" && depId < 100 - ? `${task.id}.${depId}` - : String(depId); + if (hasSelfDependency) { + subtask.dependencies = subtask.dependencies.filter((depId) => { + const normalizedDepId = + typeof depId === 'number' && depId < 100 + ? `${task.id}.${depId}` + : String(depId); - if (normalizedDepId === subtaskId) { - log( - "info", - `Removing self-dependency from subtask ${subtaskId}` - ); - stats.selfDependenciesRemoved++; - return false; - } - return true; - }); - } + if (normalizedDepId === subtaskId) { + log( + 'info', + `Removing self-dependency from subtask ${subtaskId}` + ); + stats.selfDependenciesRemoved++; + return false; + } + return true; + }); + } - // Then check for non-existent dependencies - subtask.dependencies = subtask.dependencies.filter((depId) => { - if (typeof depId === "string" && depId.includes(".")) { - if (!validSubtaskIds.has(depId)) { - log( - "info", - `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } - return true; - } + // Then check for non-existent dependencies + subtask.dependencies = subtask.dependencies.filter((depId) => { + if (typeof depId === 'string' && depId.includes('.')) { + if (!validSubtaskIds.has(depId)) { + log( + 'info', + `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } + return true; + } - // Handle numeric dependencies - const numericId = - typeof depId === "number" ? depId : parseInt(depId, 10); + // Handle numeric dependencies + const numericId = + typeof depId === 'number' ? depId : parseInt(depId, 10); - // Small numbers likely refer to subtasks in the same task - if (numericId < 100) { - const fullSubtaskId = `${task.id}.${numericId}`; + // Small numbers likely refer to subtasks in the same task + if (numericId < 100) { + const fullSubtaskId = `${task.id}.${numericId}`; - if (!validSubtaskIds.has(fullSubtaskId)) { - log( - "info", - `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } + if (!validSubtaskIds.has(fullSubtaskId)) { + log( + 'info', + `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } - return true; - } + return true; + } - // Otherwise it's a task reference - if (!validTaskIds.has(numericId)) { - log( - "info", - `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` - ); - stats.nonExistentDependenciesRemoved++; - return false; - } + // Otherwise it's a task reference + if (!validTaskIds.has(numericId)) { + log( + 'info', + `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` + ); + stats.nonExistentDependenciesRemoved++; + return false; + } - return true; - }); + return true; + }); - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - }); - } - }); + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + }); + } + }); - // Third phase: Check for circular dependencies - log("info", "Checking for circular dependencies..."); + // Third phase: Check for circular dependencies + log('info', 'Checking for circular dependencies...'); - // Build the dependency map for subtasks - const subtaskDependencyMap = new Map(); - data.tasks.forEach((task) => { - if (task.subtasks && Array.isArray(task.subtasks)) { - task.subtasks.forEach((subtask) => { - const subtaskId = `${task.id}.${subtask.id}`; + // Build the dependency map for subtasks + const subtaskDependencyMap = new Map(); + data.tasks.forEach((task) => { + if (task.subtasks && Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + const subtaskId = `${task.id}.${subtask.id}`; - if (subtask.dependencies && Array.isArray(subtask.dependencies)) { - const normalizedDeps = subtask.dependencies.map((depId) => { - if (typeof depId === "string" && depId.includes(".")) { - return depId; - } else if (typeof depId === "number" && depId < 100) { - return `${task.id}.${depId}`; - } - return String(depId); - }); - subtaskDependencyMap.set(subtaskId, normalizedDeps); - } else { - subtaskDependencyMap.set(subtaskId, []); - } - }); - } - }); + if (subtask.dependencies && Array.isArray(subtask.dependencies)) { + const normalizedDeps = subtask.dependencies.map((depId) => { + if (typeof depId === 'string' && depId.includes('.')) { + return depId; + } else if (typeof depId === 'number' && depId < 100) { + return `${task.id}.${depId}`; + } + return String(depId); + }); + subtaskDependencyMap.set(subtaskId, normalizedDeps); + } else { + subtaskDependencyMap.set(subtaskId, []); + } + }); + } + }); - // Check for and fix circular dependencies - for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) { - const visited = new Set(); - const recursionStack = new Set(); + // Check for and fix circular dependencies + for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) { + const visited = new Set(); + const recursionStack = new Set(); - // Detect cycles - const cycleEdges = findCycles( - subtaskId, - subtaskDependencyMap, - visited, - recursionStack - ); + // Detect cycles + const cycleEdges = findCycles( + subtaskId, + subtaskDependencyMap, + visited, + recursionStack + ); - if (cycleEdges.length > 0) { - const [taskId, subtaskNum] = subtaskId - .split(".") - .map((part) => Number(part)); - const task = data.tasks.find((t) => t.id === taskId); + if (cycleEdges.length > 0) { + const [taskId, subtaskNum] = subtaskId + .split('.') + .map((part) => Number(part)); + const task = data.tasks.find((t) => t.id === taskId); - if (task && task.subtasks) { - const subtask = task.subtasks.find((st) => st.id === subtaskNum); + if (task && task.subtasks) { + const subtask = task.subtasks.find((st) => st.id === subtaskNum); - if (subtask && subtask.dependencies) { - const originalLength = subtask.dependencies.length; + if (subtask && subtask.dependencies) { + const originalLength = subtask.dependencies.length; - const edgesToRemove = cycleEdges.map((edge) => { - if (edge.includes(".")) { - const [depTaskId, depSubtaskId] = edge - .split(".") - .map((part) => Number(part)); + const edgesToRemove = cycleEdges.map((edge) => { + if (edge.includes('.')) { + const [depTaskId, depSubtaskId] = edge + .split('.') + .map((part) => Number(part)); - if (depTaskId === taskId) { - return depSubtaskId; - } + if (depTaskId === taskId) { + return depSubtaskId; + } - return edge; - } + return edge; + } - return Number(edge); - }); + return Number(edge); + }); - subtask.dependencies = subtask.dependencies.filter((depId) => { - const normalizedDepId = - typeof depId === "number" && depId < 100 - ? `${taskId}.${depId}` - : String(depId); + subtask.dependencies = subtask.dependencies.filter((depId) => { + const normalizedDepId = + typeof depId === 'number' && depId < 100 + ? `${taskId}.${depId}` + : String(depId); - if ( - edgesToRemove.includes(depId) || - edgesToRemove.includes(normalizedDepId) - ) { - log( - "info", - `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` - ); - stats.circularDependenciesFixed++; - return false; - } - return true; - }); + if ( + edgesToRemove.includes(depId) || + edgesToRemove.includes(normalizedDepId) + ) { + log( + 'info', + `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` + ); + stats.circularDependenciesFixed++; + return false; + } + return true; + }); - if (subtask.dependencies.length < originalLength) { - stats.subtasksFixed++; - } - } - } - } - } + if (subtask.dependencies.length < originalLength) { + stats.subtasksFixed++; + } + } + } + } + } - // Check if any changes were made by comparing with original data - const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData); + // Check if any changes were made by comparing with original data + const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData); - if (dataChanged) { - // Save the changes - writeJSON(tasksPath, data); - log("success", "Fixed dependency issues in tasks.json"); + if (dataChanged) { + // Save the changes + writeJSON(tasksPath, data); + log('success', 'Fixed dependency issues in tasks.json'); - // Regenerate task files - log("info", "Regenerating task files to reflect dependency changes..."); - await generateTaskFiles(tasksPath, path.dirname(tasksPath)); - } else { - log("info", "No changes needed to fix dependencies"); - } + // Regenerate task files + log('info', 'Regenerating task files to reflect dependency changes...'); + await generateTaskFiles(tasksPath, path.dirname(tasksPath)); + } else { + log('info', 'No changes needed to fix dependencies'); + } - // Show detailed statistics report - const totalFixedAll = - stats.nonExistentDependenciesRemoved + - stats.selfDependenciesRemoved + - stats.duplicateDependenciesRemoved + - stats.circularDependenciesFixed; + // Show detailed statistics report + const totalFixedAll = + stats.nonExistentDependenciesRemoved + + stats.selfDependenciesRemoved + + stats.duplicateDependenciesRemoved + + stats.circularDependenciesFixed; - if (!isSilentMode()) { - if (totalFixedAll > 0) { - log("success", `Fixed ${totalFixedAll} dependency issues in total!`); + if (!isSilentMode()) { + if (totalFixedAll > 0) { + log('success', `Fixed ${totalFixedAll} dependency issues in total!`); - console.log( - boxen( - chalk.green(`Dependency Fixes Summary:\n\n`) + - `${chalk.cyan("Invalid dependencies removed:")} ${stats.nonExistentDependenciesRemoved}\n` + - `${chalk.cyan("Self-dependencies removed:")} ${stats.selfDependenciesRemoved}\n` + - `${chalk.cyan("Duplicate dependencies removed:")} ${stats.duplicateDependenciesRemoved}\n` + - `${chalk.cyan("Circular dependencies fixed:")} ${stats.circularDependenciesFixed}\n\n` + - `${chalk.cyan("Tasks fixed:")} ${stats.tasksFixed}\n` + - `${chalk.cyan("Subtasks fixed:")} ${stats.subtasksFixed}\n`, - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - } - ) - ); - } else { - log( - "success", - "No dependency issues found - all dependencies are valid" - ); + console.log( + boxen( + chalk.green(`Dependency Fixes Summary:\n\n`) + + `${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` + + `${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` + + `${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` + + `${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` + + `${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` + + `${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`, + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + } + ) + ); + } else { + log( + 'success', + 'No dependency issues found - all dependencies are valid' + ); - console.log( - boxen( - chalk.green(`All Dependencies Are Valid\n\n`) + - `${chalk.cyan("Tasks checked:")} ${data.tasks.length}\n` + - `${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`, - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - } - ) - ); - } - } - } catch (error) { - log("error", "Error in fix-dependencies command:", error); - process.exit(1); - } + console.log( + boxen( + chalk.green(`All Dependencies Are Valid\n\n`) + + `${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` + + `${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`, + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + } + ) + ); + } + } + } catch (error) { + log('error', 'Error in fix-dependencies command:', error); + process.exit(1); + } } /** @@ -1075,44 +1075,44 @@ async function fixDependenciesCommand(tasksPath, options = {}) { * @returns {boolean} - True if any changes were made */ function ensureAtLeastOneIndependentSubtask(tasksData) { - if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { - return false; - } + if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { + return false; + } - let changesDetected = false; + let changesDetected = false; - tasksData.tasks.forEach((task) => { - if ( - !task.subtasks || - !Array.isArray(task.subtasks) || - task.subtasks.length === 0 - ) { - return; - } + tasksData.tasks.forEach((task) => { + if ( + !task.subtasks || + !Array.isArray(task.subtasks) || + task.subtasks.length === 0 + ) { + return; + } - // Check if any subtask has no dependencies - const hasIndependentSubtask = task.subtasks.some( - (st) => - !st.dependencies || - !Array.isArray(st.dependencies) || - st.dependencies.length === 0 - ); + // Check if any subtask has no dependencies + const hasIndependentSubtask = task.subtasks.some( + (st) => + !st.dependencies || + !Array.isArray(st.dependencies) || + st.dependencies.length === 0 + ); - if (!hasIndependentSubtask) { - // Find the first subtask and clear its dependencies - if (task.subtasks.length > 0) { - const firstSubtask = task.subtasks[0]; - log( - "debug", - `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}` - ); - firstSubtask.dependencies = []; - changesDetected = true; - } - } - }); + if (!hasIndependentSubtask) { + // Find the first subtask and clear its dependencies + if (task.subtasks.length > 0) { + const firstSubtask = task.subtasks[0]; + log( + 'debug', + `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}` + ); + firstSubtask.dependencies = []; + changesDetected = true; + } + } + }); - return changesDetected; + return changesDetected; } /** @@ -1123,111 +1123,111 @@ function ensureAtLeastOneIndependentSubtask(tasksData) { * @returns {boolean} - True if any changes were made */ function validateAndFixDependencies(tasksData, tasksPath = null) { - if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { - log("error", "Invalid tasks data"); - return false; - } + if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { + log('error', 'Invalid tasks data'); + return false; + } - log("debug", "Validating and fixing dependencies..."); + log('debug', 'Validating and fixing dependencies...'); - // Create a deep copy for comparison - const originalData = JSON.parse(JSON.stringify(tasksData)); + // Create a deep copy for comparison + const originalData = JSON.parse(JSON.stringify(tasksData)); - // 1. Remove duplicate dependencies from tasks and subtasks - tasksData.tasks = tasksData.tasks.map((task) => { - // Handle task dependencies - if (task.dependencies) { - const uniqueDeps = [...new Set(task.dependencies)]; - task.dependencies = uniqueDeps; - } + // 1. Remove duplicate dependencies from tasks and subtasks + tasksData.tasks = tasksData.tasks.map((task) => { + // Handle task dependencies + if (task.dependencies) { + const uniqueDeps = [...new Set(task.dependencies)]; + task.dependencies = uniqueDeps; + } - // Handle subtask dependencies - if (task.subtasks) { - task.subtasks = task.subtasks.map((subtask) => { - if (subtask.dependencies) { - const uniqueDeps = [...new Set(subtask.dependencies)]; - subtask.dependencies = uniqueDeps; - } - return subtask; - }); - } - return task; - }); + // Handle subtask dependencies + if (task.subtasks) { + task.subtasks = task.subtasks.map((subtask) => { + if (subtask.dependencies) { + const uniqueDeps = [...new Set(subtask.dependencies)]; + subtask.dependencies = uniqueDeps; + } + return subtask; + }); + } + return task; + }); - // 2. Remove invalid task dependencies (non-existent tasks) - tasksData.tasks.forEach((task) => { - // Clean up task dependencies - if (task.dependencies) { - task.dependencies = task.dependencies.filter((depId) => { - // Remove self-dependencies - if (String(depId) === String(task.id)) { - return false; - } - // Remove non-existent dependencies - return taskExists(tasksData.tasks, depId); - }); - } + // 2. Remove invalid task dependencies (non-existent tasks) + tasksData.tasks.forEach((task) => { + // Clean up task dependencies + if (task.dependencies) { + task.dependencies = task.dependencies.filter((depId) => { + // Remove self-dependencies + if (String(depId) === String(task.id)) { + return false; + } + // Remove non-existent dependencies + return taskExists(tasksData.tasks, depId); + }); + } - // Clean up subtask dependencies - if (task.subtasks) { - task.subtasks.forEach((subtask) => { - if (subtask.dependencies) { - subtask.dependencies = subtask.dependencies.filter((depId) => { - // Handle numeric subtask references - if (typeof depId === "number" && depId < 100) { - const fullSubtaskId = `${task.id}.${depId}`; - return taskExists(tasksData.tasks, fullSubtaskId); - } - // Handle full task/subtask references - return taskExists(tasksData.tasks, depId); - }); - } - }); - } - }); + // Clean up subtask dependencies + if (task.subtasks) { + task.subtasks.forEach((subtask) => { + if (subtask.dependencies) { + subtask.dependencies = subtask.dependencies.filter((depId) => { + // Handle numeric subtask references + if (typeof depId === 'number' && depId < 100) { + const fullSubtaskId = `${task.id}.${depId}`; + return taskExists(tasksData.tasks, fullSubtaskId); + } + // Handle full task/subtask references + return taskExists(tasksData.tasks, depId); + }); + } + }); + } + }); - // 3. Ensure at least one subtask has no dependencies in each task - tasksData.tasks.forEach((task) => { - if (task.subtasks && task.subtasks.length > 0) { - const hasIndependentSubtask = task.subtasks.some( - (st) => - !st.dependencies || - !Array.isArray(st.dependencies) || - st.dependencies.length === 0 - ); + // 3. Ensure at least one subtask has no dependencies in each task + tasksData.tasks.forEach((task) => { + if (task.subtasks && task.subtasks.length > 0) { + const hasIndependentSubtask = task.subtasks.some( + (st) => + !st.dependencies || + !Array.isArray(st.dependencies) || + st.dependencies.length === 0 + ); - if (!hasIndependentSubtask) { - task.subtasks[0].dependencies = []; - } - } - }); + if (!hasIndependentSubtask) { + task.subtasks[0].dependencies = []; + } + } + }); - // Check if any changes were made by comparing with original data - const changesDetected = - JSON.stringify(tasksData) !== JSON.stringify(originalData); + // Check if any changes were made by comparing with original data + const changesDetected = + JSON.stringify(tasksData) !== JSON.stringify(originalData); - // Save changes if needed - if (tasksPath && changesDetected) { - try { - writeJSON(tasksPath, tasksData); - log("debug", "Saved dependency fixes to tasks.json"); - } catch (error) { - log("error", "Failed to save dependency fixes to tasks.json", error); - } - } + // Save changes if needed + if (tasksPath && changesDetected) { + try { + writeJSON(tasksPath, tasksData); + log('debug', 'Saved dependency fixes to tasks.json'); + } catch (error) { + log('error', 'Failed to save dependency fixes to tasks.json', error); + } + } - return changesDetected; + return changesDetected; } export { - addDependency, - removeDependency, - isCircularDependency, - validateTaskDependencies, - validateDependenciesCommand, - fixDependenciesCommand, - removeDuplicateDependencies, - cleanupSubtaskDependencies, - ensureAtLeastOneIndependentSubtask, - validateAndFixDependencies, + addDependency, + removeDependency, + isCircularDependency, + validateTaskDependencies, + validateDependenciesCommand, + fixDependenciesCommand, + removeDuplicateDependencies, + cleanupSubtaskDependencies, + ensureAtLeastOneIndependentSubtask, + validateAndFixDependencies }; diff --git a/scripts/modules/supported-models.json b/scripts/modules/supported-models.json index fac3da0d..bccdb96e 100644 --- a/scripts/modules/supported-models.json +++ b/scripts/modules/supported-models.json @@ -1,427 +1,427 @@ { - "anthropic": [ - { - "id": "claude-sonnet-4-20250514", - "swe_score": 0.727, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 64000 - }, - { - "id": "claude-opus-4-20250514", - "swe_score": 0.725, - "cost_per_1m_tokens": { "input": 15.0, "output": 75.0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 32000 - }, - { - "id": "claude-3-7-sonnet-20250219", - "swe_score": 0.623, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 120000 - }, - { - "id": "claude-3-5-sonnet-20241022", - "swe_score": 0.49, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 64000 - } - ], - "openai": [ - { - "id": "gpt-4o", - "swe_score": 0.332, - "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 16384 - }, - { - "id": "o1", - "swe_score": 0.489, - "cost_per_1m_tokens": { "input": 15.0, "output": 60.0 }, - "allowed_roles": ["main"] - }, - { - "id": "o3", - "swe_score": 0.5, - "cost_per_1m_tokens": { "input": 10.0, "output": 40.0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "o3-mini", - "swe_score": 0.493, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, - "allowed_roles": ["main"], - "max_tokens": 100000 - }, - { - "id": "o4-mini", - "swe_score": 0.45, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "o1-mini", - "swe_score": 0.4, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, - "allowed_roles": ["main"] - }, - { - "id": "o1-pro", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 150.0, "output": 600.0 }, - "allowed_roles": ["main"] - }, - { - "id": "gpt-4-5-preview", - "swe_score": 0.38, - "cost_per_1m_tokens": { "input": 75.0, "output": 150.0 }, - "allowed_roles": ["main"] - }, - { - "id": "gpt-4-1-mini", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, - "allowed_roles": ["main"] - }, - { - "id": "gpt-4-1-nano", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, - "allowed_roles": ["main"] - }, - { - "id": "gpt-4o-mini", - "swe_score": 0.3, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, - "allowed_roles": ["main"] - }, - { - "id": "gpt-4o-search-preview", - "swe_score": 0.33, - "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, - "allowed_roles": ["research"] - }, - { - "id": "gpt-4o-mini-search-preview", - "swe_score": 0.3, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, - "allowed_roles": ["research"] - } - ], - "google": [ - { - "id": "gemini-2.5-pro-preview-05-06", - "swe_score": 0.638, - "cost_per_1m_tokens": null, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048000 - }, - { - "id": "gemini-2.5-pro-preview-03-25", - "swe_score": 0.638, - "cost_per_1m_tokens": null, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048000 - }, - { - "id": "gemini-2.5-flash-preview-04-17", - "swe_score": 0, - "cost_per_1m_tokens": null, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048000 - }, - { - "id": "gemini-2.0-flash", - "swe_score": 0.754, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048000 - }, - { - "id": "gemini-2.0-flash-lite", - "swe_score": 0, - "cost_per_1m_tokens": null, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048000 - } - ], - "perplexity": [ - { - "id": "sonar-pro", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 3, "output": 15 }, - "allowed_roles": ["main", "research"], - "max_tokens": 8700 - }, - { - "id": "sonar", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 1, "output": 1 }, - "allowed_roles": ["research"], - "max_tokens": 8700 - }, - { - "id": "deep-research", - "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, - "allowed_roles": ["research"], - "max_tokens": 8700 - }, - { - "id": "sonar-reasoning-pro", - "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, - "allowed_roles": ["main", "research", "fallback"], - "max_tokens": 8700 - }, - { - "id": "sonar-reasoning", - "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 1, "output": 5 }, - "allowed_roles": ["main", "research", "fallback"], - "max_tokens": 8700 - } - ], - "xai": [ - { - "id": "grok-3", - "name": "Grok 3", - "swe_score": null, - "cost_per_1m_tokens": { "input": 3, "output": 15 }, - "allowed_roles": ["main", "fallback", "research"], - "max_tokens": 131072 - }, - { - "id": "grok-3-fast", - "name": "Grok 3 Fast", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 5, "output": 25 }, - "allowed_roles": ["main", "fallback", "research"], - "max_tokens": 131072 - } - ], - "ollama": [ - { - "id": "devstral:latest", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "qwen3:latest", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "qwen3:14b", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "qwen3:32b", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "mistral-small3.1:latest", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "llama3.3:latest", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - }, - { - "id": "phi4:latest", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"] - } - ], - "openrouter": [ - { - "id": "google/gemini-2.5-flash-preview-05-20", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048576 - }, - { - "id": "google/gemini-2.5-flash-preview-05-20:thinking", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 3.5 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048576 - }, - { - "id": "google/gemini-2.5-pro-exp-03-25", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "deepseek/deepseek-chat-v3-0324:free", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 163840 - }, - { - "id": "deepseek/deepseek-chat-v3-0324", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.27, "output": 1.1 }, - "allowed_roles": ["main"], - "max_tokens": 64000 - }, - { - "id": "openai/gpt-4.1", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "openai/gpt-4.1-mini", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "openai/gpt-4.1-nano", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "openai/o3", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 10, "output": 40 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 200000 - }, - { - "id": "openai/codex-mini", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.5, "output": 6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "openai/gpt-4o-mini", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "openai/o4-mini", - "swe_score": 0.45, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "openai/o4-mini-high", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "openai/o1-pro", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 150, "output": 600 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "meta-llama/llama-3.3-70b-instruct", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 120, "output": 600 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1048576 - }, - { - "id": "meta-llama/llama-4-maverick", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.18, "output": 0.6 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "meta-llama/llama-4-scout", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.08, "output": 0.3 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "qwen/qwen-max", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.6, "output": 6.4 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 32768 - }, - { - "id": "qwen/qwen-turbo", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.05, "output": 0.2 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 1000000 - }, - { - "id": "qwen/qwen3-235b-a22b", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.14, "output": 2 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 24000 - }, - { - "id": "mistralai/mistral-small-3.1-24b-instruct:free", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 96000 - }, - { - "id": "mistralai/mistral-small-3.1-24b-instruct", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 128000 - }, - { - "id": "mistralai/devstral-small", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, - "allowed_roles": ["main"], - "max_tokens": 110000 - }, - { - "id": "mistralai/mistral-nemo", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.03, "output": 0.07 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 100000 - }, - { - "id": "thudm/glm-4-32b:free", - "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, - "allowed_roles": ["main", "fallback"], - "max_tokens": 32768 - } - ] + "anthropic": [ + { + "id": "claude-sonnet-4-20250514", + "swe_score": 0.727, + "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 64000 + }, + { + "id": "claude-opus-4-20250514", + "swe_score": 0.725, + "cost_per_1m_tokens": { "input": 15.0, "output": 75.0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 32000 + }, + { + "id": "claude-3-7-sonnet-20250219", + "swe_score": 0.623, + "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 120000 + }, + { + "id": "claude-3-5-sonnet-20241022", + "swe_score": 0.49, + "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 64000 + } + ], + "openai": [ + { + "id": "gpt-4o", + "swe_score": 0.332, + "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 16384 + }, + { + "id": "o1", + "swe_score": 0.489, + "cost_per_1m_tokens": { "input": 15.0, "output": 60.0 }, + "allowed_roles": ["main"] + }, + { + "id": "o3", + "swe_score": 0.5, + "cost_per_1m_tokens": { "input": 10.0, "output": 40.0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "o3-mini", + "swe_score": 0.493, + "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "allowed_roles": ["main"], + "max_tokens": 100000 + }, + { + "id": "o4-mini", + "swe_score": 0.45, + "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "o1-mini", + "swe_score": 0.4, + "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "allowed_roles": ["main"] + }, + { + "id": "o1-pro", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 150.0, "output": 600.0 }, + "allowed_roles": ["main"] + }, + { + "id": "gpt-4-5-preview", + "swe_score": 0.38, + "cost_per_1m_tokens": { "input": 75.0, "output": 150.0 }, + "allowed_roles": ["main"] + }, + { + "id": "gpt-4-1-mini", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, + "allowed_roles": ["main"] + }, + { + "id": "gpt-4-1-nano", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, + "allowed_roles": ["main"] + }, + { + "id": "gpt-4o-mini", + "swe_score": 0.3, + "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "allowed_roles": ["main"] + }, + { + "id": "gpt-4o-search-preview", + "swe_score": 0.33, + "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, + "allowed_roles": ["research"] + }, + { + "id": "gpt-4o-mini-search-preview", + "swe_score": 0.3, + "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "allowed_roles": ["research"] + } + ], + "google": [ + { + "id": "gemini-2.5-pro-preview-05-06", + "swe_score": 0.638, + "cost_per_1m_tokens": null, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048000 + }, + { + "id": "gemini-2.5-pro-preview-03-25", + "swe_score": 0.638, + "cost_per_1m_tokens": null, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048000 + }, + { + "id": "gemini-2.5-flash-preview-04-17", + "swe_score": 0, + "cost_per_1m_tokens": null, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048000 + }, + { + "id": "gemini-2.0-flash", + "swe_score": 0.754, + "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048000 + }, + { + "id": "gemini-2.0-flash-lite", + "swe_score": 0, + "cost_per_1m_tokens": null, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048000 + } + ], + "perplexity": [ + { + "id": "sonar-pro", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 3, "output": 15 }, + "allowed_roles": ["main", "research"], + "max_tokens": 8700 + }, + { + "id": "sonar", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 1, "output": 1 }, + "allowed_roles": ["research"], + "max_tokens": 8700 + }, + { + "id": "deep-research", + "swe_score": 0.211, + "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "allowed_roles": ["research"], + "max_tokens": 8700 + }, + { + "id": "sonar-reasoning-pro", + "swe_score": 0.211, + "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "allowed_roles": ["main", "research", "fallback"], + "max_tokens": 8700 + }, + { + "id": "sonar-reasoning", + "swe_score": 0.211, + "cost_per_1m_tokens": { "input": 1, "output": 5 }, + "allowed_roles": ["main", "research", "fallback"], + "max_tokens": 8700 + } + ], + "xai": [ + { + "id": "grok-3", + "name": "Grok 3", + "swe_score": null, + "cost_per_1m_tokens": { "input": 3, "output": 15 }, + "allowed_roles": ["main", "fallback", "research"], + "max_tokens": 131072 + }, + { + "id": "grok-3-fast", + "name": "Grok 3 Fast", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 5, "output": 25 }, + "allowed_roles": ["main", "fallback", "research"], + "max_tokens": 131072 + } + ], + "ollama": [ + { + "id": "devstral:latest", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "qwen3:latest", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "qwen3:14b", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "qwen3:32b", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "mistral-small3.1:latest", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "llama3.3:latest", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + }, + { + "id": "phi4:latest", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"] + } + ], + "openrouter": [ + { + "id": "google/gemini-2.5-flash-preview-05-20", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048576 + }, + { + "id": "google/gemini-2.5-flash-preview-05-20:thinking", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.15, "output": 3.5 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048576 + }, + { + "id": "google/gemini-2.5-pro-exp-03-25", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "deepseek/deepseek-chat-v3-0324:free", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 163840 + }, + { + "id": "deepseek/deepseek-chat-v3-0324", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.27, "output": 1.1 }, + "allowed_roles": ["main"], + "max_tokens": 64000 + }, + { + "id": "openai/gpt-4.1", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "openai/gpt-4.1-mini", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "openai/gpt-4.1-nano", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "openai/o3", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 10, "output": 40 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 200000 + }, + { + "id": "openai/codex-mini", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 1.5, "output": 6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "openai/gpt-4o-mini", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "openai/o4-mini", + "swe_score": 0.45, + "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "openai/o4-mini-high", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "openai/o1-pro", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 150, "output": 600 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "meta-llama/llama-3.3-70b-instruct", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 120, "output": 600 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1048576 + }, + { + "id": "meta-llama/llama-4-maverick", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.18, "output": 0.6 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "meta-llama/llama-4-scout", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.08, "output": 0.3 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "qwen/qwen-max", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 1.6, "output": 6.4 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 32768 + }, + { + "id": "qwen/qwen-turbo", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.05, "output": 0.2 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 1000000 + }, + { + "id": "qwen/qwen3-235b-a22b", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.14, "output": 2 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 24000 + }, + { + "id": "mistralai/mistral-small-3.1-24b-instruct:free", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 96000 + }, + { + "id": "mistralai/mistral-small-3.1-24b-instruct", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 128000 + }, + { + "id": "mistralai/devstral-small", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, + "allowed_roles": ["main"], + "max_tokens": 110000 + }, + { + "id": "mistralai/mistral-nemo", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0.03, "output": 0.07 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 100000 + }, + { + "id": "thudm/glm-4-32b:free", + "swe_score": 0, + "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "allowed_roles": ["main", "fallback"], + "max_tokens": 32768 + } + ] } diff --git a/scripts/modules/task-manager/add-task.js b/scripts/modules/task-manager/add-task.js index e51e0884..57d00172 100644 --- a/scripts/modules/task-manager/add-task.js +++ b/scripts/modules/task-manager/add-task.js @@ -1,42 +1,42 @@ -import path from "path"; -import chalk from "chalk"; -import boxen from "boxen"; -import Table from "cli-table3"; -import { z } from "zod"; -import Fuse from "fuse.js"; // Import Fuse.js for advanced fuzzy search +import path from 'path'; +import chalk from 'chalk'; +import boxen from 'boxen'; +import Table from 'cli-table3'; +import { z } from 'zod'; +import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search import { - displayBanner, - getStatusWithColor, - startLoadingIndicator, - stopLoadingIndicator, - succeedLoadingIndicator, - failLoadingIndicator, - displayAiUsageSummary, -} from "../ui.js"; -import { readJSON, writeJSON, log as consoleLog, truncate } from "../utils.js"; -import { generateObjectService } from "../ai-services-unified.js"; -import { getDefaultPriority } from "../config-manager.js"; -import generateTaskFiles from "./generate-task-files.js"; + displayBanner, + getStatusWithColor, + startLoadingIndicator, + stopLoadingIndicator, + succeedLoadingIndicator, + failLoadingIndicator, + displayAiUsageSummary +} from '../ui.js'; +import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js'; +import { generateObjectService } from '../ai-services-unified.js'; +import { getDefaultPriority } from '../config-manager.js'; +import generateTaskFiles from './generate-task-files.js'; // Define Zod schema for the expected AI output object const AiTaskDataSchema = z.object({ - title: z.string().describe("Clear, concise title for the task"), - description: z - .string() - .describe("A one or two sentence description of the task"), - details: z - .string() - .describe("In-depth implementation details, considerations, and guidance"), - testStrategy: z - .string() - .describe("Detailed approach for verifying task completion"), - dependencies: z - .array(z.number()) - .optional() - .describe( - "Array of task IDs that this task depends on (must be completed before this task can start)" - ), + title: z.string().describe('Clear, concise title for the task'), + description: z + .string() + .describe('A one or two sentence description of the task'), + details: z + .string() + .describe('In-depth implementation details, considerations, and guidance'), + testStrategy: z + .string() + .describe('Detailed approach for verifying task completion'), + dependencies: z + .array(z.number()) + .optional() + .describe( + 'Array of task IDs that this task depends on (must be completed before this task can start)' + ) }); /** @@ -59,790 +59,790 @@ const AiTaskDataSchema = z.object({ * @returns {Promise} An object containing newTaskId and telemetryData */ async function addTask( - tasksPath, - prompt, - dependencies = [], - priority = null, - context = {}, - outputFormat = "text", // Default to text for CLI - manualTaskData = null, - useResearch = false + tasksPath, + prompt, + dependencies = [], + priority = null, + context = {}, + outputFormat = 'text', // Default to text for CLI + manualTaskData = null, + useResearch = false ) { - const { session, mcpLog, projectRoot, commandName, outputType } = context; - const isMCP = !!mcpLog; - - // Create a consistent logFn object regardless of context - const logFn = isMCP - ? mcpLog // Use MCP logger if provided - : { - // Create a wrapper around consoleLog for CLI - info: (...args) => consoleLog("info", ...args), - warn: (...args) => consoleLog("warn", ...args), - error: (...args) => consoleLog("error", ...args), - debug: (...args) => consoleLog("debug", ...args), - success: (...args) => consoleLog("success", ...args), - }; - - const effectivePriority = priority || getDefaultPriority(projectRoot); - - logFn.info( - `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(", ") || "None"}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` - ); - - let loadingIndicator = null; - let aiServiceResponse = null; // To store the full response from AI service - - // Create custom reporter that checks for MCP log - const report = (message, level = "info") => { - if (mcpLog) { - mcpLog[level](message); - } else if (outputFormat === "text") { - consoleLog(level, message); - } - }; - - /** - * Recursively builds a dependency graph for a given task - * @param {Array} tasks - All tasks from tasks.json - * @param {number} taskId - ID of the task to analyze - * @param {Set} visited - Set of already visited task IDs - * @param {Map} depthMap - Map of task ID to its depth in the graph - * @param {number} depth - Current depth in the recursion - * @return {Object} Dependency graph data - */ - function buildDependencyGraph( - tasks, - taskId, - visited = new Set(), - depthMap = new Map(), - depth = 0 - ) { - // Skip if we've already visited this task or it doesn't exist - if (visited.has(taskId)) { - return null; - } - - // Find the task - const task = tasks.find((t) => t.id === taskId); - if (!task) { - return null; - } - - // Mark as visited - visited.add(taskId); - - // Update depth if this is a deeper path to this task - if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { - depthMap.set(taskId, depth); - } - - // Process dependencies - const dependencyData = []; - if (task.dependencies && task.dependencies.length > 0) { - for (const depId of task.dependencies) { - const depData = buildDependencyGraph( - tasks, - depId, - visited, - depthMap, - depth + 1 - ); - if (depData) { - dependencyData.push(depData); - } - } - } - - return { - id: task.id, - title: task.title, - description: task.description, - status: task.status, - dependencies: dependencyData, - }; - } - - try { - // Read the existing tasks - let data = readJSON(tasksPath); - - // If tasks.json doesn't exist or is invalid, create a new one - if (!data || !data.tasks) { - report("tasks.json not found or invalid. Creating a new one.", "info"); - // Create default tasks data structure - data = { - tasks: [], - }; - // Ensure the directory exists and write the new file - writeJSON(tasksPath, data); - report("Created new tasks.json file with empty tasks array.", "info"); - } - - // Find the highest task ID to determine the next ID - const highestId = - data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0; - const newTaskId = highestId + 1; - - // Only show UI box for CLI mode - if (outputFormat === "text") { - console.log( - boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { - padding: 1, - borderColor: "blue", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - }) - ); - } - - // Validate dependencies before proceeding - const invalidDeps = dependencies.filter((depId) => { - // Ensure depId is parsed as a number for comparison - const numDepId = parseInt(depId, 10); - return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); - }); - - if (invalidDeps.length > 0) { - report( - `The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`, - "warn" - ); - report("Removing invalid dependencies...", "info"); - dependencies = dependencies.filter( - (depId) => !invalidDeps.includes(depId) - ); - } - // Ensure dependencies are numbers - const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); - - // Build dependency graphs for explicitly specified dependencies - const dependencyGraphs = []; - const allRelatedTaskIds = new Set(); - const depthMap = new Map(); - - // First pass: build a complete dependency graph for each specified dependency - for (const depId of numericDependencies) { - const graph = buildDependencyGraph( - data.tasks, - depId, - new Set(), - depthMap - ); - if (graph) { - dependencyGraphs.push(graph); - } - } - - // Second pass: build a set of all related task IDs for flat analysis - for (const [taskId, depth] of depthMap.entries()) { - allRelatedTaskIds.add(taskId); - } - - let taskData; - - // Check if manual task data is provided - if (manualTaskData) { - report("Using manually provided task data", "info"); - taskData = manualTaskData; - report("DEBUG: Taking MANUAL task data path.", "debug"); - - // Basic validation for manual data - if ( - !taskData.title || - typeof taskData.title !== "string" || - !taskData.description || - typeof taskData.description !== "string" - ) { - throw new Error( - "Manual task data must include at least a title and description." - ); - } - } else { - report("DEBUG: Taking AI task generation path.", "debug"); - // --- Refactored AI Interaction --- - report(`Generating task data with AI with prompt:\n${prompt}`, "info"); - - // Create context string for task creation prompt - let contextTasks = ""; - - // Create a dependency map for better understanding of the task relationships - const taskMap = {}; - data.tasks.forEach((t) => { - // For each task, only include id, title, description, and dependencies - taskMap[t.id] = { - id: t.id, - title: t.title, - description: t.description, - dependencies: t.dependencies || [], - status: t.status, - }; - }); - - // CLI-only feedback for the dependency analysis - if (outputFormat === "text") { - console.log( - boxen(chalk.cyan.bold("Task Context Analysis"), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 0, bottom: 0 }, - borderColor: "cyan", - borderStyle: "round", - }) - ); - } - - // Initialize variables that will be used in either branch - let uniqueDetailedTasks = []; - let dependentTasks = []; - let promptCategory = null; - - if (numericDependencies.length > 0) { - // If specific dependencies were provided, focus on them - // Get all tasks that were found in the dependency graph - dependentTasks = Array.from(allRelatedTaskIds) - .map((id) => data.tasks.find((t) => t.id === id)) - .filter(Boolean); - - // Sort by depth in the dependency chain - dependentTasks.sort((a, b) => { - const depthA = depthMap.get(a.id) || 0; - const depthB = depthMap.get(b.id) || 0; - return depthA - depthB; // Lowest depth (root dependencies) first - }); - - // Limit the number of detailed tasks to avoid context explosion - uniqueDetailedTasks = dependentTasks.slice(0, 8); - - contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`; - const directDeps = data.tasks.filter((t) => - numericDependencies.includes(t.id) - ); - contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join("\n")}`; - - // Add an overview of indirect dependencies if present - const indirectDeps = dependentTasks.filter( - (t) => !numericDependencies.includes(t.id) - ); - if (indirectDeps.length > 0) { - contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`; - contextTasks += `\n${indirectDeps - .slice(0, 5) - .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) - .join("\n")}`; - if (indirectDeps.length > 5) { - contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; - } - } - - // Add more details about each dependency, prioritizing direct dependencies - contextTasks += `\n\nDetailed information about dependencies:`; - for (const depTask of uniqueDetailedTasks) { - const depthInfo = depthMap.get(depTask.id) - ? ` (depth: ${depthMap.get(depTask.id)})` - : ""; - const isDirect = numericDependencies.includes(depTask.id) - ? " [DIRECT DEPENDENCY]" - : ""; - - contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`; - contextTasks += `Description: ${depTask.description}\n`; - contextTasks += `Status: ${depTask.status || "pending"}\n`; - contextTasks += `Priority: ${depTask.priority || "medium"}\n`; - - // List its dependencies - if (depTask.dependencies && depTask.dependencies.length > 0) { - const depDeps = depTask.dependencies.map((dId) => { - const depDepTask = data.tasks.find((t) => t.id === dId); - return depDepTask - ? `Task ${dId}: ${depDepTask.title}` - : `Task ${dId}`; - }); - contextTasks += `Dependencies: ${depDeps.join(", ")}\n`; - } else { - contextTasks += `Dependencies: None\n`; - } - - // Add implementation details but truncate if too long - if (depTask.details) { - const truncatedDetails = - depTask.details.length > 400 - ? depTask.details.substring(0, 400) + "... (truncated)" - : depTask.details; - contextTasks += `Implementation Details: ${truncatedDetails}\n`; - } - } - - // Add dependency chain visualization - if (dependencyGraphs.length > 0) { - contextTasks += "\n\nDependency Chain Visualization:"; - - // Helper function to format dependency chain as text - function formatDependencyChain( - node, - prefix = "", - isLast = true, - depth = 0 - ) { - if (depth > 3) return ""; // Limit depth to avoid excessive nesting - - const connector = isLast ? "└── " : "├── "; - const childPrefix = isLast ? " " : "│ "; - - let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; - - if (node.dependencies && node.dependencies.length > 0) { - for (let i = 0; i < node.dependencies.length; i++) { - const isLastChild = i === node.dependencies.length - 1; - result += formatDependencyChain( - node.dependencies[i], - prefix + childPrefix, - isLastChild, - depth + 1 - ); - } - } - - return result; - } - - // Format each dependency graph - for (const graph of dependencyGraphs) { - contextTasks += formatDependencyChain(graph); - } - } - - // Show dependency analysis in CLI mode - if (outputFormat === "text") { - if (directDeps.length > 0) { - console.log(chalk.gray(` Explicitly specified dependencies:`)); - directDeps.forEach((t) => { - console.log( - chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - if (indirectDeps.length > 0) { - console.log( - chalk.gray( - `\n Indirect dependencies (${indirectDeps.length} total):` - ) - ); - indirectDeps.slice(0, 3).forEach((t) => { - const depth = depthMap.get(t.id) || 0; - console.log( - chalk.cyan( - ` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}` - ) - ); - }); - if (indirectDeps.length > 3) { - console.log( - chalk.cyan( - ` • ... and ${indirectDeps.length - 3} more indirect dependencies` - ) - ); - } - } - - // Visualize the dependency chain - if (dependencyGraphs.length > 0) { - console.log(chalk.gray(`\n Dependency chain visualization:`)); - - // Convert dependency graph to ASCII art for terminal - function visualizeDependencyGraph( - node, - prefix = "", - isLast = true, - depth = 0 - ) { - if (depth > 2) return; // Limit depth for display - - const connector = isLast ? "└── " : "├── "; - const childPrefix = isLast ? " " : "│ "; - - console.log( - chalk.blue( - ` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}` - ) - ); - - if (node.dependencies && node.dependencies.length > 0) { - for (let i = 0; i < node.dependencies.length; i++) { - const isLastChild = i === node.dependencies.length - 1; - visualizeDependencyGraph( - node.dependencies[i], - prefix + childPrefix, - isLastChild, - depth + 1 - ); - } - } - } - - // Visualize each dependency graph - for (const graph of dependencyGraphs) { - visualizeDependencyGraph(graph); - } - } - - console.log(); // Add spacing - } - } else { - // If no dependencies provided, use Fuse.js to find semantically related tasks - // Create fuzzy search index for all tasks - const searchOptions = { - includeScore: true, // Return match scores - threshold: 0.4, // Lower threshold = stricter matching (range 0-1) - keys: [ - { name: "title", weight: 1.5 }, // Title is most important - { name: "description", weight: 2 }, // Description is very important - { name: "details", weight: 3 }, // Details is most important - // Search dependencies to find tasks that depend on similar things - { name: "dependencyTitles", weight: 0.5 }, - ], - // Sort matches by score (lower is better) - shouldSort: true, - // Allow searching in nested properties - useExtendedSearch: true, - // Return up to 50 matches - limit: 50, - }; - - // Prepare task data with dependencies expanded as titles for better semantic search - const searchableTasks = data.tasks.map((task) => { - // Get titles of this task's dependencies if they exist - const dependencyTitles = - task.dependencies?.length > 0 - ? task.dependencies - .map((depId) => { - const depTask = data.tasks.find((t) => t.id === depId); - return depTask ? depTask.title : ""; - }) - .filter((title) => title) - .join(" ") - : ""; - - return { - ...task, - dependencyTitles, - }; - }); - - // Create search index using Fuse.js - const fuse = new Fuse(searchableTasks, searchOptions); - - // Extract significant words and phrases from the prompt - const promptWords = prompt - .toLowerCase() - .replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces - .split(/\s+/) - .filter((word) => word.length > 3); // Words at least 4 chars - - // Use the user's prompt for fuzzy search - const fuzzyResults = fuse.search(prompt); - - // Also search for each significant word to catch different aspects - let wordResults = []; - for (const word of promptWords) { - if (word.length > 5) { - // Only use significant words - const results = fuse.search(word); - if (results.length > 0) { - wordResults.push(...results); - } - } - } - - // Merge and deduplicate results - const mergedResults = [...fuzzyResults]; - - // Add word results that aren't already in fuzzyResults - for (const wordResult of wordResults) { - if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) { - mergedResults.push(wordResult); - } - } - - // Group search results by relevance - const highRelevance = mergedResults - .filter((result) => result.score < 0.25) - .map((result) => result.item); - - const mediumRelevance = mergedResults - .filter((result) => result.score >= 0.25 && result.score < 0.4) - .map((result) => result.item); - - // Get recent tasks (newest first) - const recentTasks = [...data.tasks] - .sort((a, b) => b.id - a.id) - .slice(0, 5); - - // Combine high relevance, medium relevance, and recent tasks - // Prioritize high relevance first - const allRelevantTasks = [...highRelevance]; - - // Add medium relevance if not already included - for (const task of mediumRelevance) { - if (!allRelevantTasks.some((t) => t.id === task.id)) { - allRelevantTasks.push(task); - } - } - - // Add recent tasks if not already included - for (const task of recentTasks) { - if (!allRelevantTasks.some((t) => t.id === task.id)) { - allRelevantTasks.push(task); - } - } - - // Get top N results for context - const relatedTasks = allRelevantTasks.slice(0, 8); - - // Format basic task overviews - if (relatedTasks.length > 0) { - contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks - .map((t, i) => { - const relevanceMarker = i < highRelevance.length ? "⭐ " : ""; - return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`; - }) - .join("\n")}`; - } - - if ( - recentTasks.length > 0 && - !contextTasks.includes("Recently created tasks") - ) { - contextTasks += `\n\nRecently created tasks:\n${recentTasks - .filter((t) => !relatedTasks.some((rt) => rt.id === t.id)) - .slice(0, 3) - .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) - .join("\n")}`; - } - - // Add detailed information about the most relevant tasks - const allDetailedTasks = [...relatedTasks.slice(0, 25)]; - uniqueDetailedTasks = Array.from( - new Map(allDetailedTasks.map((t) => [t.id, t])).values() - ).slice(0, 20); - - if (uniqueDetailedTasks.length > 0) { - contextTasks += `\n\nDetailed information about relevant tasks:`; - for (const task of uniqueDetailedTasks) { - contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`; - contextTasks += `Description: ${task.description}\n`; - contextTasks += `Status: ${task.status || "pending"}\n`; - contextTasks += `Priority: ${task.priority || "medium"}\n`; - if (task.dependencies && task.dependencies.length > 0) { - // Format dependency list with titles - const depList = task.dependencies.map((depId) => { - const depTask = data.tasks.find((t) => t.id === depId); - return depTask - ? `Task ${depId} (${depTask.title})` - : `Task ${depId}`; - }); - contextTasks += `Dependencies: ${depList.join(", ")}\n`; - } - // Add implementation details but truncate if too long - if (task.details) { - const truncatedDetails = - task.details.length > 400 - ? task.details.substring(0, 400) + "... (truncated)" - : task.details; - contextTasks += `Implementation Details: ${truncatedDetails}\n`; - } - } - } - - // Add a concise view of the task dependency structure - contextTasks += "\n\nSummary of task dependencies in the project:"; - - // Get pending/in-progress tasks that might be most relevant based on fuzzy search - // Prioritize tasks from our similarity search - const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id)); - const relevantPendingTasks = data.tasks - .filter( - (t) => - (t.status === "pending" || t.status === "in-progress") && - // Either in our relevant set OR has relevant words in title/description - (relevantTaskIds.has(t.id) || - promptWords.some( - (word) => - t.title.toLowerCase().includes(word) || - t.description.toLowerCase().includes(word) - )) - ) - .slice(0, 10); - - for (const task of relevantPendingTasks) { - const depsStr = - task.dependencies && task.dependencies.length > 0 - ? task.dependencies.join(", ") - : "None"; - contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; - } - - // Additional analysis of common patterns - const similarPurposeTasks = data.tasks.filter((t) => - prompt.toLowerCase().includes(t.title.toLowerCase()) - ); - - let commonDeps = []; // Initialize commonDeps - - if (similarPurposeTasks.length > 0) { - contextTasks += `\n\nCommon patterns for similar tasks:`; - - // Collect dependencies from similar purpose tasks - const similarDeps = similarPurposeTasks - .filter((t) => t.dependencies && t.dependencies.length > 0) - .map((t) => t.dependencies) - .flat(); - - // Count frequency of each dependency - const depCounts = {}; - similarDeps.forEach((dep) => { - depCounts[dep] = (depCounts[dep] || 0) + 1; - }); - - // Get most common dependencies for similar tasks - commonDeps = Object.entries(depCounts) - .sort((a, b) => b[1] - a[1]) - .slice(0, 10); - - if (commonDeps.length > 0) { - contextTasks += "\nMost common dependencies for similar tasks:"; - commonDeps.forEach(([depId, count]) => { - const depTask = data.tasks.find((t) => t.id === parseInt(depId)); - if (depTask) { - contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`; - } - }); - } - } - - // Show fuzzy search analysis in CLI mode - if (outputFormat === "text") { - console.log( - chalk.gray( - ` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords` - ) - ); - - if (highRelevance.length > 0) { - console.log( - chalk.gray(`\n High relevance matches (score < 0.25):`) - ); - highRelevance.slice(0, 25).forEach((t) => { - console.log( - chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - if (mediumRelevance.length > 0) { - console.log( - chalk.gray(`\n Medium relevance matches (score < 0.4):`) - ); - mediumRelevance.slice(0, 10).forEach((t) => { - console.log( - chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - // Show dependency patterns - if (commonDeps && commonDeps.length > 0) { - console.log( - chalk.gray(`\n Common dependency patterns for similar tasks:`) - ); - commonDeps.slice(0, 3).forEach(([depId, count]) => { - const depTask = data.tasks.find((t) => t.id === parseInt(depId)); - if (depTask) { - console.log( - chalk.blue( - ` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}` - ) - ); - } - }); - } - - // Add information about which tasks will be provided in detail - if (uniqueDetailedTasks.length > 0) { - console.log( - chalk.gray( - `\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:` - ) - ); - uniqueDetailedTasks.forEach((t) => { - const isHighRelevance = highRelevance.some( - (ht) => ht.id === t.id - ); - const relevanceIndicator = isHighRelevance ? "⭐ " : ""; - console.log( - chalk.cyan( - ` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}` - ) - ); - }); - } - - console.log(); // Add spacing - } - } - - // DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT - let actualDetailedTasksCount = 0; - if (numericDependencies.length > 0) { - // In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks' - // Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate. - // For simplicity, let's assume 'dependentTasks' reflects the detailed tasks. - actualDetailedTasksCount = dependentTasks.length; - } else { - // In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct. - actualDetailedTasksCount = uniqueDetailedTasks - ? uniqueDetailedTasks.length - : 0; - } - - // Add a visual transition to show we're moving to AI generation - only for CLI - if (outputFormat === "text") { - console.log( - boxen( - chalk.white.bold("AI Task Generation") + - `\n\n${chalk.gray("Analyzing context and generating task details using AI...")}` + - `\n${chalk.cyan("Context size: ")}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + - `\n${chalk.cyan("Dependency detection: ")}${chalk.yellow(numericDependencies.length > 0 ? "Explicit dependencies" : "Auto-discovery mode")}` + - `\n${chalk.cyan("Detailed tasks: ")}${chalk.yellow( - numericDependencies.length > 0 - ? dependentTasks.length // Use length of tasks from explicit dependency path - : uniqueDetailedTasks.length // Use length of tasks from fuzzy search path - )}`, - { - padding: { top: 0, bottom: 1, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: "white", - borderStyle: "round", - } - ) - ); - console.log(); // Add spacing - } - - // System Prompt - Enhanced for dependency awareness - const systemPrompt = - "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + - "When determining dependencies for a new task, follow these principles:\n" + - "1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" + - "2. Prioritize task dependencies that are semantically related to the functionality being built.\n" + - "3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" + - "4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" + - "5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" + - "6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" + - "7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" + - "The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n"; - - // Task Structure Description (for user prompt) - const taskStructureDesc = ` + const { session, mcpLog, projectRoot, commandName, outputType } = context; + const isMCP = !!mcpLog; + + // Create a consistent logFn object regardless of context + const logFn = isMCP + ? mcpLog // Use MCP logger if provided + : { + // Create a wrapper around consoleLog for CLI + info: (...args) => consoleLog('info', ...args), + warn: (...args) => consoleLog('warn', ...args), + error: (...args) => consoleLog('error', ...args), + debug: (...args) => consoleLog('debug', ...args), + success: (...args) => consoleLog('success', ...args) + }; + + const effectivePriority = priority || getDefaultPriority(projectRoot); + + logFn.info( + `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` + ); + + let loadingIndicator = null; + let aiServiceResponse = null; // To store the full response from AI service + + // Create custom reporter that checks for MCP log + const report = (message, level = 'info') => { + if (mcpLog) { + mcpLog[level](message); + } else if (outputFormat === 'text') { + consoleLog(level, message); + } + }; + + /** + * Recursively builds a dependency graph for a given task + * @param {Array} tasks - All tasks from tasks.json + * @param {number} taskId - ID of the task to analyze + * @param {Set} visited - Set of already visited task IDs + * @param {Map} depthMap - Map of task ID to its depth in the graph + * @param {number} depth - Current depth in the recursion + * @return {Object} Dependency graph data + */ + function buildDependencyGraph( + tasks, + taskId, + visited = new Set(), + depthMap = new Map(), + depth = 0 + ) { + // Skip if we've already visited this task or it doesn't exist + if (visited.has(taskId)) { + return null; + } + + // Find the task + const task = tasks.find((t) => t.id === taskId); + if (!task) { + return null; + } + + // Mark as visited + visited.add(taskId); + + // Update depth if this is a deeper path to this task + if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { + depthMap.set(taskId, depth); + } + + // Process dependencies + const dependencyData = []; + if (task.dependencies && task.dependencies.length > 0) { + for (const depId of task.dependencies) { + const depData = buildDependencyGraph( + tasks, + depId, + visited, + depthMap, + depth + 1 + ); + if (depData) { + dependencyData.push(depData); + } + } + } + + return { + id: task.id, + title: task.title, + description: task.description, + status: task.status, + dependencies: dependencyData + }; + } + + try { + // Read the existing tasks + let data = readJSON(tasksPath); + + // If tasks.json doesn't exist or is invalid, create a new one + if (!data || !data.tasks) { + report('tasks.json not found or invalid. Creating a new one.', 'info'); + // Create default tasks data structure + data = { + tasks: [] + }; + // Ensure the directory exists and write the new file + writeJSON(tasksPath, data); + report('Created new tasks.json file with empty tasks array.', 'info'); + } + + // Find the highest task ID to determine the next ID + const highestId = + data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0; + const newTaskId = highestId + 1; + + // Only show UI box for CLI mode + if (outputFormat === 'text') { + console.log( + boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + }) + ); + } + + // Validate dependencies before proceeding + const invalidDeps = dependencies.filter((depId) => { + // Ensure depId is parsed as a number for comparison + const numDepId = parseInt(depId, 10); + return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); + }); + + if (invalidDeps.length > 0) { + report( + `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, + 'warn' + ); + report('Removing invalid dependencies...', 'info'); + dependencies = dependencies.filter( + (depId) => !invalidDeps.includes(depId) + ); + } + // Ensure dependencies are numbers + const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); + + // Build dependency graphs for explicitly specified dependencies + const dependencyGraphs = []; + const allRelatedTaskIds = new Set(); + const depthMap = new Map(); + + // First pass: build a complete dependency graph for each specified dependency + for (const depId of numericDependencies) { + const graph = buildDependencyGraph( + data.tasks, + depId, + new Set(), + depthMap + ); + if (graph) { + dependencyGraphs.push(graph); + } + } + + // Second pass: build a set of all related task IDs for flat analysis + for (const [taskId, depth] of depthMap.entries()) { + allRelatedTaskIds.add(taskId); + } + + let taskData; + + // Check if manual task data is provided + if (manualTaskData) { + report('Using manually provided task data', 'info'); + taskData = manualTaskData; + report('DEBUG: Taking MANUAL task data path.', 'debug'); + + // Basic validation for manual data + if ( + !taskData.title || + typeof taskData.title !== 'string' || + !taskData.description || + typeof taskData.description !== 'string' + ) { + throw new Error( + 'Manual task data must include at least a title and description.' + ); + } + } else { + report('DEBUG: Taking AI task generation path.', 'debug'); + // --- Refactored AI Interaction --- + report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); + + // Create context string for task creation prompt + let contextTasks = ''; + + // Create a dependency map for better understanding of the task relationships + const taskMap = {}; + data.tasks.forEach((t) => { + // For each task, only include id, title, description, and dependencies + taskMap[t.id] = { + id: t.id, + title: t.title, + description: t.description, + dependencies: t.dependencies || [], + status: t.status + }; + }); + + // CLI-only feedback for the dependency analysis + if (outputFormat === 'text') { + console.log( + boxen(chalk.cyan.bold('Task Context Analysis'), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 0, bottom: 0 }, + borderColor: 'cyan', + borderStyle: 'round' + }) + ); + } + + // Initialize variables that will be used in either branch + let uniqueDetailedTasks = []; + let dependentTasks = []; + let promptCategory = null; + + if (numericDependencies.length > 0) { + // If specific dependencies were provided, focus on them + // Get all tasks that were found in the dependency graph + dependentTasks = Array.from(allRelatedTaskIds) + .map((id) => data.tasks.find((t) => t.id === id)) + .filter(Boolean); + + // Sort by depth in the dependency chain + dependentTasks.sort((a, b) => { + const depthA = depthMap.get(a.id) || 0; + const depthB = depthMap.get(b.id) || 0; + return depthA - depthB; // Lowest depth (root dependencies) first + }); + + // Limit the number of detailed tasks to avoid context explosion + uniqueDetailedTasks = dependentTasks.slice(0, 8); + + contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`; + const directDeps = data.tasks.filter((t) => + numericDependencies.includes(t.id) + ); + contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`; + + // Add an overview of indirect dependencies if present + const indirectDeps = dependentTasks.filter( + (t) => !numericDependencies.includes(t.id) + ); + if (indirectDeps.length > 0) { + contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`; + contextTasks += `\n${indirectDeps + .slice(0, 5) + .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) + .join('\n')}`; + if (indirectDeps.length > 5) { + contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; + } + } + + // Add more details about each dependency, prioritizing direct dependencies + contextTasks += `\n\nDetailed information about dependencies:`; + for (const depTask of uniqueDetailedTasks) { + const depthInfo = depthMap.get(depTask.id) + ? ` (depth: ${depthMap.get(depTask.id)})` + : ''; + const isDirect = numericDependencies.includes(depTask.id) + ? ' [DIRECT DEPENDENCY]' + : ''; + + contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`; + contextTasks += `Description: ${depTask.description}\n`; + contextTasks += `Status: ${depTask.status || 'pending'}\n`; + contextTasks += `Priority: ${depTask.priority || 'medium'}\n`; + + // List its dependencies + if (depTask.dependencies && depTask.dependencies.length > 0) { + const depDeps = depTask.dependencies.map((dId) => { + const depDepTask = data.tasks.find((t) => t.id === dId); + return depDepTask + ? `Task ${dId}: ${depDepTask.title}` + : `Task ${dId}`; + }); + contextTasks += `Dependencies: ${depDeps.join(', ')}\n`; + } else { + contextTasks += `Dependencies: None\n`; + } + + // Add implementation details but truncate if too long + if (depTask.details) { + const truncatedDetails = + depTask.details.length > 400 + ? depTask.details.substring(0, 400) + '... (truncated)' + : depTask.details; + contextTasks += `Implementation Details: ${truncatedDetails}\n`; + } + } + + // Add dependency chain visualization + if (dependencyGraphs.length > 0) { + contextTasks += '\n\nDependency Chain Visualization:'; + + // Helper function to format dependency chain as text + function formatDependencyChain( + node, + prefix = '', + isLast = true, + depth = 0 + ) { + if (depth > 3) return ''; // Limit depth to avoid excessive nesting + + const connector = isLast ? '└── ' : '├── '; + const childPrefix = isLast ? ' ' : '│ '; + + let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; + + if (node.dependencies && node.dependencies.length > 0) { + for (let i = 0; i < node.dependencies.length; i++) { + const isLastChild = i === node.dependencies.length - 1; + result += formatDependencyChain( + node.dependencies[i], + prefix + childPrefix, + isLastChild, + depth + 1 + ); + } + } + + return result; + } + + // Format each dependency graph + for (const graph of dependencyGraphs) { + contextTasks += formatDependencyChain(graph); + } + } + + // Show dependency analysis in CLI mode + if (outputFormat === 'text') { + if (directDeps.length > 0) { + console.log(chalk.gray(` Explicitly specified dependencies:`)); + directDeps.forEach((t) => { + console.log( + chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + if (indirectDeps.length > 0) { + console.log( + chalk.gray( + `\n Indirect dependencies (${indirectDeps.length} total):` + ) + ); + indirectDeps.slice(0, 3).forEach((t) => { + const depth = depthMap.get(t.id) || 0; + console.log( + chalk.cyan( + ` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}` + ) + ); + }); + if (indirectDeps.length > 3) { + console.log( + chalk.cyan( + ` • ... and ${indirectDeps.length - 3} more indirect dependencies` + ) + ); + } + } + + // Visualize the dependency chain + if (dependencyGraphs.length > 0) { + console.log(chalk.gray(`\n Dependency chain visualization:`)); + + // Convert dependency graph to ASCII art for terminal + function visualizeDependencyGraph( + node, + prefix = '', + isLast = true, + depth = 0 + ) { + if (depth > 2) return; // Limit depth for display + + const connector = isLast ? '└── ' : '├── '; + const childPrefix = isLast ? ' ' : '│ '; + + console.log( + chalk.blue( + ` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}` + ) + ); + + if (node.dependencies && node.dependencies.length > 0) { + for (let i = 0; i < node.dependencies.length; i++) { + const isLastChild = i === node.dependencies.length - 1; + visualizeDependencyGraph( + node.dependencies[i], + prefix + childPrefix, + isLastChild, + depth + 1 + ); + } + } + } + + // Visualize each dependency graph + for (const graph of dependencyGraphs) { + visualizeDependencyGraph(graph); + } + } + + console.log(); // Add spacing + } + } else { + // If no dependencies provided, use Fuse.js to find semantically related tasks + // Create fuzzy search index for all tasks + const searchOptions = { + includeScore: true, // Return match scores + threshold: 0.4, // Lower threshold = stricter matching (range 0-1) + keys: [ + { name: 'title', weight: 1.5 }, // Title is most important + { name: 'description', weight: 2 }, // Description is very important + { name: 'details', weight: 3 }, // Details is most important + // Search dependencies to find tasks that depend on similar things + { name: 'dependencyTitles', weight: 0.5 } + ], + // Sort matches by score (lower is better) + shouldSort: true, + // Allow searching in nested properties + useExtendedSearch: true, + // Return up to 50 matches + limit: 50 + }; + + // Prepare task data with dependencies expanded as titles for better semantic search + const searchableTasks = data.tasks.map((task) => { + // Get titles of this task's dependencies if they exist + const dependencyTitles = + task.dependencies?.length > 0 + ? task.dependencies + .map((depId) => { + const depTask = data.tasks.find((t) => t.id === depId); + return depTask ? depTask.title : ''; + }) + .filter((title) => title) + .join(' ') + : ''; + + return { + ...task, + dependencyTitles + }; + }); + + // Create search index using Fuse.js + const fuse = new Fuse(searchableTasks, searchOptions); + + // Extract significant words and phrases from the prompt + const promptWords = prompt + .toLowerCase() + .replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces + .split(/\s+/) + .filter((word) => word.length > 3); // Words at least 4 chars + + // Use the user's prompt for fuzzy search + const fuzzyResults = fuse.search(prompt); + + // Also search for each significant word to catch different aspects + let wordResults = []; + for (const word of promptWords) { + if (word.length > 5) { + // Only use significant words + const results = fuse.search(word); + if (results.length > 0) { + wordResults.push(...results); + } + } + } + + // Merge and deduplicate results + const mergedResults = [...fuzzyResults]; + + // Add word results that aren't already in fuzzyResults + for (const wordResult of wordResults) { + if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) { + mergedResults.push(wordResult); + } + } + + // Group search results by relevance + const highRelevance = mergedResults + .filter((result) => result.score < 0.25) + .map((result) => result.item); + + const mediumRelevance = mergedResults + .filter((result) => result.score >= 0.25 && result.score < 0.4) + .map((result) => result.item); + + // Get recent tasks (newest first) + const recentTasks = [...data.tasks] + .sort((a, b) => b.id - a.id) + .slice(0, 5); + + // Combine high relevance, medium relevance, and recent tasks + // Prioritize high relevance first + const allRelevantTasks = [...highRelevance]; + + // Add medium relevance if not already included + for (const task of mediumRelevance) { + if (!allRelevantTasks.some((t) => t.id === task.id)) { + allRelevantTasks.push(task); + } + } + + // Add recent tasks if not already included + for (const task of recentTasks) { + if (!allRelevantTasks.some((t) => t.id === task.id)) { + allRelevantTasks.push(task); + } + } + + // Get top N results for context + const relatedTasks = allRelevantTasks.slice(0, 8); + + // Format basic task overviews + if (relatedTasks.length > 0) { + contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks + .map((t, i) => { + const relevanceMarker = i < highRelevance.length ? '⭐ ' : ''; + return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`; + }) + .join('\n')}`; + } + + if ( + recentTasks.length > 0 && + !contextTasks.includes('Recently created tasks') + ) { + contextTasks += `\n\nRecently created tasks:\n${recentTasks + .filter((t) => !relatedTasks.some((rt) => rt.id === t.id)) + .slice(0, 3) + .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) + .join('\n')}`; + } + + // Add detailed information about the most relevant tasks + const allDetailedTasks = [...relatedTasks.slice(0, 25)]; + uniqueDetailedTasks = Array.from( + new Map(allDetailedTasks.map((t) => [t.id, t])).values() + ).slice(0, 20); + + if (uniqueDetailedTasks.length > 0) { + contextTasks += `\n\nDetailed information about relevant tasks:`; + for (const task of uniqueDetailedTasks) { + contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`; + contextTasks += `Description: ${task.description}\n`; + contextTasks += `Status: ${task.status || 'pending'}\n`; + contextTasks += `Priority: ${task.priority || 'medium'}\n`; + if (task.dependencies && task.dependencies.length > 0) { + // Format dependency list with titles + const depList = task.dependencies.map((depId) => { + const depTask = data.tasks.find((t) => t.id === depId); + return depTask + ? `Task ${depId} (${depTask.title})` + : `Task ${depId}`; + }); + contextTasks += `Dependencies: ${depList.join(', ')}\n`; + } + // Add implementation details but truncate if too long + if (task.details) { + const truncatedDetails = + task.details.length > 400 + ? task.details.substring(0, 400) + '... (truncated)' + : task.details; + contextTasks += `Implementation Details: ${truncatedDetails}\n`; + } + } + } + + // Add a concise view of the task dependency structure + contextTasks += '\n\nSummary of task dependencies in the project:'; + + // Get pending/in-progress tasks that might be most relevant based on fuzzy search + // Prioritize tasks from our similarity search + const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id)); + const relevantPendingTasks = data.tasks + .filter( + (t) => + (t.status === 'pending' || t.status === 'in-progress') && + // Either in our relevant set OR has relevant words in title/description + (relevantTaskIds.has(t.id) || + promptWords.some( + (word) => + t.title.toLowerCase().includes(word) || + t.description.toLowerCase().includes(word) + )) + ) + .slice(0, 10); + + for (const task of relevantPendingTasks) { + const depsStr = + task.dependencies && task.dependencies.length > 0 + ? task.dependencies.join(', ') + : 'None'; + contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; + } + + // Additional analysis of common patterns + const similarPurposeTasks = data.tasks.filter((t) => + prompt.toLowerCase().includes(t.title.toLowerCase()) + ); + + let commonDeps = []; // Initialize commonDeps + + if (similarPurposeTasks.length > 0) { + contextTasks += `\n\nCommon patterns for similar tasks:`; + + // Collect dependencies from similar purpose tasks + const similarDeps = similarPurposeTasks + .filter((t) => t.dependencies && t.dependencies.length > 0) + .map((t) => t.dependencies) + .flat(); + + // Count frequency of each dependency + const depCounts = {}; + similarDeps.forEach((dep) => { + depCounts[dep] = (depCounts[dep] || 0) + 1; + }); + + // Get most common dependencies for similar tasks + commonDeps = Object.entries(depCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + if (commonDeps.length > 0) { + contextTasks += '\nMost common dependencies for similar tasks:'; + commonDeps.forEach(([depId, count]) => { + const depTask = data.tasks.find((t) => t.id === parseInt(depId)); + if (depTask) { + contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`; + } + }); + } + } + + // Show fuzzy search analysis in CLI mode + if (outputFormat === 'text') { + console.log( + chalk.gray( + ` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords` + ) + ); + + if (highRelevance.length > 0) { + console.log( + chalk.gray(`\n High relevance matches (score < 0.25):`) + ); + highRelevance.slice(0, 25).forEach((t) => { + console.log( + chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + if (mediumRelevance.length > 0) { + console.log( + chalk.gray(`\n Medium relevance matches (score < 0.4):`) + ); + mediumRelevance.slice(0, 10).forEach((t) => { + console.log( + chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + // Show dependency patterns + if (commonDeps && commonDeps.length > 0) { + console.log( + chalk.gray(`\n Common dependency patterns for similar tasks:`) + ); + commonDeps.slice(0, 3).forEach(([depId, count]) => { + const depTask = data.tasks.find((t) => t.id === parseInt(depId)); + if (depTask) { + console.log( + chalk.blue( + ` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}` + ) + ); + } + }); + } + + // Add information about which tasks will be provided in detail + if (uniqueDetailedTasks.length > 0) { + console.log( + chalk.gray( + `\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:` + ) + ); + uniqueDetailedTasks.forEach((t) => { + const isHighRelevance = highRelevance.some( + (ht) => ht.id === t.id + ); + const relevanceIndicator = isHighRelevance ? '⭐ ' : ''; + console.log( + chalk.cyan( + ` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}` + ) + ); + }); + } + + console.log(); // Add spacing + } + } + + // DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT + let actualDetailedTasksCount = 0; + if (numericDependencies.length > 0) { + // In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks' + // Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate. + // For simplicity, let's assume 'dependentTasks' reflects the detailed tasks. + actualDetailedTasksCount = dependentTasks.length; + } else { + // In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct. + actualDetailedTasksCount = uniqueDetailedTasks + ? uniqueDetailedTasks.length + : 0; + } + + // Add a visual transition to show we're moving to AI generation - only for CLI + if (outputFormat === 'text') { + console.log( + boxen( + chalk.white.bold('AI Task Generation') + + `\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + + `\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + + `\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` + + `\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow( + numericDependencies.length > 0 + ? dependentTasks.length // Use length of tasks from explicit dependency path + : uniqueDetailedTasks.length // Use length of tasks from fuzzy search path + )}`, + { + padding: { top: 0, bottom: 1, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: 'white', + borderStyle: 'round' + } + ) + ); + console.log(); // Add spacing + } + + // System Prompt - Enhanced for dependency awareness + const systemPrompt = + "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + + 'When determining dependencies for a new task, follow these principles:\n' + + '1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' + + '2. Prioritize task dependencies that are semantically related to the functionality being built.\n' + + '3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' + + '4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' + + '5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' + + "6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" + + '7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' + + 'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n'; + + // Task Structure Description (for user prompt) + const taskStructureDesc = ` { "title": "Task title goes here", "description": "A concise one or two sentence description of what the task involves", @@ -852,22 +852,22 @@ async function addTask( } `; - // Add any manually provided details to the prompt for context - let contextFromArgs = ""; - if (manualTaskData?.title) - contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; - if (manualTaskData?.description) - contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; - if (manualTaskData?.details) - contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; - if (manualTaskData?.testStrategy) - contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; + // Add any manually provided details to the prompt for context + let contextFromArgs = ''; + if (manualTaskData?.title) + contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; + if (manualTaskData?.description) + contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; + if (manualTaskData?.details) + contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; + if (manualTaskData?.testStrategy) + contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; - // User Prompt - const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. + // User Prompt + const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. ${contextTasks} - ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ""} + ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on. @@ -877,297 +877,297 @@ async function addTask( Make sure the details and test strategy are comprehensive and specific. DO NOT include the task ID in the title. `; - // Start the loading indicator - only for text mode - if (outputFormat === "text") { - loadingIndicator = startLoadingIndicator( - `Generating new task with ${useResearch ? "Research" : "Main"} AI... \n` - ); - } + // Start the loading indicator - only for text mode + if (outputFormat === 'text') { + loadingIndicator = startLoadingIndicator( + `Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n` + ); + } - try { - const serviceRole = useResearch ? "research" : "main"; - report("DEBUG: Calling generateObjectService...", "debug"); + try { + const serviceRole = useResearch ? 'research' : 'main'; + report('DEBUG: Calling generateObjectService...', 'debug'); - aiServiceResponse = await generateObjectService({ - // Capture the full response - role: serviceRole, - session: session, - projectRoot: projectRoot, - schema: AiTaskDataSchema, - objectName: "newTaskData", - systemPrompt: systemPrompt, - prompt: userPrompt, - commandName: commandName || "add-task", // Use passed commandName or default - outputType: outputType || (isMCP ? "mcp" : "cli"), // Use passed outputType or derive - }); - report("DEBUG: generateObjectService returned successfully.", "debug"); + aiServiceResponse = await generateObjectService({ + // Capture the full response + role: serviceRole, + session: session, + projectRoot: projectRoot, + schema: AiTaskDataSchema, + objectName: 'newTaskData', + systemPrompt: systemPrompt, + prompt: userPrompt, + commandName: commandName || 'add-task', // Use passed commandName or default + outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive + }); + report('DEBUG: generateObjectService returned successfully.', 'debug'); - if (!aiServiceResponse || !aiServiceResponse.mainResult) { - throw new Error( - "AI service did not return the expected object structure." - ); - } + if (!aiServiceResponse || !aiServiceResponse.mainResult) { + throw new Error( + 'AI service did not return the expected object structure.' + ); + } - // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object - if ( - aiServiceResponse.mainResult.title && - aiServiceResponse.mainResult.description - ) { - taskData = aiServiceResponse.mainResult; - } else if ( - aiServiceResponse.mainResult.object && - aiServiceResponse.mainResult.object.title && - aiServiceResponse.mainResult.object.description - ) { - taskData = aiServiceResponse.mainResult.object; - } else { - throw new Error("AI service did not return a valid task object."); - } + // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object + if ( + aiServiceResponse.mainResult.title && + aiServiceResponse.mainResult.description + ) { + taskData = aiServiceResponse.mainResult; + } else if ( + aiServiceResponse.mainResult.object && + aiServiceResponse.mainResult.object.title && + aiServiceResponse.mainResult.object.description + ) { + taskData = aiServiceResponse.mainResult.object; + } else { + throw new Error('AI service did not return a valid task object.'); + } - report("Successfully generated task data from AI.", "success"); + report('Successfully generated task data from AI.', 'success'); - // Success! Show checkmark - if (loadingIndicator) { - succeedLoadingIndicator( - loadingIndicator, - "Task generated successfully" - ); - loadingIndicator = null; // Clear it - } - } catch (error) { - // Failure! Show X - if (loadingIndicator) { - failLoadingIndicator(loadingIndicator, "AI generation failed"); - loadingIndicator = null; - } - report( - `DEBUG: generateObjectService caught error: ${error.message}`, - "debug" - ); - report(`Error generating task with AI: ${error.message}`, "error"); - throw error; // Re-throw error after logging - } finally { - report("DEBUG: generateObjectService finally block reached.", "debug"); - // Clean up if somehow still running - if (loadingIndicator) { - stopLoadingIndicator(loadingIndicator); - } - } - // --- End Refactored AI Interaction --- - } + // Success! Show checkmark + if (loadingIndicator) { + succeedLoadingIndicator( + loadingIndicator, + 'Task generated successfully' + ); + loadingIndicator = null; // Clear it + } + } catch (error) { + // Failure! Show X + if (loadingIndicator) { + failLoadingIndicator(loadingIndicator, 'AI generation failed'); + loadingIndicator = null; + } + report( + `DEBUG: generateObjectService caught error: ${error.message}`, + 'debug' + ); + report(`Error generating task with AI: ${error.message}`, 'error'); + throw error; // Re-throw error after logging + } finally { + report('DEBUG: generateObjectService finally block reached.', 'debug'); + // Clean up if somehow still running + if (loadingIndicator) { + stopLoadingIndicator(loadingIndicator); + } + } + // --- End Refactored AI Interaction --- + } - // Create the new task object - const newTask = { - id: newTaskId, - title: taskData.title, - description: taskData.description, - details: taskData.details || "", - testStrategy: taskData.testStrategy || "", - status: "pending", - dependencies: taskData.dependencies?.length - ? taskData.dependencies - : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified - priority: effectivePriority, - subtasks: [], // Initialize with empty subtasks array - }; + // Create the new task object + const newTask = { + id: newTaskId, + title: taskData.title, + description: taskData.description, + details: taskData.details || '', + testStrategy: taskData.testStrategy || '', + status: 'pending', + dependencies: taskData.dependencies?.length + ? taskData.dependencies + : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified + priority: effectivePriority, + subtasks: [] // Initialize with empty subtasks array + }; - // Additional check: validate all dependencies in the AI response - if (taskData.dependencies?.length) { - const allValidDeps = taskData.dependencies.every((depId) => { - const numDepId = parseInt(depId, 10); - return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); - }); + // Additional check: validate all dependencies in the AI response + if (taskData.dependencies?.length) { + const allValidDeps = taskData.dependencies.every((depId) => { + const numDepId = parseInt(depId, 10); + return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); + }); - if (!allValidDeps) { - report( - "AI suggested invalid dependencies. Filtering them out...", - "warn" - ); - newTask.dependencies = taskData.dependencies.filter((depId) => { - const numDepId = parseInt(depId, 10); - return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); - }); - } - } + if (!allValidDeps) { + report( + 'AI suggested invalid dependencies. Filtering them out...', + 'warn' + ); + newTask.dependencies = taskData.dependencies.filter((depId) => { + const numDepId = parseInt(depId, 10); + return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); + }); + } + } - // Add the task to the tasks array - data.tasks.push(newTask); + // Add the task to the tasks array + data.tasks.push(newTask); - report("DEBUG: Writing tasks.json...", "debug"); - // Write the updated tasks to the file - writeJSON(tasksPath, data); - report("DEBUG: tasks.json written.", "debug"); + report('DEBUG: Writing tasks.json...', 'debug'); + // Write the updated tasks to the file + writeJSON(tasksPath, data); + report('DEBUG: tasks.json written.', 'debug'); - // Generate markdown task files - report("Generating task files...", "info"); - report("DEBUG: Calling generateTaskFiles...", "debug"); - // Pass mcpLog if available to generateTaskFiles - await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog }); - report("DEBUG: generateTaskFiles finished.", "debug"); + // Generate markdown task files + report('Generating task files...', 'info'); + report('DEBUG: Calling generateTaskFiles...', 'debug'); + // Pass mcpLog if available to generateTaskFiles + await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog }); + report('DEBUG: generateTaskFiles finished.', 'debug'); - // Show success message - only for text output (CLI) - if (outputFormat === "text") { - const table = new Table({ - head: [ - chalk.cyan.bold("ID"), - chalk.cyan.bold("Title"), - chalk.cyan.bold("Description"), - ], - colWidths: [5, 30, 50], // Adjust widths as needed - }); + // Show success message - only for text output (CLI) + if (outputFormat === 'text') { + const table = new Table({ + head: [ + chalk.cyan.bold('ID'), + chalk.cyan.bold('Title'), + chalk.cyan.bold('Description') + ], + colWidths: [5, 30, 50] // Adjust widths as needed + }); - table.push([ - newTask.id, - truncate(newTask.title, 27), - truncate(newTask.description, 47), - ]); + table.push([ + newTask.id, + truncate(newTask.title, 27), + truncate(newTask.description, 47) + ]); - console.log(chalk.green("✓ New task created successfully:")); - console.log(table.toString()); + console.log(chalk.green('✓ New task created successfully:')); + console.log(table.toString()); - // Helper to get priority color - const getPriorityColor = (p) => { - switch (p?.toLowerCase()) { - case "high": - return "red"; - case "low": - return "gray"; - case "medium": - default: - return "yellow"; - } - }; + // Helper to get priority color + const getPriorityColor = (p) => { + switch (p?.toLowerCase()) { + case 'high': + return 'red'; + case 'low': + return 'gray'; + case 'medium': + default: + return 'yellow'; + } + }; - // Check if AI added new dependencies that weren't explicitly provided - const aiAddedDeps = newTask.dependencies.filter( - (dep) => !numericDependencies.includes(dep) - ); + // Check if AI added new dependencies that weren't explicitly provided + const aiAddedDeps = newTask.dependencies.filter( + (dep) => !numericDependencies.includes(dep) + ); - // Check if AI removed any dependencies that were explicitly provided - const aiRemovedDeps = numericDependencies.filter( - (dep) => !newTask.dependencies.includes(dep) - ); + // Check if AI removed any dependencies that were explicitly provided + const aiRemovedDeps = numericDependencies.filter( + (dep) => !newTask.dependencies.includes(dep) + ); - // Get task titles for dependencies to display - const depTitles = {}; - newTask.dependencies.forEach((dep) => { - const depTask = data.tasks.find((t) => t.id === dep); - if (depTask) { - depTitles[dep] = truncate(depTask.title, 30); - } - }); + // Get task titles for dependencies to display + const depTitles = {}; + newTask.dependencies.forEach((dep) => { + const depTask = data.tasks.find((t) => t.id === dep); + if (depTask) { + depTitles[dep] = truncate(depTask.title, 30); + } + }); - // Prepare dependency display string - let dependencyDisplay = ""; - if (newTask.dependencies.length > 0) { - dependencyDisplay = chalk.white("Dependencies:") + "\n"; - newTask.dependencies.forEach((dep) => { - const isAiAdded = aiAddedDeps.includes(dep); - const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : ""; - dependencyDisplay += - chalk.white( - ` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}` - ) + "\n"; - }); - } else { - dependencyDisplay = chalk.white("Dependencies: None") + "\n"; - } + // Prepare dependency display string + let dependencyDisplay = ''; + if (newTask.dependencies.length > 0) { + dependencyDisplay = chalk.white('Dependencies:') + '\n'; + newTask.dependencies.forEach((dep) => { + const isAiAdded = aiAddedDeps.includes(dep); + const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; + dependencyDisplay += + chalk.white( + ` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}` + ) + '\n'; + }); + } else { + dependencyDisplay = chalk.white('Dependencies: None') + '\n'; + } - // Add info about removed dependencies if any - if (aiRemovedDeps.length > 0) { - dependencyDisplay += - chalk.gray("\nUser-specified dependencies that were not used:") + - "\n"; - aiRemovedDeps.forEach((dep) => { - const depTask = data.tasks.find((t) => t.id === dep); - const title = depTask ? truncate(depTask.title, 30) : "Unknown task"; - dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n"; - }); - } + // Add info about removed dependencies if any + if (aiRemovedDeps.length > 0) { + dependencyDisplay += + chalk.gray('\nUser-specified dependencies that were not used:') + + '\n'; + aiRemovedDeps.forEach((dep) => { + const depTask = data.tasks.find((t) => t.id === dep); + const title = depTask ? truncate(depTask.title, 30) : 'Unknown task'; + dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n'; + }); + } - // Add dependency analysis summary - let dependencyAnalysis = ""; - if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { - dependencyAnalysis = - "\n" + chalk.white.bold("Dependency Analysis:") + "\n"; - if (aiAddedDeps.length > 0) { - dependencyAnalysis += - chalk.green( - `AI identified ${aiAddedDeps.length} additional dependencies` - ) + "\n"; - } - if (aiRemovedDeps.length > 0) { - dependencyAnalysis += - chalk.yellow( - `AI excluded ${aiRemovedDeps.length} user-provided dependencies` - ) + "\n"; - } - } + // Add dependency analysis summary + let dependencyAnalysis = ''; + if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { + dependencyAnalysis = + '\n' + chalk.white.bold('Dependency Analysis:') + '\n'; + if (aiAddedDeps.length > 0) { + dependencyAnalysis += + chalk.green( + `AI identified ${aiAddedDeps.length} additional dependencies` + ) + '\n'; + } + if (aiRemovedDeps.length > 0) { + dependencyAnalysis += + chalk.yellow( + `AI excluded ${aiRemovedDeps.length} user-provided dependencies` + ) + '\n'; + } + } - // Show success message box - console.log( - boxen( - chalk.white.bold(`Task ${newTaskId} Created Successfully`) + - "\n\n" + - chalk.white(`Title: ${newTask.title}`) + - "\n" + - chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + - "\n" + - chalk.white( - `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` - ) + - "\n\n" + - dependencyDisplay + - dependencyAnalysis + - "\n" + - chalk.white.bold("Next Steps:") + - "\n" + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` - ) + - "\n" + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` - ) + - "\n" + - chalk.cyan( - `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` - ), - { padding: 1, borderColor: "green", borderStyle: "round" } - ) - ); + // Show success message box + console.log( + boxen( + chalk.white.bold(`Task ${newTaskId} Created Successfully`) + + '\n\n' + + chalk.white(`Title: ${newTask.title}`) + + '\n' + + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + + '\n' + + chalk.white( + `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` + ) + + '\n\n' + + dependencyDisplay + + dependencyAnalysis + + '\n' + + chalk.white.bold('Next Steps:') + + '\n' + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` + ) + + '\n' + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` + ) + + '\n' + + chalk.cyan( + `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` + ), + { padding: 1, borderColor: 'green', borderStyle: 'round' } + ) + ); - // Display AI Usage Summary if telemetryData is available - if ( - aiServiceResponse && - aiServiceResponse.telemetryData && - (outputType === "cli" || outputType === "text") - ) { - displayAiUsageSummary(aiServiceResponse.telemetryData, "cli"); - } - } + // Display AI Usage Summary if telemetryData is available + if ( + aiServiceResponse && + aiServiceResponse.telemetryData && + (outputType === 'cli' || outputType === 'text') + ) { + displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); + } + } - report( - `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, - "debug" - ); - return { - newTaskId: newTaskId, - telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null, - }; - } catch (error) { - // Stop any loading indicator on error - if (loadingIndicator) { - stopLoadingIndicator(loadingIndicator); - } + report( + `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, + 'debug' + ); + return { + newTaskId: newTaskId, + telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null + }; + } catch (error) { + // Stop any loading indicator on error + if (loadingIndicator) { + stopLoadingIndicator(loadingIndicator); + } - report(`Error adding task: ${error.message}`, "error"); - if (outputFormat === "text") { - console.error(chalk.red(`Error: ${error.message}`)); - } - // In MCP mode, we let the direct function handler catch and format - throw error; - } + report(`Error adding task: ${error.message}`, 'error'); + if (outputFormat === 'text') { + console.error(chalk.red(`Error: ${error.message}`)); + } + // In MCP mode, we let the direct function handler catch and format + throw error; + } } export default addTask; diff --git a/scripts/modules/task-manager/clear-subtasks.js b/scripts/modules/task-manager/clear-subtasks.js index f07dd897..e69f8442 100644 --- a/scripts/modules/task-manager/clear-subtasks.js +++ b/scripts/modules/task-manager/clear-subtasks.js @@ -1,11 +1,11 @@ -import path from "path"; -import chalk from "chalk"; -import boxen from "boxen"; -import Table from "cli-table3"; +import path from 'path'; +import chalk from 'chalk'; +import boxen from 'boxen'; +import Table from 'cli-table3'; -import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js"; -import { displayBanner } from "../ui.js"; -import generateTaskFiles from "./generate-task-files.js"; +import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js'; +import { displayBanner } from '../ui.js'; +import generateTaskFiles from './generate-task-files.js'; /** * Clear subtasks from specified tasks @@ -13,138 +13,138 @@ import generateTaskFiles from "./generate-task-files.js"; * @param {string} taskIds - Task IDs to clear subtasks from */ function clearSubtasks(tasksPath, taskIds) { - log("info", `Reading tasks from ${tasksPath}...`); - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log("error", "No valid tasks found."); - process.exit(1); - } + log('info', `Reading tasks from ${tasksPath}...`); + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log('error', 'No valid tasks found.'); + process.exit(1); + } - if (!isSilentMode()) { - console.log( - boxen(chalk.white.bold("Clearing Subtasks"), { - padding: 1, - borderColor: "blue", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - }) - ); - } + if (!isSilentMode()) { + console.log( + boxen(chalk.white.bold('Clearing Subtasks'), { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 1, bottom: 1 } + }) + ); + } - // Handle multiple task IDs (comma-separated) - const taskIdArray = taskIds.split(",").map((id) => id.trim()); - let clearedCount = 0; + // Handle multiple task IDs (comma-separated) + const taskIdArray = taskIds.split(',').map((id) => id.trim()); + let clearedCount = 0; - // Create a summary table for the cleared subtasks - const summaryTable = new Table({ - head: [ - chalk.cyan.bold("Task ID"), - chalk.cyan.bold("Task Title"), - chalk.cyan.bold("Subtasks Cleared"), - ], - colWidths: [10, 50, 20], - style: { head: [], border: [] }, - }); + // Create a summary table for the cleared subtasks + const summaryTable = new Table({ + head: [ + chalk.cyan.bold('Task ID'), + chalk.cyan.bold('Task Title'), + chalk.cyan.bold('Subtasks Cleared') + ], + colWidths: [10, 50, 20], + style: { head: [], border: [] } + }); - taskIdArray.forEach((taskId) => { - const id = parseInt(taskId, 10); - if (isNaN(id)) { - log("error", `Invalid task ID: ${taskId}`); - return; - } + taskIdArray.forEach((taskId) => { + const id = parseInt(taskId, 10); + if (isNaN(id)) { + log('error', `Invalid task ID: ${taskId}`); + return; + } - const task = data.tasks.find((t) => t.id === id); - if (!task) { - log("error", `Task ${id} not found`); - return; - } + const task = data.tasks.find((t) => t.id === id); + if (!task) { + log('error', `Task ${id} not found`); + return; + } - if (!task.subtasks || task.subtasks.length === 0) { - log("info", `Task ${id} has no subtasks to clear`); - summaryTable.push([ - id.toString(), - truncate(task.title, 47), - chalk.yellow("No subtasks"), - ]); - return; - } + if (!task.subtasks || task.subtasks.length === 0) { + log('info', `Task ${id} has no subtasks to clear`); + summaryTable.push([ + id.toString(), + truncate(task.title, 47), + chalk.yellow('No subtasks') + ]); + return; + } - const subtaskCount = task.subtasks.length; - task.subtasks = []; - clearedCount++; - log("info", `Cleared ${subtaskCount} subtasks from task ${id}`); + const subtaskCount = task.subtasks.length; + task.subtasks = []; + clearedCount++; + log('info', `Cleared ${subtaskCount} subtasks from task ${id}`); - summaryTable.push([ - id.toString(), - truncate(task.title, 47), - chalk.green(`${subtaskCount} subtasks cleared`), - ]); - }); + summaryTable.push([ + id.toString(), + truncate(task.title, 47), + chalk.green(`${subtaskCount} subtasks cleared`) + ]); + }); - if (clearedCount > 0) { - writeJSON(tasksPath, data); + if (clearedCount > 0) { + writeJSON(tasksPath, data); - // Show summary table - if (!isSilentMode()) { - console.log( - boxen(chalk.white.bold("Subtask Clearing Summary:"), { - padding: { left: 2, right: 2, top: 0, bottom: 0 }, - margin: { top: 1, bottom: 0 }, - borderColor: "blue", - borderStyle: "round", - }) - ); - console.log(summaryTable.toString()); - } + // Show summary table + if (!isSilentMode()) { + console.log( + boxen(chalk.white.bold('Subtask Clearing Summary:'), { + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 0 }, + borderColor: 'blue', + borderStyle: 'round' + }) + ); + console.log(summaryTable.toString()); + } - // Regenerate task files to reflect changes - log("info", "Regenerating task files..."); - generateTaskFiles(tasksPath, path.dirname(tasksPath)); + // Regenerate task files to reflect changes + log('info', 'Regenerating task files...'); + generateTaskFiles(tasksPath, path.dirname(tasksPath)); - // Success message - if (!isSilentMode()) { - console.log( - boxen( - chalk.green( - `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` - ), - { - padding: 1, - borderColor: "green", - borderStyle: "round", - margin: { top: 1 }, - } - ) - ); + // Success message + if (!isSilentMode()) { + console.log( + boxen( + chalk.green( + `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` + ), + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1 } + } + ) + ); - // Next steps suggestion - console.log( - boxen( - chalk.white.bold("Next Steps:") + - "\n\n" + - `${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=")} to generate new subtasks\n` + - `${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`, - { - padding: 1, - borderColor: "cyan", - borderStyle: "round", - margin: { top: 1 }, - } - ) - ); - } - } else { - if (!isSilentMode()) { - console.log( - boxen(chalk.yellow("No subtasks were cleared"), { - padding: 1, - borderColor: "yellow", - borderStyle: "round", - margin: { top: 1 }, - }) - ); - } - } + // Next steps suggestion + console.log( + boxen( + chalk.white.bold('Next Steps:') + + '\n\n' + + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=')} to generate new subtasks\n` + + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`, + { + padding: 1, + borderColor: 'cyan', + borderStyle: 'round', + margin: { top: 1 } + } + ) + ); + } + } else { + if (!isSilentMode()) { + console.log( + boxen(chalk.yellow('No subtasks were cleared'), { + padding: 1, + borderColor: 'yellow', + borderStyle: 'round', + margin: { top: 1 } + }) + ); + } + } } export default clearSubtasks; diff --git a/scripts/modules/task-manager/list-tasks.js b/scripts/modules/task-manager/list-tasks.js index d82768cd..51f37460 100644 --- a/scripts/modules/task-manager/list-tasks.js +++ b/scripts/modules/task-manager/list-tasks.js @@ -1,23 +1,23 @@ -import chalk from "chalk"; -import boxen from "boxen"; -import Table from "cli-table3"; +import chalk from 'chalk'; +import boxen from 'boxen'; +import Table from 'cli-table3'; import { - log, - readJSON, - truncate, - readComplexityReport, - addComplexityToTask, -} from "../utils.js"; -import findNextTask from "./find-next-task.js"; + log, + readJSON, + truncate, + readComplexityReport, + addComplexityToTask +} from '../utils.js'; +import findNextTask from './find-next-task.js'; import { - displayBanner, - getStatusWithColor, - formatDependenciesWithStatus, - getComplexityWithColor, - createProgressBar, -} from "../ui.js"; + displayBanner, + getStatusWithColor, + formatDependenciesWithStatus, + getComplexityWithColor, + createProgressBar +} from '../ui.js'; /** * List all tasks @@ -29,734 +29,734 @@ import { * @returns {Object} - Task list result for json format */ function listTasks( - tasksPath, - statusFilter, - reportPath = null, - withSubtasks = false, - outputFormat = "text" + tasksPath, + statusFilter, + reportPath = null, + withSubtasks = false, + outputFormat = 'text' ) { - try { - const data = readJSON(tasksPath); // Reads the whole tasks.json - if (!data || !data.tasks) { - throw new Error(`No valid tasks found in ${tasksPath}`); - } + try { + const data = readJSON(tasksPath); // Reads the whole tasks.json + if (!data || !data.tasks) { + throw new Error(`No valid tasks found in ${tasksPath}`); + } - // Add complexity scores to tasks if report exists - const complexityReport = readComplexityReport(reportPath); - // Apply complexity scores to tasks - if (complexityReport && complexityReport.complexityAnalysis) { - data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); - } + // Add complexity scores to tasks if report exists + const complexityReport = readComplexityReport(reportPath); + // Apply complexity scores to tasks + if (complexityReport && complexityReport.complexityAnalysis) { + data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); + } - // Filter tasks by status if specified - const filteredTasks = - statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all' - ? data.tasks.filter( - (task) => - task.status && - task.status.toLowerCase() === statusFilter.toLowerCase() - ) - : data.tasks; // Default to all tasks if no filter or filter is 'all' + // Filter tasks by status if specified + const filteredTasks = + statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all' + ? data.tasks.filter( + (task) => + task.status && + task.status.toLowerCase() === statusFilter.toLowerCase() + ) + : data.tasks; // Default to all tasks if no filter or filter is 'all' - // Calculate completion statistics - const totalTasks = data.tasks.length; - const completedTasks = data.tasks.filter( - (task) => task.status === "done" || task.status === "completed" - ).length; - const completionPercentage = - totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; + // Calculate completion statistics + const totalTasks = data.tasks.length; + const completedTasks = data.tasks.filter( + (task) => task.status === 'done' || task.status === 'completed' + ).length; + const completionPercentage = + totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; - // Count statuses for tasks - const doneCount = completedTasks; - const inProgressCount = data.tasks.filter( - (task) => task.status === "in-progress" - ).length; - const pendingCount = data.tasks.filter( - (task) => task.status === "pending" - ).length; - const blockedCount = data.tasks.filter( - (task) => task.status === "blocked" - ).length; - const deferredCount = data.tasks.filter( - (task) => task.status === "deferred" - ).length; - const cancelledCount = data.tasks.filter( - (task) => task.status === "cancelled" - ).length; + // Count statuses for tasks + const doneCount = completedTasks; + const inProgressCount = data.tasks.filter( + (task) => task.status === 'in-progress' + ).length; + const pendingCount = data.tasks.filter( + (task) => task.status === 'pending' + ).length; + const blockedCount = data.tasks.filter( + (task) => task.status === 'blocked' + ).length; + const deferredCount = data.tasks.filter( + (task) => task.status === 'deferred' + ).length; + const cancelledCount = data.tasks.filter( + (task) => task.status === 'cancelled' + ).length; - // Count subtasks and their statuses - let totalSubtasks = 0; - let completedSubtasks = 0; - let inProgressSubtasks = 0; - let pendingSubtasks = 0; - let blockedSubtasks = 0; - let deferredSubtasks = 0; - let cancelledSubtasks = 0; + // Count subtasks and their statuses + let totalSubtasks = 0; + let completedSubtasks = 0; + let inProgressSubtasks = 0; + let pendingSubtasks = 0; + let blockedSubtasks = 0; + let deferredSubtasks = 0; + let cancelledSubtasks = 0; - data.tasks.forEach((task) => { - if (task.subtasks && task.subtasks.length > 0) { - totalSubtasks += task.subtasks.length; - completedSubtasks += task.subtasks.filter( - (st) => st.status === "done" || st.status === "completed" - ).length; - inProgressSubtasks += task.subtasks.filter( - (st) => st.status === "in-progress" - ).length; - pendingSubtasks += task.subtasks.filter( - (st) => st.status === "pending" - ).length; - blockedSubtasks += task.subtasks.filter( - (st) => st.status === "blocked" - ).length; - deferredSubtasks += task.subtasks.filter( - (st) => st.status === "deferred" - ).length; - cancelledSubtasks += task.subtasks.filter( - (st) => st.status === "cancelled" - ).length; - } - }); + data.tasks.forEach((task) => { + if (task.subtasks && task.subtasks.length > 0) { + totalSubtasks += task.subtasks.length; + completedSubtasks += task.subtasks.filter( + (st) => st.status === 'done' || st.status === 'completed' + ).length; + inProgressSubtasks += task.subtasks.filter( + (st) => st.status === 'in-progress' + ).length; + pendingSubtasks += task.subtasks.filter( + (st) => st.status === 'pending' + ).length; + blockedSubtasks += task.subtasks.filter( + (st) => st.status === 'blocked' + ).length; + deferredSubtasks += task.subtasks.filter( + (st) => st.status === 'deferred' + ).length; + cancelledSubtasks += task.subtasks.filter( + (st) => st.status === 'cancelled' + ).length; + } + }); - const subtaskCompletionPercentage = - totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; + const subtaskCompletionPercentage = + totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; - // For JSON output, return structured data - if (outputFormat === "json") { - // *** Modification: Remove 'details' field for JSON output *** - const tasksWithoutDetails = filteredTasks.map((task) => { - // <-- USES filteredTasks! - // Omit 'details' from the parent task - const { details, ...taskRest } = task; + // For JSON output, return structured data + if (outputFormat === 'json') { + // *** Modification: Remove 'details' field for JSON output *** + const tasksWithoutDetails = filteredTasks.map((task) => { + // <-- USES filteredTasks! + // Omit 'details' from the parent task + const { details, ...taskRest } = task; - // If subtasks exist, omit 'details' from them too - if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) { - taskRest.subtasks = taskRest.subtasks.map((subtask) => { - const { details: subtaskDetails, ...subtaskRest } = subtask; - return subtaskRest; - }); - } - return taskRest; - }); - // *** End of Modification *** + // If subtasks exist, omit 'details' from them too + if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) { + taskRest.subtasks = taskRest.subtasks.map((subtask) => { + const { details: subtaskDetails, ...subtaskRest } = subtask; + return subtaskRest; + }); + } + return taskRest; + }); + // *** End of Modification *** - return { - tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED - filter: statusFilter || "all", // Return the actual filter used - stats: { - total: totalTasks, - completed: doneCount, - inProgress: inProgressCount, - pending: pendingCount, - blocked: blockedCount, - deferred: deferredCount, - cancelled: cancelledCount, - completionPercentage, - subtasks: { - total: totalSubtasks, - completed: completedSubtasks, - inProgress: inProgressSubtasks, - pending: pendingSubtasks, - blocked: blockedSubtasks, - deferred: deferredSubtasks, - cancelled: cancelledSubtasks, - completionPercentage: subtaskCompletionPercentage, - }, - }, - }; - } + return { + tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED + filter: statusFilter || 'all', // Return the actual filter used + stats: { + total: totalTasks, + completed: doneCount, + inProgress: inProgressCount, + pending: pendingCount, + blocked: blockedCount, + deferred: deferredCount, + cancelled: cancelledCount, + completionPercentage, + subtasks: { + total: totalSubtasks, + completed: completedSubtasks, + inProgress: inProgressSubtasks, + pending: pendingSubtasks, + blocked: blockedSubtasks, + deferred: deferredSubtasks, + cancelled: cancelledSubtasks, + completionPercentage: subtaskCompletionPercentage + } + } + }; + } - // ... existing code for text output ... + // ... existing code for text output ... - // Calculate status breakdowns as percentages of total - const taskStatusBreakdown = { - "in-progress": totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0, - pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0, - blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0, - deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0, - cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0, - }; + // Calculate status breakdowns as percentages of total + const taskStatusBreakdown = { + 'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0, + pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0, + blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0, + deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0, + cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0 + }; - const subtaskStatusBreakdown = { - "in-progress": - totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0, - pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0, - blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0, - deferred: - totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0, - cancelled: - totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0, - }; + const subtaskStatusBreakdown = { + 'in-progress': + totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0, + pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0, + blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0, + deferred: + totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0, + cancelled: + totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0 + }; - // Create progress bars with status breakdowns - const taskProgressBar = createProgressBar( - completionPercentage, - 30, - taskStatusBreakdown - ); - const subtaskProgressBar = createProgressBar( - subtaskCompletionPercentage, - 30, - subtaskStatusBreakdown - ); + // Create progress bars with status breakdowns + const taskProgressBar = createProgressBar( + completionPercentage, + 30, + taskStatusBreakdown + ); + const subtaskProgressBar = createProgressBar( + subtaskCompletionPercentage, + 30, + subtaskStatusBreakdown + ); - // Calculate dependency statistics - const completedTaskIds = new Set( - data.tasks - .filter((t) => t.status === "done" || t.status === "completed") - .map((t) => t.id) - ); + // Calculate dependency statistics + const completedTaskIds = new Set( + data.tasks + .filter((t) => t.status === 'done' || t.status === 'completed') + .map((t) => t.id) + ); - const tasksWithNoDeps = data.tasks.filter( - (t) => - t.status !== "done" && - t.status !== "completed" && - (!t.dependencies || t.dependencies.length === 0) - ).length; + const tasksWithNoDeps = data.tasks.filter( + (t) => + t.status !== 'done' && + t.status !== 'completed' && + (!t.dependencies || t.dependencies.length === 0) + ).length; - const tasksWithAllDepsSatisfied = data.tasks.filter( - (t) => - t.status !== "done" && - t.status !== "completed" && - t.dependencies && - t.dependencies.length > 0 && - t.dependencies.every((depId) => completedTaskIds.has(depId)) - ).length; + const tasksWithAllDepsSatisfied = data.tasks.filter( + (t) => + t.status !== 'done' && + t.status !== 'completed' && + t.dependencies && + t.dependencies.length > 0 && + t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; - const tasksWithUnsatisfiedDeps = data.tasks.filter( - (t) => - t.status !== "done" && - t.status !== "completed" && - t.dependencies && - t.dependencies.length > 0 && - !t.dependencies.every((depId) => completedTaskIds.has(depId)) - ).length; + const tasksWithUnsatisfiedDeps = data.tasks.filter( + (t) => + t.status !== 'done' && + t.status !== 'completed' && + t.dependencies && + t.dependencies.length > 0 && + !t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; - // Calculate total tasks ready to work on (no deps + satisfied deps) - const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied; + // Calculate total tasks ready to work on (no deps + satisfied deps) + const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied; - // Calculate most depended-on tasks - const dependencyCount = {}; - data.tasks.forEach((task) => { - if (task.dependencies && task.dependencies.length > 0) { - task.dependencies.forEach((depId) => { - dependencyCount[depId] = (dependencyCount[depId] || 0) + 1; - }); - } - }); + // Calculate most depended-on tasks + const dependencyCount = {}; + data.tasks.forEach((task) => { + if (task.dependencies && task.dependencies.length > 0) { + task.dependencies.forEach((depId) => { + dependencyCount[depId] = (dependencyCount[depId] || 0) + 1; + }); + } + }); - // Find the most depended-on task - let mostDependedOnTaskId = null; - let maxDependents = 0; + // Find the most depended-on task + let mostDependedOnTaskId = null; + let maxDependents = 0; - for (const [taskId, count] of Object.entries(dependencyCount)) { - if (count > maxDependents) { - maxDependents = count; - mostDependedOnTaskId = parseInt(taskId); - } - } + for (const [taskId, count] of Object.entries(dependencyCount)) { + if (count > maxDependents) { + maxDependents = count; + mostDependedOnTaskId = parseInt(taskId); + } + } - // Get the most depended-on task - const mostDependedOnTask = - mostDependedOnTaskId !== null - ? data.tasks.find((t) => t.id === mostDependedOnTaskId) - : null; + // Get the most depended-on task + const mostDependedOnTask = + mostDependedOnTaskId !== null + ? data.tasks.find((t) => t.id === mostDependedOnTaskId) + : null; - // Calculate average dependencies per task - const totalDependencies = data.tasks.reduce( - (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), - 0 - ); - const avgDependenciesPerTask = totalDependencies / data.tasks.length; + // Calculate average dependencies per task + const totalDependencies = data.tasks.reduce( + (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), + 0 + ); + const avgDependenciesPerTask = totalDependencies / data.tasks.length; - // Find next task to work on, passing the complexity report - const nextItem = findNextTask(data.tasks, complexityReport); + // Find next task to work on, passing the complexity report + const nextItem = findNextTask(data.tasks, complexityReport); - // Get terminal width - more reliable method - let terminalWidth; - try { - // Try to get the actual terminal columns - terminalWidth = process.stdout.columns; - } catch (e) { - // Fallback if columns cannot be determined - log("debug", "Could not determine terminal width, using default"); - } - // Ensure we have a reasonable default if detection fails - terminalWidth = terminalWidth || 80; + // Get terminal width - more reliable method + let terminalWidth; + try { + // Try to get the actual terminal columns + terminalWidth = process.stdout.columns; + } catch (e) { + // Fallback if columns cannot be determined + log('debug', 'Could not determine terminal width, using default'); + } + // Ensure we have a reasonable default if detection fails + terminalWidth = terminalWidth || 80; - // Ensure terminal width is at least a minimum value to prevent layout issues - terminalWidth = Math.max(terminalWidth, 80); + // Ensure terminal width is at least a minimum value to prevent layout issues + terminalWidth = Math.max(terminalWidth, 80); - // Create dashboard content - const projectDashboardContent = - chalk.white.bold("Project Dashboard") + - "\n" + - `Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` + - `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` + - `Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` + - `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` + - chalk.cyan.bold("Priority Breakdown:") + - "\n" + - `${chalk.red("•")} ${chalk.white("High priority:")} ${data.tasks.filter((t) => t.priority === "high").length}\n` + - `${chalk.yellow("•")} ${chalk.white("Medium priority:")} ${data.tasks.filter((t) => t.priority === "medium").length}\n` + - `${chalk.green("•")} ${chalk.white("Low priority:")} ${data.tasks.filter((t) => t.priority === "low").length}`; + // Create dashboard content + const projectDashboardContent = + chalk.white.bold('Project Dashboard') + + '\n' + + `Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` + + `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` + + `Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` + + `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` + + chalk.cyan.bold('Priority Breakdown:') + + '\n' + + `${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` + + `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` + + `${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`; - const dependencyDashboardContent = - chalk.white.bold("Dependency Status & Next Task") + - "\n" + - chalk.cyan.bold("Dependency Metrics:") + - "\n" + - `${chalk.green("•")} ${chalk.white("Tasks with no dependencies:")} ${tasksWithNoDeps}\n` + - `${chalk.green("•")} ${chalk.white("Tasks ready to work on:")} ${tasksReadyToWork}\n` + - `${chalk.yellow("•")} ${chalk.white("Tasks blocked by dependencies:")} ${tasksWithUnsatisfiedDeps}\n` + - `${chalk.magenta("•")} ${chalk.white("Most depended-on task:")} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray("None")}\n` + - `${chalk.blue("•")} ${chalk.white("Avg dependencies per task:")} ${avgDependenciesPerTask.toFixed(1)}\n\n` + - chalk.cyan.bold("Next Task to Work On:") + - "\n" + - `ID: ${chalk.cyan(nextItem ? nextItem.id : "N/A")} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow("No task available")} + const dependencyDashboardContent = + chalk.white.bold('Dependency Status & Next Task') + + '\n' + + chalk.cyan.bold('Dependency Metrics:') + + '\n' + + `${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` + + `${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` + + `${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` + + `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` + + `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` + + chalk.cyan.bold('Next Task to Work On:') + + '\n' + + `ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')} ` + - `Priority: ${nextItem ? chalk.white(nextItem.priority || "medium") : ""} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ""} + `Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''} ` + - `Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray("N/A")}`; + `Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`; - // Calculate width for side-by-side display - // Box borders, padding take approximately 4 chars on each side - const minDashboardWidth = 50; // Minimum width for dashboard - const minDependencyWidth = 50; // Minimum width for dependency dashboard - const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing + // Calculate width for side-by-side display + // Box borders, padding take approximately 4 chars on each side + const minDashboardWidth = 50; // Minimum width for dashboard + const minDependencyWidth = 50; // Minimum width for dependency dashboard + const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing - // If terminal is wide enough, show boxes side by side with responsive widths - if (terminalWidth >= totalMinWidth) { - // Calculate widths proportionally for each box - use exact 50% width each - const availableWidth = terminalWidth; - const halfWidth = Math.floor(availableWidth / 2); + // If terminal is wide enough, show boxes side by side with responsive widths + if (terminalWidth >= totalMinWidth) { + // Calculate widths proportionally for each box - use exact 50% width each + const availableWidth = terminalWidth; + const halfWidth = Math.floor(availableWidth / 2); - // Account for border characters (2 chars on each side) - const boxContentWidth = halfWidth - 4; + // Account for border characters (2 chars on each side) + const boxContentWidth = halfWidth - 4; - // Create boxen options with precise widths - const dashboardBox = boxen(projectDashboardContent, { - padding: 1, - borderColor: "blue", - borderStyle: "round", - width: boxContentWidth, - dimBorder: false, - }); + // Create boxen options with precise widths + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); - const dependencyBox = boxen(dependencyDashboardContent, { - padding: 1, - borderColor: "magenta", - borderStyle: "round", - width: boxContentWidth, - dimBorder: false, - }); + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); - // Create a better side-by-side layout with exact spacing - const dashboardLines = dashboardBox.split("\n"); - const dependencyLines = dependencyBox.split("\n"); + // Create a better side-by-side layout with exact spacing + const dashboardLines = dashboardBox.split('\n'); + const dependencyLines = dependencyBox.split('\n'); - // Make sure both boxes have the same height - const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); + // Make sure both boxes have the same height + const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); - // For each line of output, pad the dashboard line to exactly halfWidth chars - // This ensures the dependency box starts at exactly the right position - const combinedLines = []; - for (let i = 0; i < maxHeight; i++) { - // Get the dashboard line (or empty string if we've run out of lines) - const dashLine = i < dashboardLines.length ? dashboardLines[i] : ""; - // Get the dependency line (or empty string if we've run out of lines) - const depLine = i < dependencyLines.length ? dependencyLines[i] : ""; + // For each line of output, pad the dashboard line to exactly halfWidth chars + // This ensures the dependency box starts at exactly the right position + const combinedLines = []; + for (let i = 0; i < maxHeight; i++) { + // Get the dashboard line (or empty string if we've run out of lines) + const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; + // Get the dependency line (or empty string if we've run out of lines) + const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; - // Remove any trailing spaces from dashLine before padding to exact width - const trimmedDashLine = dashLine.trimEnd(); - // Pad the dashboard line to exactly halfWidth chars with no extra spaces - const paddedDashLine = trimmedDashLine.padEnd(halfWidth, " "); + // Remove any trailing spaces from dashLine before padding to exact width + const trimmedDashLine = dashLine.trimEnd(); + // Pad the dashboard line to exactly halfWidth chars with no extra spaces + const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' '); - // Join the lines with no space in between - combinedLines.push(paddedDashLine + depLine); - } + // Join the lines with no space in between + combinedLines.push(paddedDashLine + depLine); + } - // Join all lines and output - console.log(combinedLines.join("\n")); - } else { - // Terminal too narrow, show boxes stacked vertically - const dashboardBox = boxen(projectDashboardContent, { - padding: 1, - borderColor: "blue", - borderStyle: "round", - margin: { top: 0, bottom: 1 }, - }); + // Join all lines and output + console.log(combinedLines.join('\n')); + } else { + // Terminal too narrow, show boxes stacked vertically + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); - const dependencyBox = boxen(dependencyDashboardContent, { - padding: 1, - borderColor: "magenta", - borderStyle: "round", - margin: { top: 0, bottom: 1 }, - }); + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); - // Display stacked vertically - console.log(dashboardBox); - console.log(dependencyBox); - } + // Display stacked vertically + console.log(dashboardBox); + console.log(dependencyBox); + } - if (filteredTasks.length === 0) { - console.log( - boxen( - statusFilter - ? chalk.yellow(`No tasks with status '${statusFilter}' found`) - : chalk.yellow("No tasks found"), - { padding: 1, borderColor: "yellow", borderStyle: "round" } - ) - ); - return; - } + if (filteredTasks.length === 0) { + console.log( + boxen( + statusFilter + ? chalk.yellow(`No tasks with status '${statusFilter}' found`) + : chalk.yellow('No tasks found'), + { padding: 1, borderColor: 'yellow', borderStyle: 'round' } + ) + ); + return; + } - // COMPLETELY REVISED TABLE APPROACH - // Define percentage-based column widths and calculate actual widths - // Adjust percentages based on content type and user requirements + // COMPLETELY REVISED TABLE APPROACH + // Define percentage-based column widths and calculate actual widths + // Adjust percentages based on content type and user requirements - // Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2") - const idWidthPct = withSubtasks ? 10 : 7; + // Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2") + const idWidthPct = withSubtasks ? 10 : 7; - // Calculate max status length to accommodate "in-progress" - const statusWidthPct = 15; + // Calculate max status length to accommodate "in-progress" + const statusWidthPct = 15; - // Increase priority column width as requested - const priorityWidthPct = 12; + // Increase priority column width as requested + const priorityWidthPct = 12; - // Make dependencies column smaller as requested (-20%) - const depsWidthPct = 20; + // Make dependencies column smaller as requested (-20%) + const depsWidthPct = 20; - const complexityWidthPct = 10; + const complexityWidthPct = 10; - // Calculate title/description width as remaining space (+20% from dependencies reduction) - const titleWidthPct = - 100 - - idWidthPct - - statusWidthPct - - priorityWidthPct - - depsWidthPct - - complexityWidthPct; + // Calculate title/description width as remaining space (+20% from dependencies reduction) + const titleWidthPct = + 100 - + idWidthPct - + statusWidthPct - + priorityWidthPct - + depsWidthPct - + complexityWidthPct; - // Allow 10 characters for borders and padding - const availableWidth = terminalWidth - 10; + // Allow 10 characters for borders and padding + const availableWidth = terminalWidth - 10; - // Calculate actual column widths based on percentages - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const complexityWidth = Math.floor( - availableWidth * (complexityWidthPct / 100) - ); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + // Calculate actual column widths based on percentages + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const complexityWidth = Math.floor( + availableWidth * (complexityWidthPct / 100) + ); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - // Create a table with correct borders and spacing - const table = new Table({ - head: [ - chalk.cyan.bold("ID"), - chalk.cyan.bold("Title"), - chalk.cyan.bold("Status"), - chalk.cyan.bold("Priority"), - chalk.cyan.bold("Dependencies"), - chalk.cyan.bold("Complexity"), - ], - colWidths: [ - idWidth, - titleWidth, - statusWidth, - priorityWidth, - depsWidth, - complexityWidth, // Added complexity column width - ], - style: { - head: [], // No special styling for header - border: [], // No special styling for border - compact: false, // Use default spacing - }, - wordWrap: true, - wrapOnWordBoundary: true, - }); + // Create a table with correct borders and spacing + const table = new Table({ + head: [ + chalk.cyan.bold('ID'), + chalk.cyan.bold('Title'), + chalk.cyan.bold('Status'), + chalk.cyan.bold('Priority'), + chalk.cyan.bold('Dependencies'), + chalk.cyan.bold('Complexity') + ], + colWidths: [ + idWidth, + titleWidth, + statusWidth, + priorityWidth, + depsWidth, + complexityWidth // Added complexity column width + ], + style: { + head: [], // No special styling for header + border: [], // No special styling for border + compact: false // Use default spacing + }, + wordWrap: true, + wrapOnWordBoundary: true + }); - // Process tasks for the table - filteredTasks.forEach((task) => { - // Format dependencies with status indicators (colored) - let depText = "None"; - if (task.dependencies && task.dependencies.length > 0) { - // Use the proper formatDependenciesWithStatus function for colored status - depText = formatDependenciesWithStatus( - task.dependencies, - data.tasks, - true, - complexityReport - ); - } else { - depText = chalk.gray("None"); - } + // Process tasks for the table + filteredTasks.forEach((task) => { + // Format dependencies with status indicators (colored) + let depText = 'None'; + if (task.dependencies && task.dependencies.length > 0) { + // Use the proper formatDependenciesWithStatus function for colored status + depText = formatDependenciesWithStatus( + task.dependencies, + data.tasks, + true, + complexityReport + ); + } else { + depText = chalk.gray('None'); + } - // Clean up any ANSI codes or confusing characters - const cleanTitle = task.title.replace(/\n/g, " "); + // Clean up any ANSI codes or confusing characters + const cleanTitle = task.title.replace(/\n/g, ' '); - // Get priority color - const priorityColor = - { - high: chalk.red, - medium: chalk.yellow, - low: chalk.gray, - }[task.priority || "medium"] || chalk.white; + // Get priority color + const priorityColor = + { + high: chalk.red, + medium: chalk.yellow, + low: chalk.gray + }[task.priority || 'medium'] || chalk.white; - // Format status - const status = getStatusWithColor(task.status, true); + // Format status + const status = getStatusWithColor(task.status, true); - // Add the row without truncating dependencies - table.push([ - task.id.toString(), - truncate(cleanTitle, titleWidth - 3), - status, - priorityColor(truncate(task.priority || "medium", priorityWidth - 2)), - depText, - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray("N/A"), - ]); + // Add the row without truncating dependencies + table.push([ + task.id.toString(), + truncate(cleanTitle, titleWidth - 3), + status, + priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)), + depText, + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray('N/A') + ]); - // Add subtasks if requested - if (withSubtasks && task.subtasks && task.subtasks.length > 0) { - task.subtasks.forEach((subtask) => { - // Format subtask dependencies with status indicators - let subtaskDepText = "None"; - if (subtask.dependencies && subtask.dependencies.length > 0) { - // Handle both subtask-to-subtask and subtask-to-task dependencies - const formattedDeps = subtask.dependencies - .map((depId) => { - // Check if it's a dependency on another subtask - if (typeof depId === "number" && depId < 100) { - const foundSubtask = task.subtasks.find( - (st) => st.id === depId - ); - if (foundSubtask) { - const isDone = - foundSubtask.status === "done" || - foundSubtask.status === "completed"; - const isInProgress = foundSubtask.status === "in-progress"; + // Add subtasks if requested + if (withSubtasks && task.subtasks && task.subtasks.length > 0) { + task.subtasks.forEach((subtask) => { + // Format subtask dependencies with status indicators + let subtaskDepText = 'None'; + if (subtask.dependencies && subtask.dependencies.length > 0) { + // Handle both subtask-to-subtask and subtask-to-task dependencies + const formattedDeps = subtask.dependencies + .map((depId) => { + // Check if it's a dependency on another subtask + if (typeof depId === 'number' && depId < 100) { + const foundSubtask = task.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(`${task.id}.${depId}`); - } else if (isInProgress) { - return chalk.hex("#FFA500").bold(`${task.id}.${depId}`); - } else { - return chalk.red.bold(`${task.id}.${depId}`); - } - } - } - // Default to regular task dependency - const depTask = data.tasks.find((t) => t.id === depId); - if (depTask) { - // Add complexity to depTask before checking status - addComplexityToTask(depTask, complexityReport); - const isDone = - depTask.status === "done" || depTask.status === "completed"; - const isInProgress = depTask.status === "in-progress"; - // Use the same color scheme as in formatDependenciesWithStatus - if (isDone) { - return chalk.green.bold(`${depId}`); - } else if (isInProgress) { - return chalk.hex("#FFA500").bold(`${depId}`); - } else { - return chalk.red.bold(`${depId}`); - } - } - return chalk.cyan(depId.toString()); - }) - .join(", "); + // Use consistent color formatting instead of emojis + if (isDone) { + return chalk.green.bold(`${task.id}.${depId}`); + } else if (isInProgress) { + return chalk.hex('#FFA500').bold(`${task.id}.${depId}`); + } else { + return chalk.red.bold(`${task.id}.${depId}`); + } + } + } + // Default to regular task dependency + const depTask = data.tasks.find((t) => t.id === depId); + if (depTask) { + // Add complexity to depTask before checking status + addComplexityToTask(depTask, complexityReport); + const isDone = + depTask.status === 'done' || depTask.status === 'completed'; + const isInProgress = depTask.status === 'in-progress'; + // Use the same color scheme as in formatDependenciesWithStatus + if (isDone) { + return chalk.green.bold(`${depId}`); + } else if (isInProgress) { + return chalk.hex('#FFA500').bold(`${depId}`); + } else { + return chalk.red.bold(`${depId}`); + } + } + return chalk.cyan(depId.toString()); + }) + .join(', '); - subtaskDepText = formattedDeps || chalk.gray("None"); - } + subtaskDepText = formattedDeps || chalk.gray('None'); + } - // Add the subtask row without truncating dependencies - table.push([ - `${task.id}.${subtask.id}`, - chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), - getStatusWithColor(subtask.status, true), - chalk.dim("-"), - subtaskDepText, - subtask.complexityScore - ? chalk.gray(`${subtask.complexityScore}`) - : chalk.gray("N/A"), - ]); - }); - } - }); + // Add the subtask row without truncating dependencies + table.push([ + `${task.id}.${subtask.id}`, + chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), + getStatusWithColor(subtask.status, true), + chalk.dim('-'), + subtaskDepText, + subtask.complexityScore + ? chalk.gray(`${subtask.complexityScore}`) + : chalk.gray('N/A') + ]); + }); + } + }); - // Ensure we output the table even if it had to wrap - try { - console.log(table.toString()); - } catch (err) { - log("error", `Error rendering table: ${err.message}`); + // Ensure we output the table even if it had to wrap + try { + console.log(table.toString()); + } catch (err) { + log('error', `Error rendering table: ${err.message}`); - // Fall back to simpler output - console.log( - chalk.yellow( - "\nFalling back to simple task list due to terminal width constraints:" - ) - ); - filteredTasks.forEach((task) => { - console.log( - `${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}` - ); - }); - } + // Fall back to simpler output + console.log( + chalk.yellow( + '\nFalling back to simple task list due to terminal width constraints:' + ) + ); + filteredTasks.forEach((task) => { + console.log( + `${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}` + ); + }); + } - // Show filter info if applied - if (statusFilter) { - console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`)); - console.log( - chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`) - ); - } + // Show filter info if applied + if (statusFilter) { + console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`)); + console.log( + chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`) + ); + } - // Define priority colors - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray, - }; + // Define priority colors + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + low: chalk.gray + }; - // Show next task box in a prominent color - if (nextItem) { - // Prepare subtasks section if they exist (Only tasks have .subtasks property) - let subtasksSection = ""; - // Check if the nextItem is a top-level task before looking for subtasks - const parentTaskForSubtasks = data.tasks.find( - (t) => String(t.id) === String(nextItem.id) - ); // Find the original task object - if ( - parentTaskForSubtasks && - parentTaskForSubtasks.subtasks && - parentTaskForSubtasks.subtasks.length > 0 - ) { - subtasksSection = `\n\n${chalk.white.bold("Subtasks:")}\n`; - subtasksSection += parentTaskForSubtasks.subtasks - .map((subtask) => { - // Add complexity to subtask before display - addComplexityToTask(subtask, complexityReport); - // Using a more simplified format for subtask status display - const status = subtask.status || "pending"; - const statusColors = { - done: chalk.green, - completed: chalk.green, - pending: chalk.yellow, - "in-progress": chalk.blue, - deferred: chalk.gray, - blocked: chalk.red, - cancelled: chalk.gray, - }; - const statusColor = - statusColors[status.toLowerCase()] || chalk.white; - // Ensure subtask ID is displayed correctly using parent ID from the original task object - return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`; - }) - .join("\n"); - } + // Show next task box in a prominent color + if (nextItem) { + // Prepare subtasks section if they exist (Only tasks have .subtasks property) + let subtasksSection = ''; + // Check if the nextItem is a top-level task before looking for subtasks + const parentTaskForSubtasks = data.tasks.find( + (t) => String(t.id) === String(nextItem.id) + ); // Find the original task object + if ( + parentTaskForSubtasks && + parentTaskForSubtasks.subtasks && + parentTaskForSubtasks.subtasks.length > 0 + ) { + subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`; + subtasksSection += parentTaskForSubtasks.subtasks + .map((subtask) => { + // Add complexity to subtask before display + addComplexityToTask(subtask, complexityReport); + // Using a more simplified format for subtask status display + const status = subtask.status || 'pending'; + const statusColors = { + done: chalk.green, + completed: chalk.green, + pending: chalk.yellow, + 'in-progress': chalk.blue, + deferred: chalk.gray, + blocked: chalk.red, + cancelled: chalk.gray + }; + const statusColor = + statusColors[status.toLowerCase()] || chalk.white; + // Ensure subtask ID is displayed correctly using parent ID from the original task object + return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`; + }) + .join('\n'); + } - console.log( - boxen( - chalk.hex("#FF8800").bold( - // Use nextItem.id and nextItem.title - `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` - ) + - "\n\n" + - // Use nextItem.priority, nextItem.status, nextItem.dependencies - `${chalk.white("Priority:")} ${priorityColors[nextItem.priority || "medium"](nextItem.priority || "medium")} ${chalk.white("Status:")} ${getStatusWithColor(nextItem.status, true)}\n` + - `${chalk.white("Dependencies:")} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray("None")}\n\n` + - // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) - // *** Fetching original item for description and details *** - `${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` + - subtasksSection + // <-- Subtasks are handled above now - "\n\n" + - // Use nextItem.id - `${chalk.cyan("Start working:")} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` + - // Use nextItem.id - `${chalk.cyan("View details:")} ${chalk.yellow(`task-master show ${nextItem.id}`)}`, - { - padding: { left: 2, right: 2, top: 1, bottom: 1 }, - borderColor: "#FF8800", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - title: "⚡ RECOMMENDED NEXT TASK ⚡", - titleAlignment: "center", - width: terminalWidth - 4, - fullscreen: false, - } - ) - ); - } else { - console.log( - boxen( - chalk.hex("#FF8800").bold("No eligible next task found") + - "\n\n" + - "All pending tasks have dependencies that are not yet completed, or all tasks are done.", - { - padding: 1, - borderColor: "#FF8800", - borderStyle: "round", - margin: { top: 1, bottom: 1 }, - title: "⚡ NEXT TASK ⚡", - titleAlignment: "center", - width: terminalWidth - 4, // Use full terminal width minus a small margin - } - ) - ); - } + console.log( + boxen( + chalk.hex('#FF8800').bold( + // Use nextItem.id and nextItem.title + `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` + ) + + '\n\n' + + // Use nextItem.priority, nextItem.status, nextItem.dependencies + `${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` + + `${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` + + // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) + // *** Fetching original item for description and details *** + `${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` + + subtasksSection + // <-- Subtasks are handled above now + '\n\n' + + // Use nextItem.id + `${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` + + // Use nextItem.id + `${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`, + { + padding: { left: 2, right: 2, top: 1, bottom: 1 }, + borderColor: '#FF8800', + borderStyle: 'round', + margin: { top: 1, bottom: 1 }, + title: '⚡ RECOMMENDED NEXT TASK ⚡', + titleAlignment: 'center', + width: terminalWidth - 4, + fullscreen: false + } + ) + ); + } else { + console.log( + boxen( + chalk.hex('#FF8800').bold('No eligible next task found') + + '\n\n' + + 'All pending tasks have dependencies that are not yet completed, or all tasks are done.', + { + padding: 1, + borderColor: '#FF8800', + borderStyle: 'round', + margin: { top: 1, bottom: 1 }, + title: '⚡ NEXT TASK ⚡', + titleAlignment: 'center', + width: terminalWidth - 4 // Use full terminal width minus a small margin + } + ) + ); + } - // Show next steps - console.log( - boxen( - chalk.white.bold("Suggested Next Steps:") + - "\n\n" + - `${chalk.cyan("1.")} Run ${chalk.yellow("task-master next")} to see what to work on next\n` + - `${chalk.cyan("2.")} Run ${chalk.yellow("task-master expand --id=")} to break down a task into subtasks\n` + - `${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id= --status=done")} to mark a task as complete`, - { - padding: 1, - borderColor: "gray", - borderStyle: "round", - margin: { top: 1 }, - } - ) - ); - } catch (error) { - log("error", `Error listing tasks: ${error.message}`); + // Show next steps + console.log( + boxen( + chalk.white.bold('Suggested Next Steps:') + + '\n\n' + + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` + + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=')} to break down a task into subtasks\n` + + `${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id= --status=done')} to mark a task as complete`, + { + padding: 1, + borderColor: 'gray', + borderStyle: 'round', + margin: { top: 1 } + } + ) + ); + } catch (error) { + log('error', `Error listing tasks: ${error.message}`); - if (outputFormat === "json") { - // Return structured error for JSON output - throw { - code: "TASK_LIST_ERROR", - message: error.message, - details: error.stack, - }; - } + if (outputFormat === 'json') { + // Return structured error for JSON output + throw { + code: 'TASK_LIST_ERROR', + message: error.message, + details: error.stack + }; + } - console.error(chalk.red(`Error: ${error.message}`)); - process.exit(1); - } + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } } // *** Helper function to get description for task or subtask *** function getWorkItemDescription(item, allTasks) { - if (!item) return "N/A"; - if (item.parentId) { - // It's a subtask - const parent = allTasks.find((t) => t.id === item.parentId); - const subtask = parent?.subtasks?.find( - (st) => `${parent.id}.${st.id}` === item.id - ); - return subtask?.description || "No description available."; - } else { - // It's a top-level task - const task = allTasks.find((t) => String(t.id) === String(item.id)); - return task?.description || "No description available."; - } + if (!item) return 'N/A'; + if (item.parentId) { + // It's a subtask + const parent = allTasks.find((t) => t.id === item.parentId); + const subtask = parent?.subtasks?.find( + (st) => `${parent.id}.${st.id}` === item.id + ); + return subtask?.description || 'No description available.'; + } else { + // It's a top-level task + const task = allTasks.find((t) => String(t.id) === String(item.id)); + return task?.description || 'No description available.'; + } } export default listTasks; diff --git a/scripts/modules/task-manager/models.js b/scripts/modules/task-manager/models.js index 5fe02d8a..b5b18538 100644 --- a/scripts/modules/task-manager/models.js +++ b/scripts/modules/task-manager/models.js @@ -3,71 +3,71 @@ * Core functionality for managing AI model configurations */ -import https from "https"; -import http from "http"; +import https from 'https'; +import http from 'http'; import { - getMainModelId, - getResearchModelId, - getFallbackModelId, - getAvailableModels, - getMainProvider, - getResearchProvider, - getFallbackProvider, - isApiKeySet, - getMcpApiKeyStatus, - getConfig, - writeConfig, - isConfigFilePresent, - getAllProviders, - getBaseUrlForRole, -} from "../config-manager.js"; -import { findConfigPath } from "../../../src/utils/path-utils.js"; -import { log } from "../utils.js"; + getMainModelId, + getResearchModelId, + getFallbackModelId, + getAvailableModels, + getMainProvider, + getResearchProvider, + getFallbackProvider, + isApiKeySet, + getMcpApiKeyStatus, + getConfig, + writeConfig, + isConfigFilePresent, + getAllProviders, + getBaseUrlForRole +} from '../config-manager.js'; +import { findConfigPath } from '../../../src/utils/path-utils.js'; +import { log } from '../utils.js'; /** * Fetches the list of models from OpenRouter API. * @returns {Promise} A promise that resolves with the list of model IDs or null if fetch fails. */ function fetchOpenRouterModels() { - return new Promise((resolve) => { - const options = { - hostname: "openrouter.ai", - path: "/api/v1/models", - method: "GET", - headers: { - Accept: "application/json", - }, - }; + return new Promise((resolve) => { + const options = { + hostname: 'openrouter.ai', + path: '/api/v1/models', + method: 'GET', + headers: { + Accept: 'application/json' + } + }; - const req = https.request(options, (res) => { - let data = ""; - res.on("data", (chunk) => { - data += chunk; - }); - res.on("end", () => { - if (res.statusCode === 200) { - try { - const parsedData = JSON.parse(data); - resolve(parsedData.data || []); // Return the array of models - } catch (e) { - console.error("Error parsing OpenRouter response:", e); - resolve(null); // Indicate failure - } - } else { - console.error( - `OpenRouter API request failed with status code: ${res.statusCode}` - ); - resolve(null); // Indicate failure - } - }); - }); + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + if (res.statusCode === 200) { + try { + const parsedData = JSON.parse(data); + resolve(parsedData.data || []); // Return the array of models + } catch (e) { + console.error('Error parsing OpenRouter response:', e); + resolve(null); // Indicate failure + } + } else { + console.error( + `OpenRouter API request failed with status code: ${res.statusCode}` + ); + resolve(null); // Indicate failure + } + }); + }); - req.on("error", (e) => { - console.error("Error fetching OpenRouter models:", e); - resolve(null); // Indicate failure - }); - req.end(); - }); + req.on('error', (e) => { + console.error('Error fetching OpenRouter models:', e); + resolve(null); // Indicate failure + }); + req.end(); + }); } /** @@ -75,61 +75,61 @@ function fetchOpenRouterModels() { * @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api") * @returns {Promise} A promise that resolves with the list of model objects or null if fetch fails. */ -function fetchOllamaModels(baseURL = "http://localhost:11434/api") { - return new Promise((resolve) => { - try { - // Parse the base URL to extract hostname, port, and base path - const url = new URL(baseURL); - const isHttps = url.protocol === "https:"; - const port = url.port || (isHttps ? 443 : 80); - const basePath = url.pathname.endsWith("/") - ? url.pathname.slice(0, -1) - : url.pathname; +function fetchOllamaModels(baseURL = 'http://localhost:11434/api') { + return new Promise((resolve) => { + try { + // Parse the base URL to extract hostname, port, and base path + const url = new URL(baseURL); + const isHttps = url.protocol === 'https:'; + const port = url.port || (isHttps ? 443 : 80); + const basePath = url.pathname.endsWith('/') + ? url.pathname.slice(0, -1) + : url.pathname; - const options = { - hostname: url.hostname, - port: parseInt(port, 10), - path: `${basePath}/tags`, - method: "GET", - headers: { - Accept: "application/json", - }, - }; + const options = { + hostname: url.hostname, + port: parseInt(port, 10), + path: `${basePath}/tags`, + method: 'GET', + headers: { + Accept: 'application/json' + } + }; - const requestLib = isHttps ? https : http; - const req = requestLib.request(options, (res) => { - let data = ""; - res.on("data", (chunk) => { - data += chunk; - }); - res.on("end", () => { - if (res.statusCode === 200) { - try { - const parsedData = JSON.parse(data); - resolve(parsedData.models || []); // Return the array of models - } catch (e) { - console.error("Error parsing Ollama response:", e); - resolve(null); // Indicate failure - } - } else { - console.error( - `Ollama API request failed with status code: ${res.statusCode}` - ); - resolve(null); // Indicate failure - } - }); - }); + const requestLib = isHttps ? https : http; + const req = requestLib.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + if (res.statusCode === 200) { + try { + const parsedData = JSON.parse(data); + resolve(parsedData.models || []); // Return the array of models + } catch (e) { + console.error('Error parsing Ollama response:', e); + resolve(null); // Indicate failure + } + } else { + console.error( + `Ollama API request failed with status code: ${res.statusCode}` + ); + resolve(null); // Indicate failure + } + }); + }); - req.on("error", (e) => { - console.error("Error fetching Ollama models:", e); - resolve(null); // Indicate failure - }); - req.end(); - } catch (e) { - console.error("Error parsing Ollama base URL:", e); - resolve(null); // Indicate failure - } - }); + req.on('error', (e) => { + console.error('Error fetching Ollama models:', e); + resolve(null); // Indicate failure + }); + req.end(); + } catch (e) { + console.error('Error parsing Ollama base URL:', e); + resolve(null); // Indicate failure + } + }); } /** @@ -141,125 +141,125 @@ function fetchOllamaModels(baseURL = "http://localhost:11434/api") { * @returns {Object} RESTful response with current model configuration */ async function getModelConfiguration(options = {}) { - const { mcpLog, projectRoot, session } = options; + const { mcpLog, projectRoot, session } = options; - const report = (level, ...args) => { - if (mcpLog && typeof mcpLog[level] === "function") { - mcpLog[level](...args); - } - }; + const report = (level, ...args) => { + if (mcpLog && typeof mcpLog[level] === 'function') { + mcpLog[level](...args); + } + }; - if (!projectRoot) { - throw new Error("Project root is required but not found."); - } + if (!projectRoot) { + throw new Error('Project root is required but not found.'); + } - // Use centralized config path finding instead of hardcoded path - const configPath = findConfigPath(null, { projectRoot }); - const configExists = isConfigFilePresent(projectRoot); + // Use centralized config path finding instead of hardcoded path + const configPath = findConfigPath(null, { projectRoot }); + const configExists = isConfigFilePresent(projectRoot); - log( - "debug", - `Checking for config file using findConfigPath, found: ${configPath}` - ); - log( - "debug", - `Checking config file using isConfigFilePresent(), exists: ${configExists}` - ); + log( + 'debug', + `Checking for config file using findConfigPath, found: ${configPath}` + ); + log( + 'debug', + `Checking config file using isConfigFilePresent(), exists: ${configExists}` + ); - if (!configExists) { - throw new Error( - 'The configuration file is missing. Run "task-master models --setup" to create it.' - ); - } + if (!configExists) { + throw new Error( + 'The configuration file is missing. Run "task-master models --setup" to create it.' + ); + } - try { - // Get current settings - these should use the config from the found path automatically - const mainProvider = getMainProvider(projectRoot); - const mainModelId = getMainModelId(projectRoot); - const researchProvider = getResearchProvider(projectRoot); - const researchModelId = getResearchModelId(projectRoot); - const fallbackProvider = getFallbackProvider(projectRoot); - const fallbackModelId = getFallbackModelId(projectRoot); + try { + // Get current settings - these should use the config from the found path automatically + const mainProvider = getMainProvider(projectRoot); + const mainModelId = getMainModelId(projectRoot); + const researchProvider = getResearchProvider(projectRoot); + const researchModelId = getResearchModelId(projectRoot); + const fallbackProvider = getFallbackProvider(projectRoot); + const fallbackModelId = getFallbackModelId(projectRoot); - // Check API keys - const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot); - const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot); - const researchCliKeyOk = isApiKeySet( - researchProvider, - session, - projectRoot - ); - const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot); - const fallbackCliKeyOk = fallbackProvider - ? isApiKeySet(fallbackProvider, session, projectRoot) - : true; - const fallbackMcpKeyOk = fallbackProvider - ? getMcpApiKeyStatus(fallbackProvider, projectRoot) - : true; + // Check API keys + const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot); + const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot); + const researchCliKeyOk = isApiKeySet( + researchProvider, + session, + projectRoot + ); + const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot); + const fallbackCliKeyOk = fallbackProvider + ? isApiKeySet(fallbackProvider, session, projectRoot) + : true; + const fallbackMcpKeyOk = fallbackProvider + ? getMcpApiKeyStatus(fallbackProvider, projectRoot) + : true; - // Get available models to find detailed info - const availableModels = getAvailableModels(projectRoot); + // Get available models to find detailed info + const availableModels = getAvailableModels(projectRoot); - // Find model details - const mainModelData = availableModels.find((m) => m.id === mainModelId); - const researchModelData = availableModels.find( - (m) => m.id === researchModelId - ); - const fallbackModelData = fallbackModelId - ? availableModels.find((m) => m.id === fallbackModelId) - : null; + // Find model details + const mainModelData = availableModels.find((m) => m.id === mainModelId); + const researchModelData = availableModels.find( + (m) => m.id === researchModelId + ); + const fallbackModelData = fallbackModelId + ? availableModels.find((m) => m.id === fallbackModelId) + : null; - // Return structured configuration data - return { - success: true, - data: { - activeModels: { - main: { - provider: mainProvider, - modelId: mainModelId, - sweScore: mainModelData?.swe_score || null, - cost: mainModelData?.cost_per_1m_tokens || null, - keyStatus: { - cli: mainCliKeyOk, - mcp: mainMcpKeyOk, - }, - }, - research: { - provider: researchProvider, - modelId: researchModelId, - sweScore: researchModelData?.swe_score || null, - cost: researchModelData?.cost_per_1m_tokens || null, - keyStatus: { - cli: researchCliKeyOk, - mcp: researchMcpKeyOk, - }, - }, - fallback: fallbackProvider - ? { - provider: fallbackProvider, - modelId: fallbackModelId, - sweScore: fallbackModelData?.swe_score || null, - cost: fallbackModelData?.cost_per_1m_tokens || null, - keyStatus: { - cli: fallbackCliKeyOk, - mcp: fallbackMcpKeyOk, - }, - } - : null, - }, - message: "Successfully retrieved current model configuration", - }, - }; - } catch (error) { - report("error", `Error getting model configuration: ${error.message}`); - return { - success: false, - error: { - code: "CONFIG_ERROR", - message: error.message, - }, - }; - } + // Return structured configuration data + return { + success: true, + data: { + activeModels: { + main: { + provider: mainProvider, + modelId: mainModelId, + sweScore: mainModelData?.swe_score || null, + cost: mainModelData?.cost_per_1m_tokens || null, + keyStatus: { + cli: mainCliKeyOk, + mcp: mainMcpKeyOk + } + }, + research: { + provider: researchProvider, + modelId: researchModelId, + sweScore: researchModelData?.swe_score || null, + cost: researchModelData?.cost_per_1m_tokens || null, + keyStatus: { + cli: researchCliKeyOk, + mcp: researchMcpKeyOk + } + }, + fallback: fallbackProvider + ? { + provider: fallbackProvider, + modelId: fallbackModelId, + sweScore: fallbackModelData?.swe_score || null, + cost: fallbackModelData?.cost_per_1m_tokens || null, + keyStatus: { + cli: fallbackCliKeyOk, + mcp: fallbackMcpKeyOk + } + } + : null + }, + message: 'Successfully retrieved current model configuration' + } + }; + } catch (error) { + report('error', `Error getting model configuration: ${error.message}`); + return { + success: false, + error: { + code: 'CONFIG_ERROR', + message: error.message + } + }; + } } /** @@ -271,85 +271,85 @@ async function getModelConfiguration(options = {}) { * @returns {Object} RESTful response with available models */ async function getAvailableModelsList(options = {}) { - const { mcpLog, projectRoot } = options; + const { mcpLog, projectRoot } = options; - const report = (level, ...args) => { - if (mcpLog && typeof mcpLog[level] === "function") { - mcpLog[level](...args); - } - }; + const report = (level, ...args) => { + if (mcpLog && typeof mcpLog[level] === 'function') { + mcpLog[level](...args); + } + }; - if (!projectRoot) { - throw new Error("Project root is required but not found."); - } + if (!projectRoot) { + throw new Error('Project root is required but not found.'); + } - // Use centralized config path finding instead of hardcoded path - const configPath = findConfigPath(null, { projectRoot }); - const configExists = isConfigFilePresent(projectRoot); + // Use centralized config path finding instead of hardcoded path + const configPath = findConfigPath(null, { projectRoot }); + const configExists = isConfigFilePresent(projectRoot); - log( - "debug", - `Checking for config file using findConfigPath, found: ${configPath}` - ); - log( - "debug", - `Checking config file using isConfigFilePresent(), exists: ${configExists}` - ); + log( + 'debug', + `Checking for config file using findConfigPath, found: ${configPath}` + ); + log( + 'debug', + `Checking config file using isConfigFilePresent(), exists: ${configExists}` + ); - if (!configExists) { - throw new Error( - 'The configuration file is missing. Run "task-master models --setup" to create it.' - ); - } + if (!configExists) { + throw new Error( + 'The configuration file is missing. Run "task-master models --setup" to create it.' + ); + } - try { - // Get all available models - const allAvailableModels = getAvailableModels(projectRoot); + try { + // Get all available models + const allAvailableModels = getAvailableModels(projectRoot); - if (!allAvailableModels || allAvailableModels.length === 0) { - return { - success: true, - data: { - models: [], - message: "No available models found", - }, - }; - } + if (!allAvailableModels || allAvailableModels.length === 0) { + return { + success: true, + data: { + models: [], + message: 'No available models found' + } + }; + } - // Get currently used model IDs - const mainModelId = getMainModelId(projectRoot); - const researchModelId = getResearchModelId(projectRoot); - const fallbackModelId = getFallbackModelId(projectRoot); + // Get currently used model IDs + const mainModelId = getMainModelId(projectRoot); + const researchModelId = getResearchModelId(projectRoot); + const fallbackModelId = getFallbackModelId(projectRoot); - // Filter out placeholder models and active models - const activeIds = [mainModelId, researchModelId, fallbackModelId].filter( - Boolean - ); - const otherAvailableModels = allAvailableModels.map((model) => ({ - provider: model.provider || "N/A", - modelId: model.id, - sweScore: model.swe_score || null, - cost: model.cost_per_1m_tokens || null, - allowedRoles: model.allowed_roles || [], - })); + // Filter out placeholder models and active models + const activeIds = [mainModelId, researchModelId, fallbackModelId].filter( + Boolean + ); + const otherAvailableModels = allAvailableModels.map((model) => ({ + provider: model.provider || 'N/A', + modelId: model.id, + sweScore: model.swe_score || null, + cost: model.cost_per_1m_tokens || null, + allowedRoles: model.allowed_roles || [] + })); - return { - success: true, - data: { - models: otherAvailableModels, - message: `Successfully retrieved ${otherAvailableModels.length} available models`, - }, - }; - } catch (error) { - report("error", `Error getting available models: ${error.message}`); - return { - success: false, - error: { - code: "MODELS_LIST_ERROR", - message: error.message, - }, - }; - } + return { + success: true, + data: { + models: otherAvailableModels, + message: `Successfully retrieved ${otherAvailableModels.length} available models` + } + }; + } catch (error) { + report('error', `Error getting available models: ${error.message}`); + return { + success: false, + error: { + code: 'MODELS_LIST_ERROR', + message: error.message + } + }; + } } /** @@ -364,218 +364,218 @@ async function getAvailableModelsList(options = {}) { * @returns {Object} RESTful response with result of update operation */ async function setModel(role, modelId, options = {}) { - const { mcpLog, projectRoot, providerHint } = options; + const { mcpLog, projectRoot, providerHint } = options; - const report = (level, ...args) => { - if (mcpLog && typeof mcpLog[level] === "function") { - mcpLog[level](...args); - } - }; + const report = (level, ...args) => { + if (mcpLog && typeof mcpLog[level] === 'function') { + mcpLog[level](...args); + } + }; - if (!projectRoot) { - throw new Error("Project root is required but not found."); - } + if (!projectRoot) { + throw new Error('Project root is required but not found.'); + } - // Use centralized config path finding instead of hardcoded path - const configPath = findConfigPath(null, { projectRoot }); - const configExists = isConfigFilePresent(projectRoot); + // Use centralized config path finding instead of hardcoded path + const configPath = findConfigPath(null, { projectRoot }); + const configExists = isConfigFilePresent(projectRoot); - log( - "debug", - `Checking for config file using findConfigPath, found: ${configPath}` - ); - log( - "debug", - `Checking config file using isConfigFilePresent(), exists: ${configExists}` - ); + log( + 'debug', + `Checking for config file using findConfigPath, found: ${configPath}` + ); + log( + 'debug', + `Checking config file using isConfigFilePresent(), exists: ${configExists}` + ); - if (!configExists) { - throw new Error( - 'The configuration file is missing. Run "task-master models --setup" to create it.' - ); - } + if (!configExists) { + throw new Error( + 'The configuration file is missing. Run "task-master models --setup" to create it.' + ); + } - // Validate role - if (!["main", "research", "fallback"].includes(role)) { - return { - success: false, - error: { - code: "INVALID_ROLE", - message: `Invalid role: ${role}. Must be one of: main, research, fallback.`, - }, - }; - } + // Validate role + if (!['main', 'research', 'fallback'].includes(role)) { + return { + success: false, + error: { + code: 'INVALID_ROLE', + message: `Invalid role: ${role}. Must be one of: main, research, fallback.` + } + }; + } - // Validate model ID - if (typeof modelId !== "string" || modelId.trim() === "") { - return { - success: false, - error: { - code: "INVALID_MODEL_ID", - message: `Invalid model ID: ${modelId}. Must be a non-empty string.`, - }, - }; - } + // Validate model ID + if (typeof modelId !== 'string' || modelId.trim() === '') { + return { + success: false, + error: { + code: 'INVALID_MODEL_ID', + message: `Invalid model ID: ${modelId}. Must be a non-empty string.` + } + }; + } - try { - const availableModels = getAvailableModels(projectRoot); - const currentConfig = getConfig(projectRoot); - let determinedProvider = null; // Initialize provider - let warningMessage = null; + try { + const availableModels = getAvailableModels(projectRoot); + const currentConfig = getConfig(projectRoot); + let determinedProvider = null; // Initialize provider + let warningMessage = null; - // Find the model data in internal list initially to see if it exists at all - const modelData = availableModels.find((m) => m.id === modelId); + // Find the model data in internal list initially to see if it exists at all + const modelData = availableModels.find((m) => m.id === modelId); - // --- Revised Logic: Prioritize providerHint --- // + // --- Revised Logic: Prioritize providerHint --- // - if (providerHint) { - // Hint provided (--ollama or --openrouter flag used) - if (modelData && modelData.provider === providerHint) { - // Found internally AND provider matches the hint - determinedProvider = providerHint; - report( - "info", - `Model ${modelId} found internally with matching provider hint ${determinedProvider}.` - ); - } else { - // Either not found internally, OR found but under a DIFFERENT provider than hinted. - // Proceed with custom logic based ONLY on the hint. - if (providerHint === "openrouter") { - // Check OpenRouter ONLY because hint was openrouter - report("info", `Checking OpenRouter for ${modelId} (as hinted)...`); - const openRouterModels = await fetchOpenRouterModels(); + if (providerHint) { + // Hint provided (--ollama or --openrouter flag used) + if (modelData && modelData.provider === providerHint) { + // Found internally AND provider matches the hint + determinedProvider = providerHint; + report( + 'info', + `Model ${modelId} found internally with matching provider hint ${determinedProvider}.` + ); + } else { + // Either not found internally, OR found but under a DIFFERENT provider than hinted. + // Proceed with custom logic based ONLY on the hint. + if (providerHint === 'openrouter') { + // Check OpenRouter ONLY because hint was openrouter + report('info', `Checking OpenRouter for ${modelId} (as hinted)...`); + const openRouterModels = await fetchOpenRouterModels(); - if ( - openRouterModels && - openRouterModels.some((m) => m.id === modelId) - ) { - determinedProvider = "openrouter"; + if ( + openRouterModels && + openRouterModels.some((m) => m.id === modelId) + ) { + determinedProvider = 'openrouter'; - // Check if this is a free model (ends with :free) - if (modelId.endsWith(":free")) { - warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(":free", "")}' for full functionality.`; - } else { - warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`; - } + // Check if this is a free model (ends with :free) + if (modelId.endsWith(':free')) { + warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`; + } else { + warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`; + } - report("warn", warningMessage); - } else { - // Hinted as OpenRouter but not found in live check - throw new Error( - `Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.` - ); - } - } else if (providerHint === "ollama") { - // Check Ollama ONLY because hint was ollama - report("info", `Checking Ollama for ${modelId} (as hinted)...`); + report('warn', warningMessage); + } else { + // Hinted as OpenRouter but not found in live check + throw new Error( + `Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.` + ); + } + } else if (providerHint === 'ollama') { + // Check Ollama ONLY because hint was ollama + report('info', `Checking Ollama for ${modelId} (as hinted)...`); - // Get the Ollama base URL from config - const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); - const ollamaModels = await fetchOllamaModels(ollamaBaseURL); + // Get the Ollama base URL from config + const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); + const ollamaModels = await fetchOllamaModels(ollamaBaseURL); - if (ollamaModels === null) { - // Connection failed - server probably not running - throw new Error( - `Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` - ); - } else if (ollamaModels.some((m) => m.model === modelId)) { - determinedProvider = "ollama"; - warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`; - report("warn", warningMessage); - } else { - // Server is running but model not found - const tagsUrl = `${ollamaBaseURL}/tags`; - throw new Error( - `Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}` - ); - } - } else if (providerHint === "bedrock") { - // Set provider without model validation since Bedrock models are managed by AWS - determinedProvider = "bedrock"; - warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`; - report("warn", warningMessage); - } else { - // Invalid provider hint - should not happen - throw new Error(`Invalid provider hint received: ${providerHint}`); - } - } - } else { - // No hint provided (flags not used) - if (modelData) { - // Found internally, use the provider from the internal list - determinedProvider = modelData.provider; - report( - "info", - `Model ${modelId} found internally with provider ${determinedProvider}.` - ); - } else { - // Model not found and no provider hint was given - return { - success: false, - error: { - code: "MODEL_NOT_FOUND_NO_HINT", - message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`, - }, - }; - } - } + if (ollamaModels === null) { + // Connection failed - server probably not running + throw new Error( + `Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` + ); + } else if (ollamaModels.some((m) => m.model === modelId)) { + determinedProvider = 'ollama'; + warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`; + report('warn', warningMessage); + } else { + // Server is running but model not found + const tagsUrl = `${ollamaBaseURL}/tags`; + throw new Error( + `Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}` + ); + } + } else if (providerHint === 'bedrock') { + // Set provider without model validation since Bedrock models are managed by AWS + determinedProvider = 'bedrock'; + warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`; + report('warn', warningMessage); + } else { + // Invalid provider hint - should not happen + throw new Error(`Invalid provider hint received: ${providerHint}`); + } + } + } else { + // No hint provided (flags not used) + if (modelData) { + // Found internally, use the provider from the internal list + determinedProvider = modelData.provider; + report( + 'info', + `Model ${modelId} found internally with provider ${determinedProvider}.` + ); + } else { + // Model not found and no provider hint was given + return { + success: false, + error: { + code: 'MODEL_NOT_FOUND_NO_HINT', + message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.` + } + }; + } + } - // --- End of Revised Logic --- // + // --- End of Revised Logic --- // - // At this point, we should have a determinedProvider if the model is valid (internally or custom) - if (!determinedProvider) { - // This case acts as a safeguard - return { - success: false, - error: { - code: "PROVIDER_UNDETERMINED", - message: `Could not determine the provider for model ID "${modelId}".`, - }, - }; - } + // At this point, we should have a determinedProvider if the model is valid (internally or custom) + if (!determinedProvider) { + // This case acts as a safeguard + return { + success: false, + error: { + code: 'PROVIDER_UNDETERMINED', + message: `Could not determine the provider for model ID "${modelId}".` + } + }; + } - // Update configuration - currentConfig.models[role] = { - ...currentConfig.models[role], // Keep existing params like maxTokens - provider: determinedProvider, - modelId: modelId, - }; + // Update configuration + currentConfig.models[role] = { + ...currentConfig.models[role], // Keep existing params like maxTokens + provider: determinedProvider, + modelId: modelId + }; - // Write updated configuration - const writeResult = writeConfig(currentConfig, projectRoot); - if (!writeResult) { - return { - success: false, - error: { - code: "CONFIG_WRITE_ERROR", - message: "Error writing updated configuration to configuration file", - }, - }; - } + // Write updated configuration + const writeResult = writeConfig(currentConfig, projectRoot); + if (!writeResult) { + return { + success: false, + error: { + code: 'CONFIG_WRITE_ERROR', + message: 'Error writing updated configuration to configuration file' + } + }; + } - const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`; - report("info", successMessage); + const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`; + report('info', successMessage); - return { - success: true, - data: { - role, - provider: determinedProvider, - modelId, - message: successMessage, - warning: warningMessage, // Include warning in the response data - }, - }; - } catch (error) { - report("error", `Error setting ${role} model: ${error.message}`); - return { - success: false, - error: { - code: "SET_MODEL_ERROR", - message: error.message, - }, - }; - } + return { + success: true, + data: { + role, + provider: determinedProvider, + modelId, + message: successMessage, + warning: warningMessage // Include warning in the response data + } + }; + } catch (error) { + report('error', `Error setting ${role} model: ${error.message}`); + return { + success: false, + error: { + code: 'SET_MODEL_ERROR', + message: error.message + } + }; + } } /** @@ -587,52 +587,52 @@ async function setModel(role, modelId, options = {}) { * @returns {Object} RESTful response with API key status report */ async function getApiKeyStatusReport(options = {}) { - const { mcpLog, projectRoot, session } = options; - const report = (level, ...args) => { - if (mcpLog && typeof mcpLog[level] === "function") { - mcpLog[level](...args); - } - }; + const { mcpLog, projectRoot, session } = options; + const report = (level, ...args) => { + if (mcpLog && typeof mcpLog[level] === 'function') { + mcpLog[level](...args); + } + }; - try { - const providers = getAllProviders(); - const providersToCheck = providers.filter( - (p) => p.toLowerCase() !== "ollama" - ); // Ollama is not a provider, it's a service, doesn't need an api key usually - const statusReport = providersToCheck.map((provider) => { - // Use provided projectRoot for MCP status check - const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check - const mcpOk = getMcpApiKeyStatus(provider, projectRoot); - return { - provider, - cli: cliOk, - mcp: mcpOk, - }; - }); + try { + const providers = getAllProviders(); + const providersToCheck = providers.filter( + (p) => p.toLowerCase() !== 'ollama' + ); // Ollama is not a provider, it's a service, doesn't need an api key usually + const statusReport = providersToCheck.map((provider) => { + // Use provided projectRoot for MCP status check + const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check + const mcpOk = getMcpApiKeyStatus(provider, projectRoot); + return { + provider, + cli: cliOk, + mcp: mcpOk + }; + }); - report("info", "Successfully generated API key status report."); - return { - success: true, - data: { - report: statusReport, - message: "API key status report generated.", - }, - }; - } catch (error) { - report("error", `Error generating API key status report: ${error.message}`); - return { - success: false, - error: { - code: "API_KEY_STATUS_ERROR", - message: error.message, - }, - }; - } + report('info', 'Successfully generated API key status report.'); + return { + success: true, + data: { + report: statusReport, + message: 'API key status report generated.' + } + }; + } catch (error) { + report('error', `Error generating API key status report: ${error.message}`); + return { + success: false, + error: { + code: 'API_KEY_STATUS_ERROR', + message: error.message + } + }; + } } export { - getModelConfiguration, - getAvailableModelsList, - setModel, - getApiKeyStatusReport, + getModelConfiguration, + getAvailableModelsList, + setModel, + getApiKeyStatusReport }; diff --git a/scripts/modules/task-manager/set-task-status.js b/scripts/modules/task-manager/set-task-status.js index 7e66babd..1687eb30 100644 --- a/scripts/modules/task-manager/set-task-status.js +++ b/scripts/modules/task-manager/set-task-status.js @@ -1,17 +1,17 @@ -import path from "path"; -import chalk from "chalk"; -import boxen from "boxen"; +import path from 'path'; +import chalk from 'chalk'; +import boxen from 'boxen'; -import { log, readJSON, writeJSON, findTaskById } from "../utils.js"; -import { displayBanner } from "../ui.js"; -import { validateTaskDependencies } from "../dependency-manager.js"; -import { getDebugFlag } from "../config-manager.js"; -import updateSingleTaskStatus from "./update-single-task-status.js"; -import generateTaskFiles from "./generate-task-files.js"; +import { log, readJSON, writeJSON, findTaskById } from '../utils.js'; +import { displayBanner } from '../ui.js'; +import { validateTaskDependencies } from '../dependency-manager.js'; +import { getDebugFlag } from '../config-manager.js'; +import updateSingleTaskStatus from './update-single-task-status.js'; +import generateTaskFiles from './generate-task-files.js'; import { - isValidTaskStatus, - TASK_STATUS_OPTIONS, -} from "../../../src/constants/task-status.js"; + isValidTaskStatus, + TASK_STATUS_OPTIONS +} from '../../../src/constants/task-status.js'; /** * Set the status of a task @@ -22,100 +22,100 @@ import { * @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode */ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) { - try { - if (!isValidTaskStatus(newStatus)) { - throw new Error( - `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}` - ); - } - // Determine if we're in MCP mode by checking for mcpLog - const isMcpMode = !!options?.mcpLog; + try { + if (!isValidTaskStatus(newStatus)) { + throw new Error( + `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` + ); + } + // Determine if we're in MCP mode by checking for mcpLog + const isMcpMode = !!options?.mcpLog; - // Only display UI elements if not in MCP mode - if (!isMcpMode) { - console.log( - boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { - padding: 1, - borderColor: "blue", - borderStyle: "round", - }) - ); - } + // Only display UI elements if not in MCP mode + if (!isMcpMode) { + console.log( + boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { + padding: 1, + borderColor: 'blue', + borderStyle: 'round' + }) + ); + } - log("info", `Reading tasks from ${tasksPath}...`); - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - throw new Error(`No valid tasks found in ${tasksPath}`); - } + log('info', `Reading tasks from ${tasksPath}...`); + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + throw new Error(`No valid tasks found in ${tasksPath}`); + } - // Handle multiple task IDs (comma-separated) - const taskIds = taskIdInput.split(",").map((id) => id.trim()); - const updatedTasks = []; + // Handle multiple task IDs (comma-separated) + const taskIds = taskIdInput.split(',').map((id) => id.trim()); + const updatedTasks = []; - // Update each task - for (const id of taskIds) { - await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode); - updatedTasks.push(id); - } + // Update each task + for (const id of taskIds) { + await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode); + updatedTasks.push(id); + } - // Write the updated tasks to the file - writeJSON(tasksPath, data); + // Write the updated tasks to the file + writeJSON(tasksPath, data); - // Validate dependencies after status update - log("info", "Validating dependencies after status update..."); - validateTaskDependencies(data.tasks); + // Validate dependencies after status update + log('info', 'Validating dependencies after status update...'); + validateTaskDependencies(data.tasks); - // Generate individual task files - log("info", "Regenerating task files..."); - await generateTaskFiles(tasksPath, path.dirname(tasksPath), { - mcpLog: options.mcpLog, - }); + // Generate individual task files + log('info', 'Regenerating task files...'); + await generateTaskFiles(tasksPath, path.dirname(tasksPath), { + mcpLog: options.mcpLog + }); - // Display success message - only in CLI mode - if (!isMcpMode) { - for (const id of updatedTasks) { - const task = findTaskById(data.tasks, id); - const taskName = task ? task.title : id; + // Display success message - only in CLI mode + if (!isMcpMode) { + for (const id of updatedTasks) { + const task = findTaskById(data.tasks, id); + const taskName = task ? task.title : id; - console.log( - boxen( - chalk.white.bold(`Successfully updated task ${id} status:`) + - "\n" + - `From: ${chalk.yellow(task ? task.status : "unknown")}\n` + - `To: ${chalk.green(newStatus)}`, - { padding: 1, borderColor: "green", borderStyle: "round" } - ) - ); - } - } + console.log( + boxen( + chalk.white.bold(`Successfully updated task ${id} status:`) + + '\n' + + `From: ${chalk.yellow(task ? task.status : 'unknown')}\n` + + `To: ${chalk.green(newStatus)}`, + { padding: 1, borderColor: 'green', borderStyle: 'round' } + ) + ); + } + } - // Return success value for programmatic use - return { - success: true, - updatedTasks: updatedTasks.map((id) => ({ - id, - status: newStatus, - })), - }; - } catch (error) { - log("error", `Error setting task status: ${error.message}`); + // Return success value for programmatic use + return { + success: true, + updatedTasks: updatedTasks.map((id) => ({ + id, + status: newStatus + })) + }; + } catch (error) { + log('error', `Error setting task status: ${error.message}`); - // Only show error UI in CLI mode - if (!options?.mcpLog) { - console.error(chalk.red(`Error: ${error.message}`)); + // Only show error UI in CLI mode + if (!options?.mcpLog) { + console.error(chalk.red(`Error: ${error.message}`)); - // Pass session to getDebugFlag - if (getDebugFlag(options?.session)) { - // Use getter - console.error(error); - } + // Pass session to getDebugFlag + if (getDebugFlag(options?.session)) { + // Use getter + console.error(error); + } - process.exit(1); - } else { - // In MCP mode, throw the error for the caller to handle - throw error; - } - } + process.exit(1); + } else { + // In MCP mode, throw the error for the caller to handle + throw error; + } + } } export default setTaskStatus; diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 9c1307ca..bcafbc5e 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -3,73 +3,73 @@ * 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"; + 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 { - TASKMASTER_CONFIG_FILE, - TASKMASTER_TASKS_FILE, -} from "../../src/constants/paths.js"; -import { getTaskMasterVersion } from "../../src/utils/getVersion.js"; + TASKMASTER_CONFIG_FILE, + TASKMASTER_TASKS_FILE +} from '../../src/constants/paths.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(); // Removing this to avoid clearing the terminal per command - const bannerText = figlet.textSync("Task Master", { - font: "Standard", - horizontalLayout: "default", - verticalLayout: "default", - }); + // console.clear(); // Removing this to avoid clearing the terminal per command + 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' + } + ) + ); } /** @@ -78,14 +78,14 @@ function displayBanner() { * @returns {Object} Spinner object */ function startLoadingIndicator(message) { - if (isSilentMode()) return null; + if (isSilentMode()) return null; - const spinner = ora({ - text: message, - color: "cyan", - }).start(); + const spinner = ora({ + text: message, + color: 'cyan' + }).start(); - return spinner; + return spinner; } /** @@ -93,9 +93,9 @@ function startLoadingIndicator(message) { * @param {Object} spinner - Spinner object to stop */ function stopLoadingIndicator(spinner) { - if (spinner && typeof spinner.stop === "function") { - spinner.stop(); - } + if (spinner && typeof spinner.stop === 'function') { + spinner.stop(); + } } /** @@ -104,13 +104,13 @@ function stopLoadingIndicator(spinner) { * @param {string} message - Optional success message (defaults to current text) */ function succeedLoadingIndicator(spinner, message = null) { - if (spinner && typeof spinner.succeed === "function") { - if (message) { - spinner.succeed(message); - } else { - spinner.succeed(); - } - } + if (spinner && typeof spinner.succeed === 'function') { + if (message) { + spinner.succeed(message); + } else { + spinner.succeed(); + } + } } /** @@ -119,13 +119,13 @@ function succeedLoadingIndicator(spinner, message = null) { * @param {string} message - Optional failure message (defaults to current text) */ function failLoadingIndicator(spinner, message = null) { - if (spinner && typeof spinner.fail === "function") { - if (message) { - spinner.fail(message); - } else { - spinner.fail(); - } - } + if (spinner && typeof spinner.fail === 'function') { + if (message) { + spinner.fail(message); + } else { + spinner.fail(); + } + } } /** @@ -134,13 +134,13 @@ function failLoadingIndicator(spinner, message = null) { * @param {string} message - Optional warning message (defaults to current text) */ function warnLoadingIndicator(spinner, message = null) { - if (spinner && typeof spinner.warn === "function") { - if (message) { - spinner.warn(message); - } else { - spinner.warn(); - } - } + if (spinner && typeof spinner.warn === 'function') { + if (message) { + spinner.warn(message); + } else { + spinner.warn(); + } + } } /** @@ -149,13 +149,13 @@ function warnLoadingIndicator(spinner, message = null) { * @param {string} message - Optional info message (defaults to current text) */ function infoLoadingIndicator(spinner, message = null) { - if (spinner && typeof spinner.info === "function") { - if (message) { - spinner.info(message); - } else { - spinner.info(); - } - } + if (spinner && typeof spinner.info === 'function') { + if (message) { + spinner.info(message); + } else { + spinner.info(); + } + } } /** @@ -166,120 +166,120 @@ function infoLoadingIndicator(spinner, message = null) { * @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)}%`)}`; } /** @@ -289,44 +289,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: "x", tableIcon: "⏱" }, - blocked: { color: chalk.red, icon: "!", tableIcon: "✗" }, - review: { color: chalk.magenta, icon: "?", tableIcon: "?" }, - cancelled: { color: chalk.gray, icon: "❌", tableIcon: "x" }, - }; + 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: 'x', tableIcon: '⏱' }, + blocked: { color: chalk.red, icon: '!', tableIcon: '✗' }, + review: { color: chalk.magenta, icon: '?', tableIcon: '?' }, + cancelled: { color: chalk.gray, icon: '❌', tableIcon: 'x' } + }; - 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}`); } /** @@ -338,465 +338,465 @@ 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() { - // 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", - }, - ], - }, - { - 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' + } + ] + }, + { + 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(TASKMASTER_CONFIG_FILE)}${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(TASKMASTER_CONFIG_FILE)}${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 + } + ) + ); } /** @@ -805,9 +805,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}`); } /** @@ -817,9 +817,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) + '...'; } /** @@ -827,262 +827,262 @@ function truncateString(str, maxLength) { * @param {string} tasksPath - Path to the tasks.json file */ async function displayNextTask(tasksPath, complexityReportPath = null) { - // 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 } + }) + ); } /** @@ -1092,458 +1092,458 @@ 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 ) { - // 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 } + } + ) + ); } /** @@ -1551,249 +1551,249 @@ async function displayTaskById( * @param {string} reportPath - Path to the complexity report file */ async function displayComplexityReport(reportPath) { - // 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...")); - const tasksPath = TASKMASTER_TASKS_FILE; - if (!fs.existsSync(tasksPath)) { - console.error( - '❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file.' - ); - return null; - } + if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { + // Call the analyze-complexity command + console.log(chalk.blue('Generating complexity report...')); + const tasksPath = TASKMASTER_TASKS_FILE; + if (!fs.existsSync(tasksPath)) { + console.error( + '❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file.' + ); + return null; + } - await analyzeTaskComplexity({ - output: reportPath, - research: false, // Default to no research for speed - file: tasksPath, - }); - // Read the newly generated report - return displayComplexityReport(reportPath); - } else { - console.log(chalk.yellow("Report generation cancelled.")); - return; - } - } + await analyzeTaskComplexity({ + output: reportPath, + research: false, // Default to no research for speed + file: tasksPath + }); + // 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 } + } + ) + ); } /** @@ -1802,21 +1802,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: [ @@ -1841,39 +1841,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'; } /** @@ -1881,75 +1881,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 ${TASKMASTER_CONFIG_FILE}.` - ) - ); + 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 ${TASKMASTER_CONFIG_FILE}.` + ) + ); } // --- 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 --- @@ -1960,63 +1960,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()); } /** @@ -2024,64 +2024,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 } + } + ) + ); } /** @@ -2089,67 +2089,67 @@ 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' + }) + ); } // Export UI functions export { - displayBanner, - startLoadingIndicator, - stopLoadingIndicator, - createProgressBar, - getStatusWithColor, - formatDependenciesWithStatus, - displayHelp, - getComplexityWithColor, - displayNextTask, - displayTaskById, - displayComplexityReport, - generateComplexityAnalysisPrompt, - confirmTaskOverwrite, - displayApiKeyStatus, - displayModelConfiguration, - displayAvailableModels, - displayAiUsageSummary, - succeedLoadingIndicator, - failLoadingIndicator, - warnLoadingIndicator, - infoLoadingIndicator, + displayBanner, + startLoadingIndicator, + stopLoadingIndicator, + createProgressBar, + getStatusWithColor, + formatDependenciesWithStatus, + displayHelp, + getComplexityWithColor, + displayNextTask, + displayTaskById, + displayComplexityReport, + generateComplexityAnalysisPrompt, + confirmTaskOverwrite, + displayApiKeyStatus, + displayModelConfiguration, + displayAvailableModels, + displayAiUsageSummary, + succeedLoadingIndicator, + failLoadingIndicator, + warnLoadingIndicator, + infoLoadingIndicator }; diff --git a/src/ai-providers/base-provider.js b/src/ai-providers/base-provider.js index b96561d7..c0ad3c05 100644 --- a/src/ai-providers/base-provider.js +++ b/src/ai-providers/base-provider.js @@ -1,214 +1,214 @@ -import { generateText, streamText, generateObject } from "ai"; -import { log } from "../../scripts/modules/index.js"; +import { generateText, streamText, generateObject } from 'ai'; +import { log } from '../../scripts/modules/index.js'; /** * Base class for all AI providers */ export class BaseAIProvider { - constructor() { - if (this.constructor === BaseAIProvider) { - throw new Error("BaseAIProvider cannot be instantiated directly"); - } + constructor() { + if (this.constructor === BaseAIProvider) { + throw new Error('BaseAIProvider cannot be instantiated directly'); + } - // Each provider must set their name - this.name = this.constructor.name; - } + // Each provider must set their name + this.name = this.constructor.name; + } - /** - * Validates authentication parameters - can be overridden by providers - * @param {object} params - Parameters to validate - */ - validateAuth(params) { - // Default: require API key (most providers need this) - if (!params.apiKey) { - throw new Error(`${this.name} API key is required`); - } - } + /** + * Validates authentication parameters - can be overridden by providers + * @param {object} params - Parameters to validate + */ + validateAuth(params) { + // Default: require API key (most providers need this) + if (!params.apiKey) { + throw new Error(`${this.name} API key is required`); + } + } - /** - * Validates common parameters across all methods - * @param {object} params - Parameters to validate - */ - validateParams(params) { - // Validate authentication (can be overridden by providers) - this.validateAuth(params); + /** + * Validates common parameters across all methods + * @param {object} params - Parameters to validate + */ + validateParams(params) { + // Validate authentication (can be overridden by providers) + this.validateAuth(params); - // Validate required model ID - if (!params.modelId) { - throw new Error(`${this.name} Model ID is required`); - } + // Validate required model ID + if (!params.modelId) { + throw new Error(`${this.name} Model ID is required`); + } - // Validate optional parameters - this.validateOptionalParams(params); - } + // Validate optional parameters + this.validateOptionalParams(params); + } - /** - * Validates optional parameters like temperature and maxTokens - * @param {object} params - Parameters to validate - */ - validateOptionalParams(params) { - if ( - params.temperature !== undefined && - (params.temperature < 0 || params.temperature > 1) - ) { - throw new Error("Temperature must be between 0 and 1"); - } - if (params.maxTokens !== undefined && params.maxTokens <= 0) { - throw new Error("maxTokens must be greater than 0"); - } - } + /** + * Validates optional parameters like temperature and maxTokens + * @param {object} params - Parameters to validate + */ + validateOptionalParams(params) { + if ( + params.temperature !== undefined && + (params.temperature < 0 || params.temperature > 1) + ) { + throw new Error('Temperature must be between 0 and 1'); + } + if (params.maxTokens !== undefined && params.maxTokens <= 0) { + throw new Error('maxTokens must be greater than 0'); + } + } - /** - * Validates message array structure - */ - validateMessages(messages) { - if (!messages || !Array.isArray(messages) || messages.length === 0) { - throw new Error("Invalid or empty messages array provided"); - } + /** + * Validates message array structure + */ + validateMessages(messages) { + if (!messages || !Array.isArray(messages) || messages.length === 0) { + throw new Error('Invalid or empty messages array provided'); + } - for (const msg of messages) { - if (!msg.role || !msg.content) { - throw new Error( - "Invalid message format. Each message must have role and content" - ); - } - } - } + for (const msg of messages) { + if (!msg.role || !msg.content) { + throw new Error( + 'Invalid message format. Each message must have role and content' + ); + } + } + } - /** - * Common error handler - */ - handleError(operation, error) { - const errorMessage = error.message || "Unknown error occurred"; - log("error", `${this.name} ${operation} failed: ${errorMessage}`, { - error, - }); - throw new Error( - `${this.name} API error during ${operation}: ${errorMessage}` - ); - } + /** + * Common error handler + */ + handleError(operation, error) { + const errorMessage = error.message || 'Unknown error occurred'; + log('error', `${this.name} ${operation} failed: ${errorMessage}`, { + error + }); + throw new Error( + `${this.name} API error during ${operation}: ${errorMessage}` + ); + } - /** - * Creates and returns a client instance for the provider - * @abstract - */ - getClient(params) { - throw new Error("getClient must be implemented by provider"); - } + /** + * Creates and returns a client instance for the provider + * @abstract + */ + getClient(params) { + throw new Error('getClient must be implemented by provider'); + } - /** - * Generates text using the provider's model - */ - async generateText(params) { - try { - this.validateParams(params); - this.validateMessages(params.messages); + /** + * Generates text using the provider's model + */ + async generateText(params) { + try { + this.validateParams(params); + this.validateMessages(params.messages); - log( - "debug", - `Generating ${this.name} text with model: ${params.modelId}` - ); + log( + 'debug', + `Generating ${this.name} text with model: ${params.modelId}` + ); - const client = this.getClient(params); - const result = await generateText({ - model: client(params.modelId), - messages: params.messages, - maxTokens: params.maxTokens, - temperature: params.temperature, - }); + const client = this.getClient(params); + const result = await generateText({ + model: client(params.modelId), + messages: params.messages, + maxTokens: params.maxTokens, + temperature: params.temperature + }); - log( - "debug", - `${this.name} generateText completed successfully for model: ${params.modelId}` - ); + log( + 'debug', + `${this.name} generateText completed successfully for model: ${params.modelId}` + ); - return { - text: result.text, - usage: { - inputTokens: result.usage?.promptTokens, - outputTokens: result.usage?.completionTokens, - totalTokens: result.usage?.totalTokens, - }, - }; - } catch (error) { - this.handleError("text generation", error); - } - } + return { + text: result.text, + usage: { + inputTokens: result.usage?.promptTokens, + outputTokens: result.usage?.completionTokens, + totalTokens: result.usage?.totalTokens + } + }; + } catch (error) { + this.handleError('text generation', error); + } + } - /** - * Streams text using the provider's model - */ - async streamText(params) { - try { - this.validateParams(params); - this.validateMessages(params.messages); + /** + * Streams text using the provider's model + */ + async streamText(params) { + try { + this.validateParams(params); + this.validateMessages(params.messages); - log("debug", `Streaming ${this.name} text with model: ${params.modelId}`); + log('debug', `Streaming ${this.name} text with model: ${params.modelId}`); - const client = this.getClient(params); - const stream = await streamText({ - model: client(params.modelId), - messages: params.messages, - maxTokens: params.maxTokens, - temperature: params.temperature, - }); + const client = this.getClient(params); + const stream = await streamText({ + model: client(params.modelId), + messages: params.messages, + maxTokens: params.maxTokens, + temperature: params.temperature + }); - log( - "debug", - `${this.name} streamText initiated successfully for model: ${params.modelId}` - ); + log( + 'debug', + `${this.name} streamText initiated successfully for model: ${params.modelId}` + ); - return stream; - } catch (error) { - this.handleError("text streaming", error); - } - } + return stream; + } catch (error) { + this.handleError('text streaming', error); + } + } - /** - * Generates a structured object using the provider's model - */ - async generateObject(params) { - try { - this.validateParams(params); - this.validateMessages(params.messages); + /** + * Generates a structured object using the provider's model + */ + async generateObject(params) { + try { + this.validateParams(params); + this.validateMessages(params.messages); - if (!params.schema) { - throw new Error("Schema is required for object generation"); - } - if (!params.objectName) { - throw new Error("Object name is required for object generation"); - } + if (!params.schema) { + throw new Error('Schema is required for object generation'); + } + if (!params.objectName) { + throw new Error('Object name is required for object generation'); + } - log( - "debug", - `Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}` - ); + log( + 'debug', + `Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}` + ); - const client = this.getClient(params); - const result = await generateObject({ - model: client(params.modelId), - messages: params.messages, - schema: params.schema, - mode: "auto", - maxTokens: params.maxTokens, - temperature: params.temperature, - }); + const client = this.getClient(params); + const result = await generateObject({ + model: client(params.modelId), + messages: params.messages, + schema: params.schema, + mode: 'auto', + maxTokens: params.maxTokens, + temperature: params.temperature + }); - log( - "debug", - `${this.name} generateObject completed successfully for model: ${params.modelId}` - ); + log( + 'debug', + `${this.name} generateObject completed successfully for model: ${params.modelId}` + ); - return { - object: result.object, - usage: { - inputTokens: result.usage?.promptTokens, - outputTokens: result.usage?.completionTokens, - totalTokens: result.usage?.totalTokens, - }, - }; - } catch (error) { - this.handleError("object generation", error); - } - } + return { + object: result.object, + usage: { + inputTokens: result.usage?.promptTokens, + outputTokens: result.usage?.completionTokens, + totalTokens: result.usage?.totalTokens + } + }; + } catch (error) { + this.handleError('object generation', error); + } + } } diff --git a/tests/unit/scripts/modules/task-manager/add-task.test.js b/tests/unit/scripts/modules/task-manager/add-task.test.js index 3a3e6288..9a31cf10 100644 --- a/tests/unit/scripts/modules/task-manager/add-task.test.js +++ b/tests/unit/scripts/modules/task-manager/add-task.test.js @@ -1,400 +1,404 @@ /** * Tests for the add-task.js module */ -import { jest } from '@jest/globals'; +import { jest } from "@jest/globals"; // Mock the dependencies before importing the module under test -jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ - readJSON: jest.fn(), - writeJSON: jest.fn(), - log: jest.fn(), - CONFIG: { - model: 'mock-claude-model', - maxTokens: 4000, - temperature: 0.7, - debug: false - }, - truncate: jest.fn((text) => text) +jest.unstable_mockModule("../../../../../scripts/modules/utils.js", () => ({ + readJSON: jest.fn(), + writeJSON: jest.fn(), + log: jest.fn(), + CONFIG: { + model: "mock-claude-model", + maxTokens: 4000, + temperature: 0.7, + debug: false, + }, + truncate: jest.fn((text) => text), })); -jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ - displayBanner: jest.fn(), - getStatusWithColor: jest.fn((status) => status), - startLoadingIndicator: jest.fn(), - stopLoadingIndicator: jest.fn(), - displayAiUsageSummary: jest.fn() +jest.unstable_mockModule("../../../../../scripts/modules/ui.js", () => ({ + displayBanner: jest.fn(), + getStatusWithColor: jest.fn((status) => status), + startLoadingIndicator: jest.fn(), + stopLoadingIndicator: jest.fn(), + succeedLoadingIndicator: jest.fn(), + failLoadingIndicator: jest.fn(), + warnLoadingIndicator: jest.fn(), + infoLoadingIndicator: jest.fn(), + displayAiUsageSummary: jest.fn(), })); jest.unstable_mockModule( - '../../../../../scripts/modules/ai-services-unified.js', - () => ({ - generateObjectService: jest.fn().mockResolvedValue({ - mainResult: { - object: { - title: 'Task from prompt: Create a new authentication system', - description: - 'Task generated from: Create a new authentication system', - details: - 'Implementation details for task generated from prompt: Create a new authentication system', - testStrategy: 'Write unit tests to verify functionality', - dependencies: [] - } - }, - telemetryData: { - timestamp: new Date().toISOString(), - userId: '1234567890', - commandName: 'add-task', - modelUsed: 'claude-3-5-sonnet', - providerName: 'anthropic', - inputTokens: 1000, - outputTokens: 500, - totalTokens: 1500, - totalCost: 0.012414, - currency: 'USD' - } - }) - }) + "../../../../../scripts/modules/ai-services-unified.js", + () => ({ + generateObjectService: jest.fn().mockResolvedValue({ + mainResult: { + object: { + title: "Task from prompt: Create a new authentication system", + description: + "Task generated from: Create a new authentication system", + details: + "Implementation details for task generated from prompt: Create a new authentication system", + testStrategy: "Write unit tests to verify functionality", + dependencies: [], + }, + }, + telemetryData: { + timestamp: new Date().toISOString(), + userId: "1234567890", + commandName: "add-task", + modelUsed: "claude-3-5-sonnet", + providerName: "anthropic", + inputTokens: 1000, + outputTokens: 500, + totalTokens: 1500, + totalCost: 0.012414, + currency: "USD", + }, + }), + }) ); jest.unstable_mockModule( - '../../../../../scripts/modules/config-manager.js', - () => ({ - getDefaultPriority: jest.fn(() => 'medium') - }) + "../../../../../scripts/modules/config-manager.js", + () => ({ + getDefaultPriority: jest.fn(() => "medium"), + }) ); jest.unstable_mockModule( - '../../../../../scripts/modules/task-manager/generate-task-files.js', - () => ({ - default: jest.fn().mockResolvedValue() - }) + "../../../../../scripts/modules/task-manager/generate-task-files.js", + () => ({ + default: jest.fn().mockResolvedValue(), + }) ); // Mock external UI libraries -jest.unstable_mockModule('chalk', () => ({ - default: { - white: { bold: jest.fn((text) => text) }, - cyan: Object.assign( - jest.fn((text) => text), - { - bold: jest.fn((text) => text) - } - ), - green: jest.fn((text) => text), - yellow: jest.fn((text) => text), - bold: jest.fn((text) => text) - } +jest.unstable_mockModule("chalk", () => ({ + default: { + white: { bold: jest.fn((text) => text) }, + cyan: Object.assign( + jest.fn((text) => text), + { + bold: jest.fn((text) => text), + } + ), + green: jest.fn((text) => text), + yellow: jest.fn((text) => text), + bold: jest.fn((text) => text), + }, })); -jest.unstable_mockModule('boxen', () => ({ - default: jest.fn((text) => text) +jest.unstable_mockModule("boxen", () => ({ + default: jest.fn((text) => text), })); -jest.unstable_mockModule('cli-table3', () => ({ - default: jest.fn().mockImplementation(() => ({ - push: jest.fn(), - toString: jest.fn(() => 'mocked table') - })) +jest.unstable_mockModule("cli-table3", () => ({ + default: jest.fn().mockImplementation(() => ({ + push: jest.fn(), + toString: jest.fn(() => "mocked table"), + })), })); // Import the mocked modules const { readJSON, writeJSON, log } = await import( - '../../../../../scripts/modules/utils.js' + "../../../../../scripts/modules/utils.js" ); const { generateObjectService } = await import( - '../../../../../scripts/modules/ai-services-unified.js' + "../../../../../scripts/modules/ai-services-unified.js" ); const generateTaskFiles = await import( - '../../../../../scripts/modules/task-manager/generate-task-files.js' + "../../../../../scripts/modules/task-manager/generate-task-files.js" ); // Import the module under test const { default: addTask } = await import( - '../../../../../scripts/modules/task-manager/add-task.js' + "../../../../../scripts/modules/task-manager/add-task.js" ); -describe('addTask', () => { - const sampleTasks = { - tasks: [ - { - id: 1, - title: 'Task 1', - description: 'First task', - status: 'pending', - dependencies: [] - }, - { - id: 2, - title: 'Task 2', - description: 'Second task', - status: 'pending', - dependencies: [] - }, - { - id: 3, - title: 'Task 3', - description: 'Third task', - status: 'pending', - dependencies: [1] - } - ] - }; +describe("addTask", () => { + const sampleTasks = { + tasks: [ + { + id: 1, + title: "Task 1", + description: "First task", + status: "pending", + dependencies: [], + }, + { + id: 2, + title: "Task 2", + description: "Second task", + status: "pending", + dependencies: [], + }, + { + id: 3, + title: "Task 3", + description: "Third task", + status: "pending", + dependencies: [1], + }, + ], + }; - // Create a helper function for consistent mcpLog mock - const createMcpLogMock = () => ({ - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - success: jest.fn() - }); + // Create a helper function for consistent mcpLog mock + const createMcpLogMock = () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + success: jest.fn(), + }); - beforeEach(() => { - jest.clearAllMocks(); - readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks))); + beforeEach(() => { + jest.clearAllMocks(); + readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks))); - // Mock console.log to avoid output during tests - jest.spyOn(console, 'log').mockImplementation(() => {}); - }); + // Mock console.log to avoid output during tests + jest.spyOn(console, "log").mockImplementation(() => {}); + }); - afterEach(() => { - console.log.mockRestore(); - }); + afterEach(() => { + console.log.mockRestore(); + }); - test('should add a new task using AI', async () => { - // Arrange - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should add a new task using AI", async () => { + // Arrange + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act - const result = await addTask( - 'tasks/tasks.json', - prompt, - [], - 'medium', - context, - 'json' - ); + // Act + const result = await addTask( + "tasks/tasks.json", + prompt, + [], + "medium", + context, + "json" + ); - // Assert - expect(readJSON).toHaveBeenCalledWith('tasks/tasks.json'); - expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object)); - expect(writeJSON).toHaveBeenCalledWith( - 'tasks/tasks.json', - expect.objectContaining({ - tasks: expect.arrayContaining([ - expect.objectContaining({ - id: 4, // Next ID after existing tasks - title: expect.stringContaining( - 'Create a new authentication system' - ), - status: 'pending' - }) - ]) - }) - ); - expect(generateTaskFiles.default).toHaveBeenCalled(); - expect(result).toEqual( - expect.objectContaining({ - newTaskId: 4, - telemetryData: expect.any(Object) - }) - ); - }); + // Assert + expect(readJSON).toHaveBeenCalledWith("tasks/tasks.json"); + expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object)); + expect(writeJSON).toHaveBeenCalledWith( + "tasks/tasks.json", + expect.objectContaining({ + tasks: expect.arrayContaining([ + expect.objectContaining({ + id: 4, // Next ID after existing tasks + title: expect.stringContaining( + "Create a new authentication system" + ), + status: "pending", + }), + ]), + }) + ); + expect(generateTaskFiles.default).toHaveBeenCalled(); + expect(result).toEqual( + expect.objectContaining({ + newTaskId: 4, + telemetryData: expect.any(Object), + }) + ); + }); - test('should validate dependencies when adding a task', async () => { - // Arrange - const prompt = 'Create a new authentication system'; - const validDependencies = [1, 2]; // These exist in sampleTasks - const context = { - mcpLog: createMcpLogMock() - }; + test("should validate dependencies when adding a task", async () => { + // Arrange + const prompt = "Create a new authentication system"; + const validDependencies = [1, 2]; // These exist in sampleTasks + const context = { + mcpLog: createMcpLogMock(), + }; - // Act - const result = await addTask( - 'tasks/tasks.json', - prompt, - validDependencies, - 'medium', - context, - 'json' - ); + // Act + const result = await addTask( + "tasks/tasks.json", + prompt, + validDependencies, + "medium", + context, + "json" + ); - // Assert - expect(writeJSON).toHaveBeenCalledWith( - 'tasks/tasks.json', - expect.objectContaining({ - tasks: expect.arrayContaining([ - expect.objectContaining({ - id: 4, - dependencies: validDependencies - }) - ]) - }) - ); - }); + // Assert + expect(writeJSON).toHaveBeenCalledWith( + "tasks/tasks.json", + expect.objectContaining({ + tasks: expect.arrayContaining([ + expect.objectContaining({ + id: 4, + dependencies: validDependencies, + }), + ]), + }) + ); + }); - test('should filter out invalid dependencies', async () => { - // Arrange - const prompt = 'Create a new authentication system'; - const invalidDependencies = [999]; // Non-existent task ID - const context = { mcpLog: createMcpLogMock() }; + test("should filter out invalid dependencies", async () => { + // Arrange + const prompt = "Create a new authentication system"; + const invalidDependencies = [999]; // Non-existent task ID + const context = { mcpLog: createMcpLogMock() }; - // Act - const result = await addTask( - 'tasks/tasks.json', - prompt, - invalidDependencies, - 'medium', - context, - 'json' - ); + // Act + const result = await addTask( + "tasks/tasks.json", + prompt, + invalidDependencies, + "medium", + context, + "json" + ); - // Assert - expect(writeJSON).toHaveBeenCalledWith( - 'tasks/tasks.json', - expect.objectContaining({ - tasks: expect.arrayContaining([ - expect.objectContaining({ - id: 4, - dependencies: [] // Invalid dependencies should be filtered out - }) - ]) - }) - ); - expect(context.mcpLog.warn).toHaveBeenCalledWith( - expect.stringContaining( - 'The following dependencies do not exist or are invalid: 999' - ) - ); - }); + // Assert + expect(writeJSON).toHaveBeenCalledWith( + "tasks/tasks.json", + expect.objectContaining({ + tasks: expect.arrayContaining([ + expect.objectContaining({ + id: 4, + dependencies: [], // Invalid dependencies should be filtered out + }), + ]), + }) + ); + expect(context.mcpLog.warn).toHaveBeenCalledWith( + expect.stringContaining( + "The following dependencies do not exist or are invalid: 999" + ) + ); + }); - test('should use specified priority', async () => { - // Arrange - const prompt = 'Create a new authentication system'; - const priority = 'high'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should use specified priority", async () => { + // Arrange + const prompt = "Create a new authentication system"; + const priority = "high"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act - await addTask('tasks/tasks.json', prompt, [], priority, context, 'json'); + // Act + await addTask("tasks/tasks.json", prompt, [], priority, context, "json"); - // Assert - expect(writeJSON).toHaveBeenCalledWith( - 'tasks/tasks.json', - expect.objectContaining({ - tasks: expect.arrayContaining([ - expect.objectContaining({ - priority: priority - }) - ]) - }) - ); - }); + // Assert + expect(writeJSON).toHaveBeenCalledWith( + "tasks/tasks.json", + expect.objectContaining({ + tasks: expect.arrayContaining([ + expect.objectContaining({ + priority: priority, + }), + ]), + }) + ); + }); - test('should handle empty tasks file', async () => { - // Arrange - readJSON.mockReturnValue({ tasks: [] }); - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should handle empty tasks file", async () => { + // Arrange + readJSON.mockReturnValue({ tasks: [] }); + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act - const result = await addTask( - 'tasks/tasks.json', - prompt, - [], - 'medium', - context, - 'json' - ); + // Act + const result = await addTask( + "tasks/tasks.json", + prompt, + [], + "medium", + context, + "json" + ); - // Assert - expect(result.newTaskId).toBe(1); // First task should have ID 1 - expect(writeJSON).toHaveBeenCalledWith( - 'tasks/tasks.json', - expect.objectContaining({ - tasks: expect.arrayContaining([ - expect.objectContaining({ - id: 1 - }) - ]) - }) - ); - }); + // Assert + expect(result.newTaskId).toBe(1); // First task should have ID 1 + expect(writeJSON).toHaveBeenCalledWith( + "tasks/tasks.json", + expect.objectContaining({ + tasks: expect.arrayContaining([ + expect.objectContaining({ + id: 1, + }), + ]), + }) + ); + }); - test('should handle missing tasks file', async () => { - // Arrange - readJSON.mockReturnValue(null); - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should handle missing tasks file", async () => { + // Arrange + readJSON.mockReturnValue(null); + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act - const result = await addTask( - 'tasks/tasks.json', - prompt, - [], - 'medium', - context, - 'json' - ); + // Act + const result = await addTask( + "tasks/tasks.json", + prompt, + [], + "medium", + context, + "json" + ); - // Assert - expect(result.newTaskId).toBe(1); // First task should have ID 1 - expect(writeJSON).toHaveBeenCalledTimes(2); // Once to create file, once to add task - }); + // Assert + expect(result.newTaskId).toBe(1); // First task should have ID 1 + expect(writeJSON).toHaveBeenCalledTimes(2); // Once to create file, once to add task + }); - test('should handle AI service errors', async () => { - // Arrange - generateObjectService.mockRejectedValueOnce(new Error('AI service failed')); - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should handle AI service errors", async () => { + // Arrange + generateObjectService.mockRejectedValueOnce(new Error("AI service failed")); + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act & Assert - await expect( - addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json') - ).rejects.toThrow('AI service failed'); - }); + // Act & Assert + await expect( + addTask("tasks/tasks.json", prompt, [], "medium", context, "json") + ).rejects.toThrow("AI service failed"); + }); - test('should handle file read errors', async () => { - // Arrange - readJSON.mockImplementation(() => { - throw new Error('File read failed'); - }); - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should handle file read errors", async () => { + // Arrange + readJSON.mockImplementation(() => { + throw new Error("File read failed"); + }); + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act & Assert - await expect( - addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json') - ).rejects.toThrow('File read failed'); - }); + // Act & Assert + await expect( + addTask("tasks/tasks.json", prompt, [], "medium", context, "json") + ).rejects.toThrow("File read failed"); + }); - test('should handle file write errors', async () => { - // Arrange - writeJSON.mockImplementation(() => { - throw new Error('File write failed'); - }); - const prompt = 'Create a new authentication system'; - const context = { - mcpLog: createMcpLogMock() - }; + test("should handle file write errors", async () => { + // Arrange + writeJSON.mockImplementation(() => { + throw new Error("File write failed"); + }); + const prompt = "Create a new authentication system"; + const context = { + mcpLog: createMcpLogMock(), + }; - // Act & Assert - await expect( - addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json') - ).rejects.toThrow('File write failed'); - }); + // Act & Assert + await expect( + addTask("tasks/tasks.json", prompt, [], "medium", context, "json") + ).rejects.toThrow("File write failed"); + }); }); diff --git a/tests/unit/ui.test.js b/tests/unit/ui.test.js index dbab8ea8..02b869f0 100644 --- a/tests/unit/ui.test.js +++ b/tests/unit/ui.test.js @@ -82,19 +82,19 @@ describe("UI Module", () => { test("should return done status with emoji for console output", () => { const result = getStatusWithColor("done"); expect(result).toMatch(/done/); - expect(result).toContain("✅"); + expect(result).toContain("✓"); }); test("should return pending status with emoji for console output", () => { const result = getStatusWithColor("pending"); expect(result).toMatch(/pending/); - expect(result).toContain("⏱️"); + expect(result).toContain("○"); }); test("should return deferred status with emoji for console output", () => { const result = getStatusWithColor("deferred"); expect(result).toMatch(/deferred/); - expect(result).toContain("⏱️"); + expect(result).toContain("x"); }); test("should return in-progress status with emoji for console output", () => {