From 9b87dd23de0b9095256a2d7b4802b92ce4b0a2e5 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Sat, 31 May 2025 19:47:18 -0400 Subject: [PATCH] fix(gateway/auth): Implement proper auth/init flow with automatic background userId generation - Fix getUserId() to use placeholder that triggers auth/init if the auth/init endpoint is down for whatever reason - Add silent auth/init attempt in AI services - Improve hosted mode error handling - Remove fake userId/email generation from init.js --- .taskmasterconfig | 6 +- package-lock.json | 134 + package.json | 1 + scripts/init.js | 573 +- scripts/modules/ai-services-unified.js | 105 +- scripts/modules/commands.js | 6066 ++++++++--------- scripts/modules/config-manager.js | 27 +- scripts/modules/task-manager/add-task.js | 2262 +++--- scripts/modules/user-management.js | 69 +- tasks/tasks.json | 13 + .../modules/telemetry-enhancements.test.js | 1 + 11 files changed, 4699 insertions(+), 4558 deletions(-) diff --git a/.taskmasterconfig b/.taskmasterconfig index f20347cd..7de2eb02 100644 --- a/.taskmasterconfig +++ b/.taskmasterconfig @@ -29,9 +29,9 @@ "azureBaseURL": "https://your-endpoint.azure.com/" }, "account": { - "userId": "277779c9-1ee2-4ef8-aa3a-2176745b71a9", - "userEmail": "user_1748640077834@taskmaster.dev", - "mode": "hosted", + "userId": "1234567890", + "userEmail": "", + "mode": "byok", "telemetryEnabled": true } } diff --git a/package-lock.json b/package-lock.json index 3d4629ff..bab68564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", "ollama-ai-provider": "^1.2.0", + "open": "^10.1.2", "openai": "^4.89.0", "ora": "^8.2.0", "task-master-ai": "^0.15.0", @@ -5423,6 +5424,21 @@ "dev": true, "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6192,6 +6208,46 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8030,6 +8086,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8088,6 +8159,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -8176,6 +8265,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9933,6 +10037,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.89.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.89.0.tgz", @@ -10706,6 +10828,18 @@ "node": ">=16" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", diff --git a/package.json b/package.json index e1599c28..3d04e943 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", "ollama-ai-provider": "^1.2.0", + "open": "^10.1.2", "openai": "^4.89.0", "ora": "^8.2.0", "task-master-ai": "^0.15.0", diff --git a/scripts/init.js b/scripts/init.js index c207e459..5ade3583 100755 --- a/scripts/init.js +++ b/scripts/init.js @@ -22,10 +22,16 @@ import chalk from "chalk"; import figlet from "figlet"; import boxen from "boxen"; import gradient from "gradient-string"; +import inquirer from "inquirer"; +import open from "open"; +import express from "express"; import { isSilentMode } from "./modules/utils.js"; import { convertAllCursorRulesToRooRules } from "./modules/rule-transformer.js"; import { execSync } from "child_process"; -import { registerUserWithGateway } from "./modules/telemetry-submission.js"; +import { + initializeUser, + registerUserWithGateway, +} from "./modules/user-management.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -379,46 +385,51 @@ async function initializeProject(options = {}) { }; } - // STEP 1: Create/find userId first (MCP/non-interactive mode) - let userId = null; - let gatewayRegistration = null; + // NON-INTERACTIVE MODE - Use proper auth/init flow + let userSetupResult; try { - // Try to get existing userId from config if it exists + // Check if existing config has userId const existingConfigPath = path.join(process.cwd(), ".taskmasterconfig"); + let existingUserId = null; + if (fs.existsSync(existingConfigPath)) { const existingConfig = JSON.parse( fs.readFileSync(existingConfigPath, "utf8") ); - userId = existingConfig.account?.userId; - const existingUserEmail = existingConfig.account?.userEmail; + existingUserId = existingConfig.account?.userId; + } - // Pass existing data to gateway for validation/lookup - gatewayRegistration = await registerUserWithGateway( - existingUserEmail || tempEmail, - userId - ); - - if (gatewayRegistration.success) { - userId = gatewayRegistration.userId; - } else { - // Generate fallback userId if gateway unavailable - userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + if (existingUserId) { + // Validate existing userId through auth/init + userSetupResult = await registerUserWithGateway(null, process.cwd()); + if (!userSetupResult.success) { + throw new Error( + `Failed to validate existing user: ${userSetupResult.error}` + ); + } + } else { + // Create new user through auth/init + userSetupResult = await initializeUser(process.cwd()); + if (!userSetupResult.success) { + throw new Error( + `Failed to initialize user: ${userSetupResult.error}` + ); } } } catch (error) { - // Generate fallback userId on any error - userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + log("error", `User initialization failed: ${error.message}`); + throw error; // Don't fall back to random userId! } - // For non-interactive mode, default to BYOK mode with proper userId + // Create project structure with properly authenticated userId createProjectStructure( addAliases, dryRun, - gatewayRegistration, - "byok", + userSetupResult, // Pass the full auth result + "byok", // or determine from result null, - userId + userSetupResult.userId || null ); } else { // Interactive logic - NEW FLOW STARTS HERE @@ -444,127 +455,124 @@ async function initializeProject(options = {}) { ) ); - // Generate or retrieve userId from gateway - let userId = null; - let gatewayRegistration = null; + // INTERACTIVE MODE - Also use proper auth/init flow + // STEP 1: Proper user setup + let userSetupResult; try { - // Try to get existing userId from config if it exists + // Same logic as non-interactive mode const existingConfigPath = path.join( process.cwd(), ".taskmasterconfig" ); + let existingUserId = null; + if (fs.existsSync(existingConfigPath)) { const existingConfig = JSON.parse( fs.readFileSync(existingConfigPath, "utf8") ); - userId = existingConfig.account?.userId; - const existingUserEmail = existingConfig.account?.userEmail; + existingUserId = existingConfig.account?.userId; + } - // Pass existing data to gateway for validation/lookup - gatewayRegistration = await registerUserWithGateway( - existingUserEmail || tempEmail, - userId - ); - - if (gatewayRegistration.success) { - userId = gatewayRegistration.userId; - } else { - // Generate fallback userId if gateway unavailable - userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + if (existingUserId) { + userSetupResult = await registerUserWithGateway(null, process.cwd()); + if (!userSetupResult.success) { + throw new Error( + `Failed to validate existing user: ${userSetupResult.error}` + ); + } + } else { + userSetupResult = await initializeUser(process.cwd()); + if (!userSetupResult.success) { + throw new Error( + `Failed to initialize user: ${userSetupResult.error}` + ); } } } catch (error) { - // Generate fallback userId on any error - userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + log("error", `User initialization failed: ${error.message}`); + // Don't fall back to random userId - exit or prompt user + throw error; } - // STEP 2: Choose AI access method (MAIN DECISION) + // STEP 2: Choose AI access method using inquirer + const modeResponse = await inquirer.prompt([ + { + type: "list", + name: "accessMode", + message: "Choose Your AI Access Method:", + choices: [ + { + name: "šŸ”‘ BYOK - Bring Your Own API Keys (You manage API keys & billing)", + value: "byok", + }, + { + name: "šŸŽÆ Hosted API Gateway - All models, no keys needed (Recommended)", + value: "hosted", + }, + ], + default: "hosted", + }, + ]); + + const selectedMode = modeResponse.accessMode; + console.log( boxen( - chalk.white.bold("Choose Your AI Access Method") + - "\n\n" + - chalk.cyan.bold("(1) BYOK - Bring Your Own API Keys") + - "\n" + - chalk.white( - " → You manage API keys & billing with AI providers" - ) + - "\n" + - chalk.white(" → Pay provider directly based on token usage") + - "\n" + - chalk.white( - " → Requires setup with each provider individually" - ) + - "\n\n" + - chalk.green.bold("(2) Hosted API Gateway") + - " " + - chalk.yellow.bold("(Recommended)") + - "\n" + - chalk.white(" → Use any model, zero API keys needed") + - "\n" + - chalk.white(" → Flat, credit-based pricing with no surprises") + - "\n" + - chalk.white(" → Support the development of Taskmaster"), - { - padding: 1, - margin: { top: 1, bottom: 1 }, - borderStyle: "round", - borderColor: "cyan", - title: "šŸŽÆ AI Access Setup", - titleAlignment: "center", - } - ) - ); - - let choice; - while (true) { - choice = await promptQuestion( - rl, - chalk.cyan.bold("Your choice (1 or 2): ") - ); - - if (choice === "1" || choice.toLowerCase() === "byok") { - console.log( - boxen( - chalk.blue.bold("šŸ”‘ BYOK Mode Selected") + + selectedMode === "byok" + ? chalk.blue.bold("šŸ”‘ BYOK Mode Selected") + "\n\n" + chalk.white("You'll manage your own API keys and billing.") + "\n" + chalk.white("After setup, add your API keys to ") + chalk.cyan(".env") + - chalk.white(" file."), - { - padding: 1, - margin: { top: 1, bottom: 1 }, - borderStyle: "round", - borderColor: "blue", - } - ) - ); - return "byok"; - } else if (choice === "2" || choice.toLowerCase() === "hosted") { - console.log( - boxen( - chalk.green.bold("šŸŽÆ Hosted API Gateway Selected") + + chalk.white(" file.") + : chalk.green.bold("šŸŽÆ Hosted API Gateway Selected") + "\n\n" + chalk.white( "All AI models available instantly - no API keys needed!" ) + "\n" + chalk.dim("Let's set up your subscription plan..."), - { - padding: 0.5, - margin: { top: 0.5, bottom: 0.5 }, - borderStyle: "round", - borderColor: "green", - } - ) - ); - return "hosted"; - } else { - console.log(chalk.red("Please enter 1 or 2")); - } + { + padding: 1, + margin: { top: 1, bottom: 1 }, + borderStyle: "round", + borderColor: selectedMode === "byok" ? "blue" : "green", + } + ) + ); + + // STEP 3: If hosted mode, handle subscription plan with Stripe simulation + let selectedPlan = null; + if (selectedMode === "hosted") { + selectedPlan = await handleHostedSubscription(); } + + // STEP 4: Continue with aliases (this fixes the hanging issue) + const aliasResponse = await inquirer.prompt([ + { + type: "confirm", + name: "addAliases", + message: "Add shell aliases (tm, taskmaster) for easier access?", + default: true, + }, + ]); + + const addAliases = aliasResponse.addAliases; + + const dryRun = options.dryRun || false; + + // STEP 5: Show overview and continue with project creation + rl.close(); + createProjectStructure( + addAliases, + dryRun, + userSetupResult, + selectedMode, + selectedPlan, + userSetupResult.userId + ); } catch (error) { rl.close(); log("error", `Error during initialization process: ${error.message}`); @@ -1137,251 +1145,132 @@ function setupMCPConfiguration(targetDir) { log("info", "MCP server will use the installed task-master-ai package"); } -// Function to let user choose between BYOK and Hosted API Gateway -async function selectAccessMode() { +// Function to handle hosted subscription with browser pattern +async function handleHostedSubscription() { + const planResponse = await inquirer.prompt([ + { + type: "list", + name: "plan", + message: "Select Your Monthly AI Credit Pack:", + choices: [ + { + name: "50 credits - $5/mo [$0.10 per credit] - Perfect for personal projects", + value: { name: "Starter", credits: 50, price: "$5/mo", value: 1 }, + }, + { + name: "120 credits - $10/mo [$0.083 per credit] - Popular choice", + value: { name: "Popular", credits: 120, price: "$10/mo", value: 2 }, + }, + { + name: "250 credits - $20/mo [$0.08 per credit] - Great value", + value: { name: "Pro", credits: 250, price: "$20/mo", value: 3 }, + }, + { + name: "550 credits - $40/mo [$0.073 per credit] - Best value", + value: { + name: "Enterprise", + credits: 550, + price: "$40/mo", + value: 4, + }, + }, + ], + default: 1, // Popular plan + }, + ]); + + const selectedPlan = planResponse.plan; + console.log( boxen( - chalk.cyan.bold("šŸš€ Choose Your AI Access Method") + + chalk.green.bold(`āœ… Selected: ${selectedPlan.name} Plan`) + "\n\n" + - chalk.white("TaskMaster supports two ways to access AI models:") + + chalk.white( + `${selectedPlan.credits} credits/month for ${selectedPlan.price}` + ) + "\n\n" + - chalk.yellow.bold("(1) BYOK - Bring Your Own API Keys") + + chalk.yellow("šŸ”„ Opening browser for Stripe checkout...") + "\n" + - chalk.white(" āœ“ Use your existing provider accounts") + - "\n" + - chalk.white(" āœ“ Pay providers directly") + - "\n" + - chalk.white(" āœ“ Full control over billing & usage") + - "\n" + - chalk.dim(" → Best for: Teams with existing AI accounts") + - "\n\n" + - chalk.green.bold("(2) Hosted API Gateway") + - chalk.yellow(" (Recommended)") + - "\n" + - chalk.white(" āœ“ No API keys required") + - "\n" + - chalk.white(" āœ“ Access all supported models instantly") + - "\n" + - chalk.white(" āœ“ Simple credit-based billing") + - "\n" + - chalk.white(" āœ“ Better rates through volume pricing") + - "\n" + - chalk.dim(" → Best for: Getting started quickly"), + chalk.dim("Complete your subscription setup in the browser."), { padding: 1, - margin: { top: 1, bottom: 1 }, - borderStyle: "round", - borderColor: "cyan", - title: "šŸŽÆ AI Access Configuration", - titleAlignment: "center", - } - ) - ); - - let choice; - while (true) { - choice = await promptQuestion( - rl, - chalk.cyan("Your choice") + - chalk.gray(" (1 for BYOK, 2 for Hosted)") + - ": " - ); - - if (choice === "1" || choice.toLowerCase() === "byok") { - console.log( - boxen( - chalk.blue.bold("šŸ”‘ BYOK Mode Selected") + - "\n\n" + - chalk.white("You'll configure your own AI provider API keys.") + - "\n" + - chalk.dim("The setup will guide you through model configuration."), - { - padding: 0.5, - margin: { top: 0.5, bottom: 0.5 }, - borderStyle: "round", - borderColor: "blue", - } - ) - ); - return "byok"; - } else if (choice === "2" || choice.toLowerCase() === "hosted") { - console.log( - boxen( - chalk.green.bold("šŸŽÆ Hosted API Gateway Selected") + - "\n\n" + - chalk.white( - "All AI models available instantly - no API keys needed!" - ) + - "\n" + - chalk.dim("Let's set up your subscription plan..."), - { - padding: 0.5, - margin: { top: 0.5, bottom: 0.5 }, - borderStyle: "round", - borderColor: "green", - } - ) - ); - return "hosted"; - } else { - console.log(chalk.red("Please enter 1 or 2")); - } - } -} - -// Function to let user select a subscription plan -async function selectSubscriptionPlan() { - console.log( - boxen( - chalk.cyan.bold("šŸ’³ Select Your Monthly AI Credit Pack") + - "\n\n" + - chalk.white("Choose the plan that fits your usage:") + - "\n\n" + - chalk.white("(1) ") + - chalk.yellow.bold("50 credits") + - chalk.white(" - ") + - chalk.green("$5/mo") + - chalk.gray(" [$0.10 per credit]") + - "\n" + - chalk.dim(" → Perfect for: Personal projects, light usage") + - "\n\n" + - chalk.white("(2) ") + - chalk.yellow.bold("120 credits") + - chalk.white(" - ") + - chalk.green("$10/mo") + - chalk.gray(" [$0.083 per credit]") + - chalk.cyan.bold(" ← Popular") + - "\n" + - chalk.dim(" → Perfect for: Active development, small teams") + - "\n\n" + - chalk.white("(3) ") + - chalk.yellow.bold("250 credits") + - chalk.white(" - ") + - chalk.green("$20/mo") + - chalk.gray(" [$0.08 per credit]") + - chalk.blue.bold(" ← Great Value") + - "\n" + - chalk.dim(" → Perfect for: Professional development, medium teams") + - "\n\n" + - chalk.white("(4) ") + - chalk.yellow.bold("550 credits") + - chalk.white(" - ") + - chalk.green("$40/mo") + - chalk.gray(" [$0.073 per credit]") + - chalk.magenta.bold(" ← Best Value") + - "\n" + - chalk.dim(" → Perfect for: Heavy usage, large teams, enterprises") + - "\n\n" + - chalk.blue("šŸ’” ") + - chalk.white("Credits roll over month-to-month. Cancel anytime."), - { - padding: 1, - margin: { top: 1, bottom: 1 }, + margin: { top: 0.5, bottom: 0.5 }, borderStyle: "round", borderColor: "green", - title: "šŸ’³ Subscription Plans", - titleAlignment: "center", } ) ); - const plans = [ - { - name: "Starter", - credits: 50, - price: "$5/mo", - perCredit: "$0.10", - value: 1, - }, - { - name: "Popular", - credits: 120, - price: "$10/mo", - perCredit: "$0.083", - value: 2, - }, - { - name: "Pro", - credits: 250, - price: "$20/mo", - perCredit: "$0.08", - value: 3, - }, - { - name: "Enterprise", - credits: 550, - price: "$40/mo", - perCredit: "$0.073", - value: 4, - }, - ]; + // Stripe simulation with browser opening pattern (like Shopify CLI) + await simulateStripeCheckout(selectedPlan); - let choice; - while (true) { - choice = await promptQuestion( - rl, - chalk.cyan("Your choice") + chalk.gray(" (1-4)") + ": " - ); - - const planIndex = parseInt(choice) - 1; - if (planIndex >= 0 && planIndex < plans.length) { - const selectedPlan = plans[planIndex]; - - console.log( - boxen( - chalk.green.bold(`āœ… Selected: ${selectedPlan.name} Plan`) + - "\n\n" + - chalk.white( - `${selectedPlan.credits} credits/month for ${selectedPlan.price}` - ) + - "\n" + - chalk.gray(`(${selectedPlan.perCredit} per credit)`) + - "\n\n" + - chalk.yellow("šŸ”„ Opening Stripe checkout...") + - "\n" + - chalk.dim("Complete your subscription setup in the browser."), - { - padding: 1, - margin: { top: 0.5, bottom: 0.5 }, - borderStyle: "round", - borderColor: "green", - } - ) - ); - - // TODO: Integrate with actual Stripe checkout - // For now, simulate the process - console.log(chalk.yellow("\nā³ Simulating Stripe checkout process...")); - console.log(chalk.green("āœ… Subscription setup complete! (Simulated)")); - - return selectedPlan; - } else { - console.log(chalk.red("Please enter a number from 1 to 4")); - } - } + return selectedPlan; } -// Function to create or retrieve user ID -async function getOrCreateUserId() { - // Try to find existing userId first - const existingConfig = path.join(process.cwd(), ".taskmasterconfig"); - if (fs.existsSync(existingConfig)) { - try { - const config = JSON.parse(fs.readFileSync(existingConfig, "utf8")); - if (config.userId) { - log("info", `Using existing user ID: ${config.userId}`); - return config.userId; - } - } catch (error) { - log("warn", "Could not read existing config, creating new user ID"); - } +// Stripe checkout simulation with browser pattern +async function simulateStripeCheckout(plan) { + console.log(chalk.yellow("\nā³ Starting Stripe checkout process...")); + + // Start a simple HTTP server to handle the callback + const app = express(); + let server; + let checkoutComplete = false; + + // For demo/testing, we'll use a simple success simulation + const checkoutUrl = `https://example-stripe-simulation.com/checkout?plan=${plan.value}&return_url=http://localhost:3333/success`; + + app.get("/success", (req, res) => { + checkoutComplete = true; + res.send(` + + +

āœ… Subscription Complete!

+

Your ${plan.name} plan (${plan.credits} credits/month) is now active.

+

You can close this window and return to your terminal.

+ + + `); + setTimeout(() => { + server.close(); + }, 1000); + }); + + // Start the callback server + server = app.listen(3333, () => { + console.log(chalk.blue("šŸ“” Started local callback server on port 3333")); + }); + + // Prompt user before opening browser + await inquirer.prompt([ + { + type: "input", + name: "ready", + message: chalk.cyan( + "Press Enter to open your browser for Stripe checkout..." + ), + }, + ]); + + // Open the browser (for demo, we'll simulate immediate success) + console.log(chalk.blue("🌐 Opening browser...")); + + // For demo purposes, simulate immediate success instead of opening real browser + // In real implementation: await open(checkoutUrl); + console.log(chalk.gray(`Demo URL: ${checkoutUrl}`)); + + // Simulate the checkout completion after 2 seconds + setTimeout(() => { + console.log(chalk.green("āœ… Subscription setup complete! (Simulated)")); + checkoutComplete = true; + server.close(); + }, 2000); + + // Wait for checkout completion + while (!checkoutComplete) { + await new Promise((resolve) => setTimeout(resolve, 500)); } - // Generate new user ID - const { v4: uuidv4 } = require("uuid"); - const newUserId = uuidv4(); - log("info", `Generated new user ID: ${newUserId}`); - return newUserId; + console.log(chalk.green("šŸŽ‰ Payment successful! Continuing setup...")); } // Ensure necessary functions are exported diff --git a/scripts/modules/ai-services-unified.js b/scripts/modules/ai-services-unified.js index 6a71459b..a81ae84e 100644 --- a/scripts/modules/ai-services-unified.js +++ b/scripts/modules/ai-services-unified.js @@ -278,6 +278,7 @@ async function _attemptProviderCallWithRetries( * @param {string} commandName - Command name for tracking * @param {string} outputType - Output type (cli, mcp) * @param {string} projectRoot - Project root path + * @param {string} initialRole - The initial client role * @returns {Promise} AI response with usage data */ async function _callGatewayAI( @@ -288,36 +289,62 @@ async function _callGatewayAI( userId, commandName, outputType, - projectRoot + projectRoot, + initialRole ) { - const gatewayUrl = - process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444"; - const endpoint = `${gatewayUrl}/api/v1/ai/${serviceType}`; + // Hard-code service-level constants + const gatewayUrl = "http://localhost:4444"; // or your production URL + const serviceApiKey = "339a81c9-5b9c-4d60-92d8-cba2ee2a8cc3"; // Hardcoded service key -- if you change this, the Hosted Gateway will not work - // Get API key from env - const apiKey = resolveEnvVariable("TASKMASTER_API_KEY", null, projectRoot); - if (!apiKey) { - throw new Error("TASKMASTER_API_KEY not found for hosted mode"); + // Get user auth info for headers + const userMgmt = require("./user-management.js"); + const userToken = await userMgmt.getUserToken(projectRoot); + const userEmail = await userMgmt.getUserEmail(projectRoot); + + if (!userToken) { + throw new Error( + "User token not found. Run 'task-master init' to register with gateway." + ); } - // need to make sure the user is authenticated and has a valid paid user token + enough credits for this call + const endpoint = `${gatewayUrl}/api/v1/ai/${serviceType}`; + + // Extract messages from callParams and convert to gateway format + const systemPrompt = + callParams.messages?.find((m) => m.role === "system")?.content || ""; + const prompt = + callParams.messages?.find((m) => m.role === "user")?.content || ""; const requestBody = { - provider: providerName, - model: modelId, - serviceType, - userId, + role: initialRole, + messages: callParams.messages, + modelId, commandName, outputType, - ...callParams, + roleParams: { + maxTokens: callParams.maxTokens, + temperature: callParams.temperature, + }, + ...(serviceType === "generateObject" && { + schema: callParams.schema, + objectName: callParams.objectName, + }), }; + const headers = { + "Content-Type": "application/json", + "X-TaskMaster-API-Key": serviceApiKey, // Service-level auth (hardcoded) + Authorization: `Bearer ${userToken}`, // User-level auth + }; + + // Add user email header if available + if (userEmail) { + headers["X-User-Email"] = userEmail; + } + const response = await fetch(endpoint, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, + headers, body: JSON.stringify(requestBody), }); @@ -383,14 +410,45 @@ async function _unifiedServiceRunner(serviceType, params) { const effectiveProjectRoot = projectRoot || findProjectRoot(); const userId = getUserId(effectiveProjectRoot); - // Check if user is in hosted mode + // If userId is the placeholder, try to initialize user silently + if (userId === "1234567890") { + try { + // Dynamic import to avoid circular dependency + const userMgmt = await import("./user-management.js"); + const initResult = await userMgmt.initializeUser(effectiveProjectRoot); + + if (initResult.success) { + // Update the config with the new userId + const { writeConfig, getConfig } = await import("./config-manager.js"); + const config = getConfig(effectiveProjectRoot); + config.account.userId = initResult.userId; + writeConfig(config, effectiveProjectRoot); + + log("info", "User successfully authenticated with gateway"); + } else { + log("warn", `Silent auth/init failed: ${initResult.error}`); + } + } catch (error) { + log("warn", `Silent auth/init attempt failed: ${error.message}`); + } + } + + // Add hosted mode check here const hostedMode = isHostedMode(effectiveProjectRoot); if (hostedMode) { - // For hosted mode, route through gateway + // Route through gateway - use your existing implementation log("info", "Routing AI call through TaskMaster gateway (hosted mode)"); try { + // Check if we have a valid userId (not placeholder) + const finalUserId = getUserId(effectiveProjectRoot); // Re-check after potential auth + if (finalUserId === "1234567890" || !finalUserId) { + throw new Error( + "Hosted mode requires user authentication. Please run 'task-master init' to register with the gateway, or switch to BYOK mode if the gateway service is unavailable." + ); + } + // Get the role configuration for provider/model selection let providerName, modelId; if (initialRole === "main") { @@ -442,10 +500,11 @@ async function _unifiedServiceRunner(serviceType, params) { callParams, providerName, modelId, - userId, + finalUserId, commandName, outputType, - effectiveProjectRoot + effectiveProjectRoot, + initialRole ); // For hosted mode, we don't need to submit telemetry separately @@ -455,7 +514,7 @@ async function _unifiedServiceRunner(serviceType, params) { // Convert gateway account info to telemetry format for UI display telemetryData = { timestamp: new Date().toISOString(), - userId, + userId: finalUserId, commandName, modelUsed: modelId, providerName, diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 315d4636..d1ed91ff 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -3,624 +3,624 @@ * Command-line interface for the Task Master CLI */ -import { program } from 'commander'; -import path from 'path'; -import chalk from 'chalk'; -import boxen from 'boxen'; -import fs from 'fs'; -import https from 'https'; -import http from 'http'; -import inquirer from 'inquirer'; -import ora from 'ora'; // Import ora +import { program } from "commander"; +import path from "path"; +import chalk from "chalk"; +import boxen from "boxen"; +import fs from "fs"; +import https from "https"; +import http from "http"; +import inquirer from "inquirer"; +import ora from "ora"; // Import ora -import { log, readJSON, findProjectRoot } from './utils.js'; +import { log, readJSON, findProjectRoot } from "./utils.js"; import { - parsePRD, - updateTasks, - generateTaskFiles, - setTaskStatus, - listTasks, - expandTask, - expandAllTasks, - clearSubtasks, - addTask, - addSubtask, - removeSubtask, - analyzeTaskComplexity, - updateTaskById, - updateSubtaskById, - removeTask, - findTaskById, - taskExists, - moveTask -} from './task-manager.js'; + parsePRD, + updateTasks, + generateTaskFiles, + setTaskStatus, + listTasks, + expandTask, + expandAllTasks, + clearSubtasks, + addTask, + addSubtask, + removeSubtask, + analyzeTaskComplexity, + updateTaskById, + updateSubtaskById, + removeTask, + findTaskById, + taskExists, + moveTask, +} from "./task-manager.js"; import { - addDependency, - removeDependency, - validateDependenciesCommand, - fixDependenciesCommand -} from './dependency-manager.js'; + addDependency, + removeDependency, + validateDependenciesCommand, + fixDependenciesCommand, +} from "./dependency-manager.js"; import { - isApiKeySet, - getDebugFlag, - getConfig, - writeConfig, - ConfigurationError, - isConfigFilePresent, - getAvailableModels, - getBaseUrlForRole -} from './config-manager.js'; + isApiKeySet, + getDebugFlag, + getConfig, + writeConfig, + ConfigurationError, + isConfigFilePresent, + getAvailableModels, + getBaseUrlForRole, +} from "./config-manager.js"; import { - displayBanner, - displayHelp, - displayNextTask, - displayTaskById, - displayComplexityReport, - getStatusWithColor, - confirmTaskOverwrite, - startLoadingIndicator, - stopLoadingIndicator, - displayModelConfiguration, - displayAvailableModels, - displayApiKeyStatus, - displayAiUsageSummary, - displayMultipleTasksSummary -} from './ui.js'; + displayBanner, + displayHelp, + displayNextTask, + displayTaskById, + displayComplexityReport, + getStatusWithColor, + confirmTaskOverwrite, + startLoadingIndicator, + stopLoadingIndicator, + displayModelConfiguration, + displayAvailableModels, + displayApiKeyStatus, + displayAiUsageSummary, + displayMultipleTasksSummary, +} from "./ui.js"; -import { initializeProject } from '../init.js'; +import { initializeProject } from "../init.js"; import { - getModelConfiguration, - getAvailableModelsList, - setModel, - getApiKeyStatusReport -} from './task-manager/models.js'; + getModelConfiguration, + getAvailableModelsList, + setModel, + getApiKeyStatusReport, +} from "./task-manager/models.js"; import { - isValidTaskStatus, - TASK_STATUS_OPTIONS -} from '../../src/constants/task-status.js'; -import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; + isValidTaskStatus, + TASK_STATUS_OPTIONS, +} from "../../src/constants/task-status.js"; +import { getTaskMasterVersion } from "../../src/utils/getVersion.js"; /** * Runs the interactive setup process for model configuration. * @param {string|null} projectRoot - The resolved project root directory. */ async function runInteractiveSetup(projectRoot) { - if (!projectRoot) { - console.error( - chalk.red( - 'Error: Could not determine project root for interactive setup.' - ) - ); - process.exit(1); - } + if (!projectRoot) { + console.error( + chalk.red( + "Error: Could not determine project root for interactive setup." + ) + ); + process.exit(1); + } - const currentConfigResult = await getModelConfiguration({ projectRoot }); - const currentModels = currentConfigResult.success - ? currentConfigResult.data.activeModels - : { main: null, research: null, fallback: null }; - // Handle potential config load failure gracefully for the setup flow - if ( - !currentConfigResult.success && - currentConfigResult.error?.code !== 'CONFIG_MISSING' - ) { - console.warn( - chalk.yellow( - `Warning: Could not load current model configuration: ${currentConfigResult.error?.message || 'Unknown error'}. Proceeding with defaults.` - ) - ); - } + const currentConfigResult = await getModelConfiguration({ projectRoot }); + const currentModels = currentConfigResult.success + ? currentConfigResult.data.activeModels + : { main: null, research: null, fallback: null }; + // Handle potential config load failure gracefully for the setup flow + if ( + !currentConfigResult.success && + currentConfigResult.error?.code !== "CONFIG_MISSING" + ) { + console.warn( + chalk.yellow( + `Warning: Could not load current model configuration: ${currentConfigResult.error?.message || "Unknown error"}. Proceeding with defaults.` + ) + ); + } - // Helper function to fetch OpenRouter models (duplicated for CLI context) - function fetchOpenRouterModelsCLI() { - return new Promise((resolve) => { - const options = { - hostname: 'openrouter.ai', - path: '/api/v1/models', - method: 'GET', - headers: { - Accept: 'application/json' - } - }; + // Helper function to fetch OpenRouter models (duplicated for CLI context) + function fetchOpenRouterModelsCLI() { + 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(); + }); + } - // Helper function to fetch Ollama models (duplicated for CLI context) - function fetchOllamaModelsCLI(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; + // Helper function to fetch Ollama models (duplicated for CLI context) + function fetchOllamaModelsCLI(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 + } + }); + } - // Helper to get choices and default index for a role - const getPromptData = (role, allowNone = false) => { - const currentModel = currentModels[role]; // Use the fetched data - const allModelsRaw = getAvailableModels(); // Get all available models + // Helper to get choices and default index for a role + const getPromptData = (role, allowNone = false) => { + const currentModel = currentModels[role]; // Use the fetched data + const allModelsRaw = getAvailableModels(); // Get all available models - // Manually group models by provider - const modelsByProvider = allModelsRaw.reduce((acc, model) => { - if (!acc[model.provider]) { - acc[model.provider] = []; - } - acc[model.provider].push(model); - return acc; - }, {}); + // Manually group models by provider + const modelsByProvider = allModelsRaw.reduce((acc, model) => { + if (!acc[model.provider]) { + acc[model.provider] = []; + } + acc[model.provider].push(model); + return acc; + }, {}); - const cancelOption = { name: 'ā¹ Cancel Model Setup', value: '__CANCEL__' }; // Symbol updated - const noChangeOption = currentModel?.modelId - ? { - name: `āœ” No change to current ${role} model (${currentModel.modelId})`, // Symbol updated - value: '__NO_CHANGE__' - } - : null; + const cancelOption = { name: "ā¹ Cancel Model Setup", value: "__CANCEL__" }; // Symbol updated + const noChangeOption = currentModel?.modelId + ? { + name: `āœ” No change to current ${role} model (${currentModel.modelId})`, // Symbol updated + value: "__NO_CHANGE__", + } + : null; - const customOpenRouterOption = { - name: '* Custom OpenRouter model', // Symbol updated - value: '__CUSTOM_OPENROUTER__' - }; + const customOpenRouterOption = { + name: "* Custom OpenRouter model", // Symbol updated + value: "__CUSTOM_OPENROUTER__", + }; - const customOllamaOption = { - name: '* Custom Ollama model', // Symbol updated - value: '__CUSTOM_OLLAMA__' - }; + const customOllamaOption = { + name: "* Custom Ollama model", // Symbol updated + value: "__CUSTOM_OLLAMA__", + }; - const customBedrockOption = { - name: '* Custom Bedrock model', // Add Bedrock custom option - value: '__CUSTOM_BEDROCK__' - }; + const customBedrockOption = { + name: "* Custom Bedrock model", // Add Bedrock custom option + value: "__CUSTOM_BEDROCK__", + }; - let choices = []; - let defaultIndex = 0; // Default to 'Cancel' + let choices = []; + let defaultIndex = 0; // Default to 'Cancel' - // Filter and format models allowed for this role using the manually grouped data - const roleChoices = Object.entries(modelsByProvider) - .map(([provider, models]) => { - const providerModels = models - .filter((m) => m.allowed_roles.includes(role)) - .map((m) => ({ - name: `${provider} / ${m.id} ${ - m.cost_per_1m_tokens - ? chalk.gray( - `($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)` - ) - : '' - }`, - value: { id: m.id, provider }, - short: `${provider}/${m.id}` - })); - if (providerModels.length > 0) { - return [...providerModels]; - } - return null; - }) - .filter(Boolean) - .flat(); + // Filter and format models allowed for this role using the manually grouped data + const roleChoices = Object.entries(modelsByProvider) + .map(([provider, models]) => { + const providerModels = models + .filter((m) => m.allowed_roles.includes(role)) + .map((m) => ({ + name: `${provider} / ${m.id} ${ + m.cost_per_1m_tokens + ? chalk.gray( + `($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)` + ) + : "" + }`, + value: { id: m.id, provider }, + short: `${provider}/${m.id}`, + })); + if (providerModels.length > 0) { + return [...providerModels]; + } + return null; + }) + .filter(Boolean) + .flat(); - // Find the index of the currently selected model for setting the default - let currentChoiceIndex = -1; - if (currentModel?.modelId && currentModel?.provider) { - currentChoiceIndex = roleChoices.findIndex( - (choice) => - typeof choice.value === 'object' && - choice.value.id === currentModel.modelId && - choice.value.provider === currentModel.provider - ); - } + // Find the index of the currently selected model for setting the default + let currentChoiceIndex = -1; + if (currentModel?.modelId && currentModel?.provider) { + currentChoiceIndex = roleChoices.findIndex( + (choice) => + typeof choice.value === "object" && + choice.value.id === currentModel.modelId && + choice.value.provider === currentModel.provider + ); + } - // Construct final choices list based on whether 'None' is allowed - const commonPrefix = []; - if (noChangeOption) { - commonPrefix.push(noChangeOption); - } - commonPrefix.push(cancelOption); - commonPrefix.push(customOpenRouterOption); - commonPrefix.push(customOllamaOption); - commonPrefix.push(customBedrockOption); + // Construct final choices list based on whether 'None' is allowed + const commonPrefix = []; + if (noChangeOption) { + commonPrefix.push(noChangeOption); + } + commonPrefix.push(cancelOption); + commonPrefix.push(customOpenRouterOption); + commonPrefix.push(customOllamaOption); + commonPrefix.push(customBedrockOption); - let prefixLength = commonPrefix.length; // Initial prefix length + let prefixLength = commonPrefix.length; // Initial prefix length - if (allowNone) { - choices = [ - ...commonPrefix, - new inquirer.Separator(), - { name: '⚪ None (disable)', value: null }, // Symbol updated - new inquirer.Separator(), - ...roleChoices - ]; - // Adjust default index: Prefix + Sep1 + None + Sep2 (+3) - const noneOptionIndex = prefixLength + 1; - defaultIndex = - currentChoiceIndex !== -1 - ? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators - : noneOptionIndex; // Default to 'None' if no current model matched - } else { - choices = [ - ...commonPrefix, - new inquirer.Separator(), - ...roleChoices, - new inquirer.Separator() - ]; - // Adjust default index: Prefix + Sep (+1) - defaultIndex = - currentChoiceIndex !== -1 - ? currentChoiceIndex + prefixLength + 1 // Offset by prefix and separator - : noChangeOption - ? 1 - : 0; // Default to 'No Change' if present, else 'Cancel' - } + if (allowNone) { + choices = [ + ...commonPrefix, + new inquirer.Separator(), + { name: "⚪ None (disable)", value: null }, // Symbol updated + new inquirer.Separator(), + ...roleChoices, + ]; + // Adjust default index: Prefix + Sep1 + None + Sep2 (+3) + const noneOptionIndex = prefixLength + 1; + defaultIndex = + currentChoiceIndex !== -1 + ? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators + : noneOptionIndex; // Default to 'None' if no current model matched + } else { + choices = [ + ...commonPrefix, + new inquirer.Separator(), + ...roleChoices, + new inquirer.Separator(), + ]; + // Adjust default index: Prefix + Sep (+1) + defaultIndex = + currentChoiceIndex !== -1 + ? currentChoiceIndex + prefixLength + 1 // Offset by prefix and separator + : noChangeOption + ? 1 + : 0; // Default to 'No Change' if present, else 'Cancel' + } - // Ensure defaultIndex is valid within the final choices array length - if (defaultIndex < 0 || defaultIndex >= choices.length) { - // If default calculation failed or pointed outside bounds, reset intelligently - defaultIndex = 0; // Default to 'Cancel' - console.warn( - `Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.` - ); // Add warning - } + // Ensure defaultIndex is valid within the final choices array length + if (defaultIndex < 0 || defaultIndex >= choices.length) { + // If default calculation failed or pointed outside bounds, reset intelligently + defaultIndex = 0; // Default to 'Cancel' + console.warn( + `Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.` + ); // Add warning + } - return { choices, default: defaultIndex }; - }; + return { choices, default: defaultIndex }; + }; - // --- Generate choices using the helper --- - const mainPromptData = getPromptData('main'); - const researchPromptData = getPromptData('research'); - const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback + // --- Generate choices using the helper --- + const mainPromptData = getPromptData("main"); + const researchPromptData = getPromptData("research"); + const fallbackPromptData = getPromptData("fallback", true); // Allow 'None' for fallback - const answers = await inquirer.prompt([ - { - type: 'list', - name: 'mainModel', - message: 'Select the main model for generation/updates:', - choices: mainPromptData.choices, - default: mainPromptData.default - }, - { - type: 'list', - name: 'researchModel', - message: 'Select the research model:', - choices: researchPromptData.choices, - default: researchPromptData.default, - when: (ans) => ans.mainModel !== '__CANCEL__' - }, - { - type: 'list', - name: 'fallbackModel', - message: 'Select the fallback model (optional):', - choices: fallbackPromptData.choices, - default: fallbackPromptData.default, - when: (ans) => - ans.mainModel !== '__CANCEL__' && ans.researchModel !== '__CANCEL__' - } - ]); + const answers = await inquirer.prompt([ + { + type: "list", + name: "mainModel", + message: "Select the main model for generation/updates:", + choices: mainPromptData.choices, + default: mainPromptData.default, + }, + { + type: "list", + name: "researchModel", + message: "Select the research model:", + choices: researchPromptData.choices, + default: researchPromptData.default, + when: (ans) => ans.mainModel !== "__CANCEL__", + }, + { + type: "list", + name: "fallbackModel", + message: "Select the fallback model (optional):", + choices: fallbackPromptData.choices, + default: fallbackPromptData.default, + when: (ans) => + ans.mainModel !== "__CANCEL__" && ans.researchModel !== "__CANCEL__", + }, + ]); - let setupSuccess = true; - let setupConfigModified = false; - const coreOptionsSetup = { projectRoot }; // Pass root for setup actions + let setupSuccess = true; + let setupConfigModified = false; + const coreOptionsSetup = { projectRoot }; // Pass root for setup actions - // Helper to handle setting a model (including custom) - async function handleSetModel(role, selectedValue, currentModelId) { - if (selectedValue === '__CANCEL__') { - console.log( - chalk.yellow(`\nSetup canceled during ${role} model selection.`) - ); - setupSuccess = false; // Also mark success as false on cancel - return false; // Indicate cancellation - } + // Helper to handle setting a model (including custom) + async function handleSetModel(role, selectedValue, currentModelId) { + if (selectedValue === "__CANCEL__") { + console.log( + chalk.yellow(`\nSetup canceled during ${role} model selection.`) + ); + setupSuccess = false; // Also mark success as false on cancel + return false; // Indicate cancellation + } - // Handle the new 'No Change' option - if (selectedValue === '__NO_CHANGE__') { - console.log(chalk.gray(`No change selected for ${role} model.`)); - return true; // Indicate success, continue setup - } + // Handle the new 'No Change' option + if (selectedValue === "__NO_CHANGE__") { + console.log(chalk.gray(`No change selected for ${role} model.`)); + return true; // Indicate success, continue setup + } - let modelIdToSet = null; - let providerHint = null; - let isCustomSelection = false; + let modelIdToSet = null; + let providerHint = null; + let isCustomSelection = false; - if (selectedValue === '__CUSTOM_OPENROUTER__') { - isCustomSelection = true; - const { customId } = await inquirer.prompt([ - { - type: 'input', - name: 'customId', - message: `Enter the custom OpenRouter Model ID for the ${role} role:` - } - ]); - if (!customId) { - console.log(chalk.yellow('No custom ID entered. Skipping role.')); - return true; // Continue setup, but don't set this role - } - modelIdToSet = customId; - providerHint = 'openrouter'; - // Validate against live OpenRouter list - const openRouterModels = await fetchOpenRouterModelsCLI(); - if ( - !openRouterModels || - !openRouterModels.some((m) => m.id === modelIdToSet) - ) { - console.error( - chalk.red( - `Error: Model ID "${modelIdToSet}" not found in the live OpenRouter model list. Please check the ID.` - ) - ); - setupSuccess = false; - return true; // Continue setup, but mark as failed - } - } else if (selectedValue === '__CUSTOM_OLLAMA__') { - isCustomSelection = true; - const { customId } = await inquirer.prompt([ - { - type: 'input', - name: 'customId', - message: `Enter the custom Ollama Model ID for the ${role} role:` - } - ]); - if (!customId) { - console.log(chalk.yellow('No custom ID entered. Skipping role.')); - return true; // Continue setup, but don't set this role - } - modelIdToSet = customId; - providerHint = 'ollama'; - // Get the Ollama base URL from config for this role - const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); - // Validate against live Ollama list - const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseURL); - if (ollamaModels === null) { - console.error( - chalk.red( - `Error: Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` - ) - ); - setupSuccess = false; - return true; // Continue setup, but mark as failed - } else if (!ollamaModels.some((m) => m.model === modelIdToSet)) { - console.error( - chalk.red( - `Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.` - ) - ); - console.log( - chalk.yellow( - `You can check available models with: curl ${ollamaBaseURL}/tags` - ) - ); - setupSuccess = false; - return true; // Continue setup, but mark as failed - } - } else if (selectedValue === '__CUSTOM_BEDROCK__') { - isCustomSelection = true; - const { customId } = await inquirer.prompt([ - { - type: 'input', - name: 'customId', - message: `Enter the custom Bedrock Model ID for the ${role} role (e.g., anthropic.claude-3-sonnet-20240229-v1:0):` - } - ]); - if (!customId) { - console.log(chalk.yellow('No custom ID entered. Skipping role.')); - return true; // Continue setup, but don't set this role - } - modelIdToSet = customId; - providerHint = 'bedrock'; + if (selectedValue === "__CUSTOM_OPENROUTER__") { + isCustomSelection = true; + const { customId } = await inquirer.prompt([ + { + type: "input", + name: "customId", + message: `Enter the custom OpenRouter Model ID for the ${role} role:`, + }, + ]); + if (!customId) { + console.log(chalk.yellow("No custom ID entered. Skipping role.")); + return true; // Continue setup, but don't set this role + } + modelIdToSet = customId; + providerHint = "openrouter"; + // Validate against live OpenRouter list + const openRouterModels = await fetchOpenRouterModelsCLI(); + if ( + !openRouterModels || + !openRouterModels.some((m) => m.id === modelIdToSet) + ) { + console.error( + chalk.red( + `Error: Model ID "${modelIdToSet}" not found in the live OpenRouter model list. Please check the ID.` + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } + } else if (selectedValue === "__CUSTOM_OLLAMA__") { + isCustomSelection = true; + const { customId } = await inquirer.prompt([ + { + type: "input", + name: "customId", + message: `Enter the custom Ollama Model ID for the ${role} role:`, + }, + ]); + if (!customId) { + console.log(chalk.yellow("No custom ID entered. Skipping role.")); + return true; // Continue setup, but don't set this role + } + modelIdToSet = customId; + providerHint = "ollama"; + // Get the Ollama base URL from config for this role + const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); + // Validate against live Ollama list + const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseURL); + if (ollamaModels === null) { + console.error( + chalk.red( + `Error: Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } else if (!ollamaModels.some((m) => m.model === modelIdToSet)) { + console.error( + chalk.red( + `Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.` + ) + ); + console.log( + chalk.yellow( + `You can check available models with: curl ${ollamaBaseURL}/tags` + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } + } else if (selectedValue === "__CUSTOM_BEDROCK__") { + isCustomSelection = true; + const { customId } = await inquirer.prompt([ + { + type: "input", + name: "customId", + message: `Enter the custom Bedrock Model ID for the ${role} role (e.g., anthropic.claude-3-sonnet-20240229-v1:0):`, + }, + ]); + if (!customId) { + console.log(chalk.yellow("No custom ID entered. Skipping role.")); + return true; // Continue setup, but don't set this role + } + modelIdToSet = customId; + providerHint = "bedrock"; - // Check if AWS environment variables exist - if ( - !process.env.AWS_ACCESS_KEY_ID || - !process.env.AWS_SECRET_ACCESS_KEY - ) { - console.error( - chalk.red( - `Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.` - ) - ); - setupSuccess = false; - return true; // Continue setup, but mark as failed - } + // Check if AWS environment variables exist + if ( + !process.env.AWS_ACCESS_KEY_ID || + !process.env.AWS_SECRET_ACCESS_KEY + ) { + console.error( + chalk.red( + `Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.` + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } - console.log( - chalk.blue( - `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` - ) - ); - } else if ( - selectedValue && - typeof selectedValue === 'object' && - selectedValue.id - ) { - // Standard model selected from list - modelIdToSet = selectedValue.id; - providerHint = selectedValue.provider; // Provider is known - } else if (selectedValue === null && role === 'fallback') { - // Handle disabling fallback - modelIdToSet = null; - providerHint = null; - } else if (selectedValue) { - console.error( - chalk.red( - `Internal Error: Unexpected selection value for ${role}: ${JSON.stringify(selectedValue)}` - ) - ); - setupSuccess = false; - return true; - } + console.log( + chalk.blue( + `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` + ) + ); + } else if ( + selectedValue && + typeof selectedValue === "object" && + selectedValue.id + ) { + // Standard model selected from list + modelIdToSet = selectedValue.id; + providerHint = selectedValue.provider; // Provider is known + } else if (selectedValue === null && role === "fallback") { + // Handle disabling fallback + modelIdToSet = null; + providerHint = null; + } else if (selectedValue) { + console.error( + chalk.red( + `Internal Error: Unexpected selection value for ${role}: ${JSON.stringify(selectedValue)}` + ) + ); + setupSuccess = false; + return true; + } - // Only proceed if there's a change to be made - if (modelIdToSet !== currentModelId) { - if (modelIdToSet) { - // Set a specific model (standard or custom) - const result = await setModel(role, modelIdToSet, { - ...coreOptionsSetup, - providerHint // Pass the hint - }); - if (result.success) { - console.log( - chalk.blue( - `Set ${role} model: ${result.data.provider} / ${result.data.modelId}` - ) - ); - if (result.data.warning) { - // Display warning if returned by setModel - console.log(chalk.yellow(result.data.warning)); - } - setupConfigModified = true; - } else { - console.error( - chalk.red( - `Error setting ${role} model: ${result.error?.message || 'Unknown'}` - ) - ); - setupSuccess = false; - } - } else if (role === 'fallback') { - // Disable fallback model - const currentCfg = getConfig(projectRoot); - if (currentCfg?.models?.fallback?.modelId) { - // Check if it was actually set before clearing - currentCfg.models.fallback = { - ...currentCfg.models.fallback, - provider: undefined, - modelId: undefined - }; - if (writeConfig(currentCfg, projectRoot)) { - console.log(chalk.blue('Fallback model disabled.')); - setupConfigModified = true; - } else { - console.error( - chalk.red('Failed to disable fallback model in config file.') - ); - setupSuccess = false; - } - } else { - console.log(chalk.blue('Fallback model was already disabled.')); - } - } - } - return true; // Indicate setup should continue - } + // Only proceed if there's a change to be made + if (modelIdToSet !== currentModelId) { + if (modelIdToSet) { + // Set a specific model (standard or custom) + const result = await setModel(role, modelIdToSet, { + ...coreOptionsSetup, + providerHint, // Pass the hint + }); + if (result.success) { + console.log( + chalk.blue( + `Set ${role} model: ${result.data.provider} / ${result.data.modelId}` + ) + ); + if (result.data.warning) { + // Display warning if returned by setModel + console.log(chalk.yellow(result.data.warning)); + } + setupConfigModified = true; + } else { + console.error( + chalk.red( + `Error setting ${role} model: ${result.error?.message || "Unknown"}` + ) + ); + setupSuccess = false; + } + } else if (role === "fallback") { + // Disable fallback model + const currentCfg = getConfig(projectRoot); + if (currentCfg?.models?.fallback?.modelId) { + // Check if it was actually set before clearing + currentCfg.models.fallback = { + ...currentCfg.models.fallback, + provider: undefined, + modelId: undefined, + }; + if (writeConfig(currentCfg, projectRoot)) { + console.log(chalk.blue("Fallback model disabled.")); + setupConfigModified = true; + } else { + console.error( + chalk.red("Failed to disable fallback model in config file.") + ); + setupSuccess = false; + } + } else { + console.log(chalk.blue("Fallback model was already disabled.")); + } + } + } + return true; // Indicate setup should continue + } - // Process answers using the handler - if ( - !(await handleSetModel( - 'main', - answers.mainModel, - currentModels.main?.modelId // <--- Now 'currentModels' is defined - )) - ) { - return false; // Explicitly return false if cancelled - } - if ( - !(await handleSetModel( - 'research', - answers.researchModel, - currentModels.research?.modelId // <--- Now 'currentModels' is defined - )) - ) { - return false; // Explicitly return false if cancelled - } - if ( - !(await handleSetModel( - 'fallback', - answers.fallbackModel, - currentModels.fallback?.modelId // <--- Now 'currentModels' is defined - )) - ) { - return false; // Explicitly return false if cancelled - } + // Process answers using the handler + if ( + !(await handleSetModel( + "main", + answers.mainModel, + currentModels.main?.modelId // <--- Now 'currentModels' is defined + )) + ) { + return false; // Explicitly return false if cancelled + } + if ( + !(await handleSetModel( + "research", + answers.researchModel, + currentModels.research?.modelId // <--- Now 'currentModels' is defined + )) + ) { + return false; // Explicitly return false if cancelled + } + if ( + !(await handleSetModel( + "fallback", + answers.fallbackModel, + currentModels.fallback?.modelId // <--- Now 'currentModels' is defined + )) + ) { + return false; // Explicitly return false if cancelled + } - if (setupSuccess && setupConfigModified) { - console.log(chalk.green.bold('\nModel setup complete!')); - } else if (setupSuccess && !setupConfigModified) { - console.log(chalk.yellow('\nNo changes made to model configuration.')); - } else if (!setupSuccess) { - console.error( - chalk.red( - '\nErrors occurred during model selection. Please review and try again.' - ) - ); - } - return true; // Indicate setup flow completed (not cancelled) - // Let the main command flow continue to display results + if (setupSuccess && setupConfigModified) { + console.log(chalk.green.bold("\nModel setup complete!")); + } else if (setupSuccess && !setupConfigModified) { + console.log(chalk.yellow("\nNo changes made to model configuration.")); + } else if (!setupSuccess) { + console.error( + chalk.red( + "\nErrors occurred during model selection. Please review and try again." + ) + ); + } + return true; // Indicate setup flow completed (not cancelled) + // Let the main command flow continue to display results } /** @@ -628,1010 +628,1010 @@ async function runInteractiveSetup(projectRoot) { * @param {Object} program - Commander program instance */ function registerCommands(programInstance) { - // Add global error handler for unknown options - programInstance.on('option:unknown', function (unknownOption) { - const commandName = this._name || 'unknown'; - console.error(chalk.red(`Error: Unknown option '${unknownOption}'`)); - console.error( - chalk.yellow( - `Run 'task-master ${commandName} --help' to see available options` - ) - ); - process.exit(1); - }); - - // parse-prd command - programInstance - .command('parse-prd') - .description('Parse a PRD file and generate tasks') - .argument('[file]', 'Path to the PRD file') - .option( - '-i, --input ', - 'Path to the PRD file (alternative to positional argument)' - ) - .option('-o, --output ', 'Output file path', 'tasks/tasks.json') - .option('-n, --num-tasks ', 'Number of tasks to generate', '10') - .option('-f, --force', 'Skip confirmation when overwriting existing tasks') - .option( - '--append', - 'Append new tasks to existing tasks.json instead of overwriting' - ) - .option( - '-r, --research', - 'Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown' - ) - .action(async (file, options) => { - // Use input option if file argument not provided - const inputFile = file || options.input; - const defaultPrdPath = 'scripts/prd.txt'; - const numTasks = parseInt(options.numTasks, 10); - const outputPath = options.output; - const force = options.force || false; - const append = options.append || false; - const research = options.research || false; - let useForce = force; - let useAppend = append; - - // Helper function to check if tasks.json exists and confirm overwrite - async function confirmOverwriteIfNeeded() { - if (fs.existsSync(outputPath) && !useForce && !useAppend) { - const overwrite = await confirmTaskOverwrite(outputPath); - if (!overwrite) { - log('info', 'Operation cancelled.'); - return false; - } - // If user confirms 'y', we should set useForce = true for the parsePRD call - // Only overwrite if not appending - useForce = true; - } - return true; - } - - let spinner; - - try { - if (!inputFile) { - if (fs.existsSync(defaultPrdPath)) { - console.log( - chalk.blue(`Using default PRD file path: ${defaultPrdPath}`) - ); - if (!(await confirmOverwriteIfNeeded())) return; - - console.log(chalk.blue(`Generating ${numTasks} tasks...`)); - spinner = ora('Parsing PRD and generating tasks...\n').start(); - await parsePRD(defaultPrdPath, outputPath, numTasks, { - append: useAppend, // Changed key from useAppend to append - force: useForce, // Changed key from useForce to force - research: research - }); - spinner.succeed('Tasks generated successfully!'); - return; - } - - console.log( - chalk.yellow( - 'No PRD file specified and default PRD file not found at scripts/prd.txt.' - ) - ); - console.log( - boxen( - chalk.white.bold('Parse PRD Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master parse-prd [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -i, --input Path to the PRD file (alternative to positional argument)\n' + - ' -o, --output Output file path (default: "tasks/tasks.json")\n' + - ' -n, --num-tasks Number of tasks to generate (default: 10)\n' + - ' -f, --force Skip confirmation when overwriting existing tasks\n' + - ' --append Append new tasks to existing tasks.json instead of overwriting\n' + - ' -r, --research Use Perplexity AI for research-backed task generation\n\n' + - chalk.cyan('Example:') + - '\n' + - ' task-master parse-prd requirements.txt --num-tasks 15\n' + - ' task-master parse-prd --input=requirements.txt\n' + - ' task-master parse-prd --force\n' + - ' task-master parse-prd requirements_v2.txt --append\n' + - ' task-master parse-prd requirements.txt --research\n\n' + - chalk.yellow('Note: This command will:') + - '\n' + - ' 1. Look for a PRD file at scripts/prd.txt by default\n' + - ' 2. Use the file specified by --input or positional argument if provided\n' + - ' 3. Generate tasks from the PRD and either:\n' + - ' - Overwrite any existing tasks.json file (default)\n' + - ' - Append to existing tasks.json if --append is used', - { padding: 1, borderColor: 'blue', borderStyle: 'round' } - ) - ); - return; - } - - if (!fs.existsSync(inputFile)) { - console.error( - chalk.red(`Error: Input PRD file not found: ${inputFile}`) - ); - process.exit(1); - } - - if (!(await confirmOverwriteIfNeeded())) return; - - console.log(chalk.blue(`Parsing PRD file: ${inputFile}`)); - console.log(chalk.blue(`Generating ${numTasks} tasks...`)); - if (append) { - console.log(chalk.blue('Appending to existing tasks...')); - } - if (research) { - console.log( - chalk.blue( - 'Using Perplexity AI for research-backed task generation' - ) - ); - } - - spinner = ora('Parsing PRD and generating tasks...\n').start(); - await parsePRD(inputFile, outputPath, numTasks, { - append: useAppend, - force: useForce, - research: research - }); - spinner.succeed('Tasks generated successfully!'); - } catch (error) { - if (spinner) { - spinner.fail(`Error parsing PRD: ${error.message}`); - } else { - console.error(chalk.red(`Error parsing PRD: ${error.message}`)); - } - process.exit(1); - } - }); - - // update command - programInstance - .command('update') - .description( - 'Update multiple tasks with ID >= "from" based on new information or implementation changes' - ) - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '--from ', - 'Task ID to start updating from (tasks with ID >= this value will be updated)', - '1' - ) - .option( - '-p, --prompt ', - 'Prompt explaining the changes or new context (required)' - ) - .option( - '-r, --research', - 'Use Perplexity AI for research-backed task updates' - ) - .action(async (options) => { - const tasksPath = options.file; - const fromId = parseInt(options.from, 10); // Validation happens here - const prompt = options.prompt; - const useResearch = options.research || false; - - // Check if there's an 'id' option which is a common mistake (instead of 'from') - if ( - process.argv.includes('--id') || - process.argv.some((arg) => arg.startsWith('--id=')) - ) { - console.error( - chalk.red('Error: The update command uses --from=, not --id=') - ); - console.log(chalk.yellow('\nTo update multiple tasks:')); - console.log( - ` task-master update --from=${fromId} --prompt="Your prompt here"` - ); - console.log( - chalk.yellow( - '\nTo update a single specific task, use the update-task command instead:' - ) - ); - console.log( - ` task-master update-task --id= --prompt="Your prompt here"` - ); - process.exit(1); - } - - if (!prompt) { - console.error( - chalk.red( - 'Error: --prompt parameter is required. Please provide information about the changes.' - ) - ); - process.exit(1); - } - - console.log( - chalk.blue( - `Updating tasks from ID >= ${fromId} with prompt: "${prompt}"` - ) - ); - console.log(chalk.blue(`Tasks file: ${tasksPath}`)); - - if (useResearch) { - console.log( - chalk.blue('Using Perplexity AI for research-backed task updates') - ); - } - - // Call core updateTasks, passing empty context for CLI - await updateTasks( - tasksPath, - fromId, - prompt, - useResearch, - {} // Pass empty context - ); - }); - - // update-task command - programInstance - .command('update-task') - .description( - 'Update a single specific task by ID with new information (use --id parameter)' - ) - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option('-i, --id ', 'Task ID to update (required)') - .option( - '-p, --prompt ', - 'Prompt explaining the changes or new context (required)' - ) - .option( - '-r, --research', - 'Use Perplexity AI for research-backed task updates' - ) - .action(async (options) => { - try { - const tasksPath = options.file; - - // Validate required parameters - if (!options.id) { - console.error(chalk.red('Error: --id parameter is required')); - console.log( - chalk.yellow( - 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' - ) - ); - process.exit(1); - } - - // Parse the task ID and validate it's a number - const taskId = parseInt(options.id, 10); - if (isNaN(taskId) || taskId <= 0) { - console.error( - chalk.red( - `Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.` - ) - ); - console.log( - chalk.yellow( - 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' - ) - ); - process.exit(1); - } - - if (!options.prompt) { - console.error( - chalk.red( - 'Error: --prompt parameter is required. Please provide information about the changes.' - ) - ); - console.log( - chalk.yellow( - 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' - ) - ); - process.exit(1); - } - - const prompt = options.prompt; - const useResearch = options.research || false; - - // Validate tasks file exists - if (!fs.existsSync(tasksPath)) { - console.error( - chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) - ); - if (tasksPath === 'tasks/tasks.json') { - console.log( - chalk.yellow( - 'Hint: Run task-master init or task-master parse-prd to create tasks.json first' - ) - ); - } else { - console.log( - chalk.yellow( - `Hint: Check if the file path is correct: ${tasksPath}` - ) - ); - } - process.exit(1); - } - - console.log( - chalk.blue(`Updating task ${taskId} with prompt: "${prompt}"`) - ); - console.log(chalk.blue(`Tasks file: ${tasksPath}`)); - - if (useResearch) { - // Verify Perplexity API key exists if using research - if (!isApiKeySet('perplexity')) { - console.log( - chalk.yellow( - 'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.' - ) - ); - console.log( - chalk.yellow('Falling back to Claude AI for task update.') - ); - } else { - console.log( - chalk.blue('Using Perplexity AI for research-backed task update') - ); - } - } - - const result = await updateTaskById( - tasksPath, - taskId, - prompt, - useResearch - ); - - // If the task wasn't updated (e.g., if it was already marked as done) - if (!result) { - console.log( - chalk.yellow( - '\nTask update was not completed. Review the messages above for details.' - ) - ); - } - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - - // Provide more helpful error messages for common issues - if ( - error.message.includes('task') && - error.message.includes('not found') - ) { - console.log(chalk.yellow('\nTo fix this issue:')); - console.log( - ' 1. Run task-master list to see all available task IDs' - ); - console.log(' 2. Use a valid task ID with the --id parameter'); - } else if (error.message.includes('API key')) { - console.log( - chalk.yellow( - '\nThis error is related to API keys. Check your environment variables.' - ) - ); - } - - // Use getDebugFlag getter instead of CONFIG.debug - if (getDebugFlag()) { - console.error(error); - } - - process.exit(1); - } - }); - - // update-subtask command - programInstance - .command('update-subtask') - .description( - 'Update a subtask by appending additional timestamped information' - ) - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-i, --id ', - 'Subtask ID to update in format "parentId.subtaskId" (required)' - ) - .option( - '-p, --prompt ', - 'Prompt explaining what information to add (required)' - ) - .option('-r, --research', 'Use Perplexity AI for research-backed updates') - .action(async (options) => { - try { - const tasksPath = options.file; - - // Validate required parameters - if (!options.id) { - console.error(chalk.red('Error: --id parameter is required')); - console.log( - chalk.yellow( - 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' - ) - ); - process.exit(1); - } - - // Validate subtask ID format (should contain a dot) - const subtaskId = options.id; - if (!subtaskId.includes('.')) { - console.error( - chalk.red( - `Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"` - ) - ); - console.log( - chalk.yellow( - 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' - ) - ); - process.exit(1); - } - - if (!options.prompt) { - console.error( - chalk.red( - 'Error: --prompt parameter is required. Please provide information to add to the subtask.' - ) - ); - console.log( - chalk.yellow( - 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' - ) - ); - process.exit(1); - } - - const prompt = options.prompt; - const useResearch = options.research || false; - - // Validate tasks file exists - if (!fs.existsSync(tasksPath)) { - console.error( - chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) - ); - if (tasksPath === 'tasks/tasks.json') { - console.log( - chalk.yellow( - 'Hint: Run task-master init or task-master parse-prd to create tasks.json first' - ) - ); - } else { - console.log( - chalk.yellow( - `Hint: Check if the file path is correct: ${tasksPath}` - ) - ); - } - process.exit(1); - } - - console.log( - chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`) - ); - console.log(chalk.blue(`Tasks file: ${tasksPath}`)); - - if (useResearch) { - // Verify Perplexity API key exists if using research - if (!isApiKeySet('perplexity')) { - console.log( - chalk.yellow( - 'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.' - ) - ); - console.log( - chalk.yellow('Falling back to Claude AI for subtask update.') - ); - } else { - console.log( - chalk.blue( - 'Using Perplexity AI for research-backed subtask update' - ) - ); - } - } - - const result = await updateSubtaskById( - tasksPath, - subtaskId, - prompt, - useResearch - ); - - if (!result) { - console.log( - chalk.yellow( - '\nSubtask update was not completed. Review the messages above for details.' - ) - ); - } - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - - // Provide more helpful error messages for common issues - if ( - error.message.includes('subtask') && - error.message.includes('not found') - ) { - console.log(chalk.yellow('\nTo fix this issue:')); - console.log( - ' 1. Run task-master list --with-subtasks to see all available subtask IDs' - ); - console.log( - ' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"' - ); - } else if (error.message.includes('API key')) { - console.log( - chalk.yellow( - '\nThis error is related to API keys. Check your environment variables.' - ) - ); - } - - // Use getDebugFlag getter instead of CONFIG.debug - if (getDebugFlag()) { - console.error(error); - } - - process.exit(1); - } - }); - - // generate command - programInstance - .command('generate') - .description('Generate task files from tasks.json') - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option('-o, --output ', 'Output directory', 'tasks') - .action(async (options) => { - const tasksPath = options.file; - const outputDir = options.output; - - console.log(chalk.blue(`Generating task files from: ${tasksPath}`)); - console.log(chalk.blue(`Output directory: ${outputDir}`)); - - await generateTaskFiles(tasksPath, outputDir); - }); - - // set-status command - programInstance - .command('set-status') - .alias('mark') - .alias('set') - .description('Set the status of a task') - .option( - '-i, --id ', - 'Task ID (can be comma-separated for multiple tasks)' - ) - .option( - '-s, --status ', - `New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})` - ) - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (options) => { - const tasksPath = options.file; - const taskId = options.id; - const status = options.status; - - if (!taskId || !status) { - console.error(chalk.red('Error: Both --id and --status are required')); - process.exit(1); - } - - if (!isValidTaskStatus(status)) { - console.error( - chalk.red( - `Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` - ) - ); - - process.exit(1); - } - - console.log( - chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`) - ); - - await setTaskStatus(tasksPath, taskId, status); - }); - - // list command - programInstance - .command('list') - .description('List all tasks') - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-r, --report ', - 'Path to the complexity report file', - 'scripts/task-complexity-report.json' - ) - .option('-s, --status ', 'Filter by status') - .option('--with-subtasks', 'Show subtasks for each task') - .action(async (options) => { - const tasksPath = options.file; - const reportPath = options.report; - const statusFilter = options.status; - const withSubtasks = options.withSubtasks || false; - - console.log(chalk.blue(`Listing tasks from: ${tasksPath}`)); - if (statusFilter) { - console.log(chalk.blue(`Filtering by status: ${statusFilter}`)); - } - if (withSubtasks) { - console.log(chalk.blue('Including subtasks in listing')); - } - - await listTasks(tasksPath, statusFilter, reportPath, withSubtasks); - }); - - // expand command - programInstance - .command('expand') - .description('Expand a task into subtasks using AI') - .option('-i, --id ', 'ID of the task to expand') - .option( - '-a, --all', - 'Expand all pending tasks based on complexity analysis' - ) - .option( - '-n, --num ', - 'Number of subtasks to generate (uses complexity analysis by default if available)' - ) - .option( - '-r, --research', - 'Enable research-backed generation (e.g., using Perplexity)', - false - ) - .option('-p, --prompt ', 'Additional context for subtask generation') - .option('-f, --force', 'Force expansion even if subtasks exist', false) // Ensure force option exists - .option( - '--file ', - 'Path to the tasks file (relative to project root)', - 'tasks/tasks.json' - ) // Allow file override - .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path - - if (options.all) { - // --- Handle expand --all --- - console.log(chalk.blue('Expanding all pending tasks...')); - // Updated call to the refactored expandAllTasks - try { - const result = await expandAllTasks( - tasksPath, - options.num, // Pass num - options.research, // Pass research flag - options.prompt, // Pass additional context - options.force, // Pass force flag - {} // Pass empty context for CLI calls - // outputFormat defaults to 'text' in expandAllTasks for CLI - ); - } catch (error) { - console.error( - chalk.red(`Error expanding all tasks: ${error.message}`) - ); - process.exit(1); - } - } else if (options.id) { - // --- Handle expand --id (Should be correct from previous refactor) --- - if (!options.id) { - console.error( - chalk.red('Error: Task ID is required unless using --all.') - ); - process.exit(1); - } - - console.log(chalk.blue(`Expanding task ${options.id}...`)); - try { - // Call the refactored expandTask function - await expandTask( - tasksPath, - options.id, - options.num, - options.research, - options.prompt, - {}, // Pass empty context for CLI calls - options.force // Pass the force flag down - ); - // expandTask logs its own success/failure for single task - } catch (error) { - console.error( - chalk.red(`Error expanding task ${options.id}: ${error.message}`) - ); - process.exit(1); - } - } else { - console.error( - chalk.red('Error: You must specify either a task ID (--id) or --all.') - ); - programInstance.help(); // Show help - } - }); - - // analyze-complexity command - programInstance - .command('analyze-complexity') - .description( - `Analyze tasks and generate expansion recommendations${chalk.reset('')}` - ) - .option( - '-o, --output ', - 'Output file path for the report', - 'scripts/task-complexity-report.json' - ) - .option( - '-m, --model ', - 'LLM model to use for analysis (defaults to configured model)' - ) - .option( - '-t, --threshold ', - 'Minimum complexity score to recommend expansion (1-10)', - '5' - ) - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-r, --research', - 'Use Perplexity AI for research-backed complexity analysis' - ) - .option( - '-i, --id ', - 'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")' - ) - .option('--from ', 'Starting task ID in a range to analyze') - .option('--to ', 'Ending task ID in a range to analyze') - .action(async (options) => { - const tasksPath = options.file || 'tasks/tasks.json'; - const outputPath = options.output; - const modelOverride = options.model; - const thresholdScore = parseFloat(options.threshold); - const useResearch = options.research || false; - - console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`)); - console.log(chalk.blue(`Output report will be saved to: ${outputPath}`)); - - if (options.id) { - console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`)); - } else if (options.from || options.to) { - const fromStr = options.from ? options.from : 'first'; - const toStr = options.to ? options.to : 'last'; - console.log( - chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`) - ); - } - - if (useResearch) { - console.log( - chalk.blue( - 'Using Perplexity AI for research-backed complexity analysis' - ) - ); - } - - await analyzeTaskComplexity(options); - }); - - // research command - programInstance - .command('research') - .description('Perform AI-powered research queries with project context') - .argument('', 'Research prompt to investigate') - .option('--file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-i, --id ', - 'Comma-separated task/subtask IDs to include as context (e.g., "15,16.2")' - ) - .option( - '-f, --files ', - 'Comma-separated file paths to include as context' - ) - .option( - '-c, --context ', - 'Additional custom context to include in the research prompt' - ) - .option( - '-t, --tree', - 'Include project file tree structure in the research context' - ) - .option( - '-s, --save ', - 'Save research results to the specified task/subtask(s)' - ) - .option( - '-d, --detail ', - 'Output detail level: low, medium, high', - 'medium' - ) - .action(async (prompt, options) => { - // Parameter validation - if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { - console.error( - chalk.red('Error: Research prompt is required and cannot be empty') - ); - process.exit(1); - } - - // Validate detail level - const validDetailLevels = ['low', 'medium', 'high']; - if ( - options.detail && - !validDetailLevels.includes(options.detail.toLowerCase()) - ) { - console.error( - chalk.red( - `Error: Detail level must be one of: ${validDetailLevels.join(', ')}` - ) - ); - process.exit(1); - } - - // Validate and parse task IDs if provided - let taskIds = []; - if (options.id) { - try { - taskIds = options.id.split(',').map((id) => { - const trimmedId = id.trim(); - // Support both task IDs (e.g., "15") and subtask IDs (e.g., "15.2") - if (!/^\d+(\.\d+)?$/.test(trimmedId)) { - throw new Error( - `Invalid task ID format: "${trimmedId}". Expected format: "15" or "15.2"` - ); - } - return trimmedId; - }); - } catch (error) { - console.error(chalk.red(`Error parsing task IDs: ${error.message}`)); - process.exit(1); - } - } - - // Validate and parse file paths if provided - let filePaths = []; - if (options.files) { - try { - filePaths = options.files.split(',').map((filePath) => { - const trimmedPath = filePath.trim(); - if (trimmedPath.length === 0) { - throw new Error('Empty file path provided'); - } - return trimmedPath; - }); - } catch (error) { - console.error( - chalk.red(`Error parsing file paths: ${error.message}`) - ); - process.exit(1); - } - } - - // Validate save option if provided - if (options.save) { - const saveTarget = options.save.trim(); - if (saveTarget.length === 0) { - console.error(chalk.red('Error: Save target cannot be empty')); - process.exit(1); - } - // Check if it's a valid file path (basic validation) - if (saveTarget.includes('..') || saveTarget.startsWith('/')) { - console.error( - chalk.red( - 'Error: Save path must be relative and cannot contain ".."' - ) - ); - process.exit(1); - } - } - - // Determine project root and tasks file path - const projectRoot = findProjectRoot() || '.'; - const tasksPath = - options.file || path.join(projectRoot, 'tasks', 'tasks.json'); - - // Validate tasks file exists if task IDs are specified - if (taskIds.length > 0) { - try { - const tasksData = readJSON(tasksPath); - if (!tasksData || !tasksData.tasks) { - console.error( - chalk.red(`Error: No valid tasks found in ${tasksPath}`) - ); - process.exit(1); - } - } catch (error) { - console.error( - chalk.red(`Error reading tasks file: ${error.message}`) - ); - process.exit(1); - } - } - - // Validate file paths exist if specified - if (filePaths.length > 0) { - for (const filePath of filePaths) { - const fullPath = path.isAbsolute(filePath) - ? filePath - : path.join(projectRoot, filePath); - if (!fs.existsSync(fullPath)) { - console.error(chalk.red(`Error: File not found: ${filePath}`)); - process.exit(1); - } - } - } - - // Create validated parameters object - const validatedParams = { - prompt: prompt.trim(), - taskIds: taskIds, - filePaths: filePaths, - customContext: options.context ? options.context.trim() : null, - includeProjectTree: !!options.tree, - saveTarget: options.save ? options.save.trim() : null, - detailLevel: options.detail ? options.detail.toLowerCase() : 'medium', - tasksPath: tasksPath, - projectRoot: projectRoot - }; - - // Display what we're about to do - console.log(chalk.blue(`Researching: "${validatedParams.prompt}"`)); - - if (validatedParams.taskIds.length > 0) { - console.log( - chalk.gray(`Task context: ${validatedParams.taskIds.join(', ')}`) - ); - } - - if (validatedParams.filePaths.length > 0) { - console.log( - chalk.gray(`File context: ${validatedParams.filePaths.join(', ')}`) - ); - } - - if (validatedParams.customContext) { - console.log( - chalk.gray( - `Custom context: ${validatedParams.customContext.substring(0, 50)}${validatedParams.customContext.length > 50 ? '...' : ''}` - ) - ); - } - - if (validatedParams.includeProjectTree) { - console.log(chalk.gray('Including project file tree')); - } - - console.log(chalk.gray(`Detail level: ${validatedParams.detailLevel}`)); - - try { - // Import the research function - const { performResearch } = await import('./task-manager/research.js'); - - // Prepare research options - const researchOptions = { - taskIds: validatedParams.taskIds, - filePaths: validatedParams.filePaths, - customContext: validatedParams.customContext || '', - includeProjectTree: validatedParams.includeProjectTree, - detailLevel: validatedParams.detailLevel, - projectRoot: validatedParams.projectRoot - }; - - // Execute research - const result = await performResearch( - validatedParams.prompt, - researchOptions, - { - commandName: 'research', - outputType: 'cli' - }, - 'text' - ); - - // Save results if requested - if (validatedParams.saveTarget) { - const saveContent = `# Research Query: ${validatedParams.prompt} + // Add global error handler for unknown options + programInstance.on("option:unknown", function (unknownOption) { + const commandName = this._name || "unknown"; + console.error(chalk.red(`Error: Unknown option '${unknownOption}'`)); + console.error( + chalk.yellow( + `Run 'task-master ${commandName} --help' to see available options` + ) + ); + process.exit(1); + }); + + // parse-prd command + programInstance + .command("parse-prd") + .description("Parse a PRD file and generate tasks") + .argument("[file]", "Path to the PRD file") + .option( + "-i, --input ", + "Path to the PRD file (alternative to positional argument)" + ) + .option("-o, --output ", "Output file path", "tasks/tasks.json") + .option("-n, --num-tasks ", "Number of tasks to generate", "10") + .option("-f, --force", "Skip confirmation when overwriting existing tasks") + .option( + "--append", + "Append new tasks to existing tasks.json instead of overwriting" + ) + .option( + "-r, --research", + "Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown" + ) + .action(async (file, options) => { + // Use input option if file argument not provided + const inputFile = file || options.input; + const defaultPrdPath = "scripts/prd.txt"; + const numTasks = parseInt(options.numTasks, 10); + const outputPath = options.output; + const force = options.force || false; + const append = options.append || false; + const research = options.research || false; + let useForce = force; + let useAppend = append; + + // Helper function to check if tasks.json exists and confirm overwrite + async function confirmOverwriteIfNeeded() { + if (fs.existsSync(outputPath) && !useForce && !useAppend) { + const overwrite = await confirmTaskOverwrite(outputPath); + if (!overwrite) { + log("info", "Operation cancelled."); + return false; + } + // If user confirms 'y', we should set useForce = true for the parsePRD call + // Only overwrite if not appending + useForce = true; + } + return true; + } + + let spinner; + + try { + if (!inputFile) { + if (fs.existsSync(defaultPrdPath)) { + console.log( + chalk.blue(`Using default PRD file path: ${defaultPrdPath}`) + ); + if (!(await confirmOverwriteIfNeeded())) return; + + console.log(chalk.blue(`Generating ${numTasks} tasks...`)); + spinner = ora("Parsing PRD and generating tasks...\n").start(); + await parsePRD(defaultPrdPath, outputPath, numTasks, { + append: useAppend, // Changed key from useAppend to append + force: useForce, // Changed key from useForce to force + research: research, + }); + spinner.succeed("Tasks generated successfully!"); + return; + } + + console.log( + chalk.yellow( + "No PRD file specified and default PRD file not found at scripts/prd.txt." + ) + ); + console.log( + boxen( + chalk.white.bold("Parse PRD Help") + + "\n\n" + + chalk.cyan("Usage:") + + "\n" + + ` task-master parse-prd [options]\n\n` + + chalk.cyan("Options:") + + "\n" + + " -i, --input Path to the PRD file (alternative to positional argument)\n" + + ' -o, --output Output file path (default: "tasks/tasks.json")\n' + + " -n, --num-tasks Number of tasks to generate (default: 10)\n" + + " -f, --force Skip confirmation when overwriting existing tasks\n" + + " --append Append new tasks to existing tasks.json instead of overwriting\n" + + " -r, --research Use Perplexity AI for research-backed task generation\n\n" + + chalk.cyan("Example:") + + "\n" + + " task-master parse-prd requirements.txt --num-tasks 15\n" + + " task-master parse-prd --input=requirements.txt\n" + + " task-master parse-prd --force\n" + + " task-master parse-prd requirements_v2.txt --append\n" + + " task-master parse-prd requirements.txt --research\n\n" + + chalk.yellow("Note: This command will:") + + "\n" + + " 1. Look for a PRD file at scripts/prd.txt by default\n" + + " 2. Use the file specified by --input or positional argument if provided\n" + + " 3. Generate tasks from the PRD and either:\n" + + " - Overwrite any existing tasks.json file (default)\n" + + " - Append to existing tasks.json if --append is used", + { padding: 1, borderColor: "blue", borderStyle: "round" } + ) + ); + return; + } + + if (!fs.existsSync(inputFile)) { + console.error( + chalk.red(`Error: Input PRD file not found: ${inputFile}`) + ); + process.exit(1); + } + + if (!(await confirmOverwriteIfNeeded())) return; + + console.log(chalk.blue(`Parsing PRD file: ${inputFile}`)); + console.log(chalk.blue(`Generating ${numTasks} tasks...`)); + if (append) { + console.log(chalk.blue("Appending to existing tasks...")); + } + if (research) { + console.log( + chalk.blue( + "Using Perplexity AI for research-backed task generation" + ) + ); + } + + spinner = ora("Parsing PRD and generating tasks...\n").start(); + await parsePRD(inputFile, outputPath, numTasks, { + append: useAppend, + force: useForce, + research: research, + }); + spinner.succeed("Tasks generated successfully!"); + } catch (error) { + if (spinner) { + spinner.fail(`Error parsing PRD: ${error.message}`); + } else { + console.error(chalk.red(`Error parsing PRD: ${error.message}`)); + } + process.exit(1); + } + }); + + // update command + programInstance + .command("update") + .description( + 'Update multiple tasks with ID >= "from" based on new information or implementation changes' + ) + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option( + "--from ", + "Task ID to start updating from (tasks with ID >= this value will be updated)", + "1" + ) + .option( + "-p, --prompt ", + "Prompt explaining the changes or new context (required)" + ) + .option( + "-r, --research", + "Use Perplexity AI for research-backed task updates" + ) + .action(async (options) => { + const tasksPath = options.file; + const fromId = parseInt(options.from, 10); // Validation happens here + const prompt = options.prompt; + const useResearch = options.research || false; + + // Check if there's an 'id' option which is a common mistake (instead of 'from') + if ( + process.argv.includes("--id") || + process.argv.some((arg) => arg.startsWith("--id=")) + ) { + console.error( + chalk.red("Error: The update command uses --from=, not --id=") + ); + console.log(chalk.yellow("\nTo update multiple tasks:")); + console.log( + ` task-master update --from=${fromId} --prompt="Your prompt here"` + ); + console.log( + chalk.yellow( + "\nTo update a single specific task, use the update-task command instead:" + ) + ); + console.log( + ` task-master update-task --id= --prompt="Your prompt here"` + ); + process.exit(1); + } + + if (!prompt) { + console.error( + chalk.red( + "Error: --prompt parameter is required. Please provide information about the changes." + ) + ); + process.exit(1); + } + + console.log( + chalk.blue( + `Updating tasks from ID >= ${fromId} with prompt: "${prompt}"` + ) + ); + console.log(chalk.blue(`Tasks file: ${tasksPath}`)); + + if (useResearch) { + console.log( + chalk.blue("Using Perplexity AI for research-backed task updates") + ); + } + + // Call core updateTasks, passing empty context for CLI + await updateTasks( + tasksPath, + fromId, + prompt, + useResearch, + {} // Pass empty context + ); + }); + + // update-task command + programInstance + .command("update-task") + .description( + "Update a single specific task by ID with new information (use --id parameter)" + ) + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option("-i, --id ", "Task ID to update (required)") + .option( + "-p, --prompt ", + "Prompt explaining the changes or new context (required)" + ) + .option( + "-r, --research", + "Use Perplexity AI for research-backed task updates" + ) + .action(async (options) => { + try { + const tasksPath = options.file; + + // Validate required parameters + if (!options.id) { + console.error(chalk.red("Error: --id parameter is required")); + console.log( + chalk.yellow( + 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' + ) + ); + process.exit(1); + } + + // Parse the task ID and validate it's a number + const taskId = parseInt(options.id, 10); + if (isNaN(taskId) || taskId <= 0) { + console.error( + chalk.red( + `Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.` + ) + ); + console.log( + chalk.yellow( + 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' + ) + ); + process.exit(1); + } + + if (!options.prompt) { + console.error( + chalk.red( + "Error: --prompt parameter is required. Please provide information about the changes." + ) + ); + console.log( + chalk.yellow( + 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' + ) + ); + process.exit(1); + } + + const prompt = options.prompt; + const useResearch = options.research || false; + + // Validate tasks file exists + if (!fs.existsSync(tasksPath)) { + console.error( + chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) + ); + if (tasksPath === "tasks/tasks.json") { + console.log( + chalk.yellow( + "Hint: Run task-master init or task-master parse-prd to create tasks.json first" + ) + ); + } else { + console.log( + chalk.yellow( + `Hint: Check if the file path is correct: ${tasksPath}` + ) + ); + } + process.exit(1); + } + + console.log( + chalk.blue(`Updating task ${taskId} with prompt: "${prompt}"`) + ); + console.log(chalk.blue(`Tasks file: ${tasksPath}`)); + + if (useResearch) { + // Verify Perplexity API key exists if using research + if (!isApiKeySet("perplexity")) { + console.log( + chalk.yellow( + "Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available." + ) + ); + console.log( + chalk.yellow("Falling back to Claude AI for task update.") + ); + } else { + console.log( + chalk.blue("Using Perplexity AI for research-backed task update") + ); + } + } + + const result = await updateTaskById( + tasksPath, + taskId, + prompt, + useResearch + ); + + // If the task wasn't updated (e.g., if it was already marked as done) + if (!result) { + console.log( + chalk.yellow( + "\nTask update was not completed. Review the messages above for details." + ) + ); + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + + // Provide more helpful error messages for common issues + if ( + error.message.includes("task") && + error.message.includes("not found") + ) { + console.log(chalk.yellow("\nTo fix this issue:")); + console.log( + " 1. Run task-master list to see all available task IDs" + ); + console.log(" 2. Use a valid task ID with the --id parameter"); + } else if (error.message.includes("API key")) { + console.log( + chalk.yellow( + "\nThis error is related to API keys. Check your environment variables." + ) + ); + } + + // Use getDebugFlag getter instead of CONFIG.debug + if (getDebugFlag()) { + console.error(error); + } + + process.exit(1); + } + }); + + // update-subtask command + programInstance + .command("update-subtask") + .description( + "Update a subtask by appending additional timestamped information" + ) + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option( + "-i, --id ", + 'Subtask ID to update in format "parentId.subtaskId" (required)' + ) + .option( + "-p, --prompt ", + "Prompt explaining what information to add (required)" + ) + .option("-r, --research", "Use Perplexity AI for research-backed updates") + .action(async (options) => { + try { + const tasksPath = options.file; + + // Validate required parameters + if (!options.id) { + console.error(chalk.red("Error: --id parameter is required")); + console.log( + chalk.yellow( + 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' + ) + ); + process.exit(1); + } + + // Validate subtask ID format (should contain a dot) + const subtaskId = options.id; + if (!subtaskId.includes(".")) { + console.error( + chalk.red( + `Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"` + ) + ); + console.log( + chalk.yellow( + 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' + ) + ); + process.exit(1); + } + + if (!options.prompt) { + console.error( + chalk.red( + "Error: --prompt parameter is required. Please provide information to add to the subtask." + ) + ); + console.log( + chalk.yellow( + 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' + ) + ); + process.exit(1); + } + + const prompt = options.prompt; + const useResearch = options.research || false; + + // Validate tasks file exists + if (!fs.existsSync(tasksPath)) { + console.error( + chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) + ); + if (tasksPath === "tasks/tasks.json") { + console.log( + chalk.yellow( + "Hint: Run task-master init or task-master parse-prd to create tasks.json first" + ) + ); + } else { + console.log( + chalk.yellow( + `Hint: Check if the file path is correct: ${tasksPath}` + ) + ); + } + process.exit(1); + } + + console.log( + chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`) + ); + console.log(chalk.blue(`Tasks file: ${tasksPath}`)); + + if (useResearch) { + // Verify Perplexity API key exists if using research + if (!isApiKeySet("perplexity")) { + console.log( + chalk.yellow( + "Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available." + ) + ); + console.log( + chalk.yellow("Falling back to Claude AI for subtask update.") + ); + } else { + console.log( + chalk.blue( + "Using Perplexity AI for research-backed subtask update" + ) + ); + } + } + + const result = await updateSubtaskById( + tasksPath, + subtaskId, + prompt, + useResearch + ); + + if (!result) { + console.log( + chalk.yellow( + "\nSubtask update was not completed. Review the messages above for details." + ) + ); + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + + // Provide more helpful error messages for common issues + if ( + error.message.includes("subtask") && + error.message.includes("not found") + ) { + console.log(chalk.yellow("\nTo fix this issue:")); + console.log( + " 1. Run task-master list --with-subtasks to see all available subtask IDs" + ); + console.log( + ' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"' + ); + } else if (error.message.includes("API key")) { + console.log( + chalk.yellow( + "\nThis error is related to API keys. Check your environment variables." + ) + ); + } + + // Use getDebugFlag getter instead of CONFIG.debug + if (getDebugFlag()) { + console.error(error); + } + + process.exit(1); + } + }); + + // generate command + programInstance + .command("generate") + .description("Generate task files from tasks.json") + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option("-o, --output ", "Output directory", "tasks") + .action(async (options) => { + const tasksPath = options.file; + const outputDir = options.output; + + console.log(chalk.blue(`Generating task files from: ${tasksPath}`)); + console.log(chalk.blue(`Output directory: ${outputDir}`)); + + await generateTaskFiles(tasksPath, outputDir); + }); + + // set-status command + programInstance + .command("set-status") + .alias("mark") + .alias("set") + .description("Set the status of a task") + .option( + "-i, --id ", + "Task ID (can be comma-separated for multiple tasks)" + ) + .option( + "-s, --status ", + `New status (one of: ${TASK_STATUS_OPTIONS.join(", ")})` + ) + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .action(async (options) => { + const tasksPath = options.file; + const taskId = options.id; + const status = options.status; + + if (!taskId || !status) { + console.error(chalk.red("Error: Both --id and --status are required")); + process.exit(1); + } + + if (!isValidTaskStatus(status)) { + console.error( + chalk.red( + `Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}` + ) + ); + + process.exit(1); + } + + console.log( + chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`) + ); + + await setTaskStatus(tasksPath, taskId, status); + }); + + // list command + programInstance + .command("list") + .description("List all tasks") + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option( + "-r, --report ", + "Path to the complexity report file", + "scripts/task-complexity-report.json" + ) + .option("-s, --status ", "Filter by status") + .option("--with-subtasks", "Show subtasks for each task") + .action(async (options) => { + const tasksPath = options.file; + const reportPath = options.report; + const statusFilter = options.status; + const withSubtasks = options.withSubtasks || false; + + console.log(chalk.blue(`Listing tasks from: ${tasksPath}`)); + if (statusFilter) { + console.log(chalk.blue(`Filtering by status: ${statusFilter}`)); + } + if (withSubtasks) { + console.log(chalk.blue("Including subtasks in listing")); + } + + await listTasks(tasksPath, statusFilter, reportPath, withSubtasks); + }); + + // expand command + programInstance + .command("expand") + .description("Expand a task into subtasks using AI") + .option("-i, --id ", "ID of the task to expand") + .option( + "-a, --all", + "Expand all pending tasks based on complexity analysis" + ) + .option( + "-n, --num ", + "Number of subtasks to generate (uses complexity analysis by default if available)" + ) + .option( + "-r, --research", + "Enable research-backed generation (e.g., using Perplexity)", + false + ) + .option("-p, --prompt ", "Additional context for subtask generation") + .option("-f, --force", "Force expansion even if subtasks exist", false) // Ensure force option exists + .option( + "--file ", + "Path to the tasks file (relative to project root)", + "tasks/tasks.json" + ) // Allow file override + .action(async (options) => { + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red("Error: Could not find project root.")); + process.exit(1); + } + const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path + + if (options.all) { + // --- Handle expand --all --- + console.log(chalk.blue("Expanding all pending tasks...")); + // Updated call to the refactored expandAllTasks + try { + const result = await expandAllTasks( + tasksPath, + options.num, // Pass num + options.research, // Pass research flag + options.prompt, // Pass additional context + options.force, // Pass force flag + {} // Pass empty context for CLI calls + // outputFormat defaults to 'text' in expandAllTasks for CLI + ); + } catch (error) { + console.error( + chalk.red(`Error expanding all tasks: ${error.message}`) + ); + process.exit(1); + } + } else if (options.id) { + // --- Handle expand --id (Should be correct from previous refactor) --- + if (!options.id) { + console.error( + chalk.red("Error: Task ID is required unless using --all.") + ); + process.exit(1); + } + + console.log(chalk.blue(`Expanding task ${options.id}...`)); + try { + // Call the refactored expandTask function + await expandTask( + tasksPath, + options.id, + options.num, + options.research, + options.prompt, + {}, // Pass empty context for CLI calls + options.force // Pass the force flag down + ); + // expandTask logs its own success/failure for single task + } catch (error) { + console.error( + chalk.red(`Error expanding task ${options.id}: ${error.message}`) + ); + process.exit(1); + } + } else { + console.error( + chalk.red("Error: You must specify either a task ID (--id) or --all.") + ); + programInstance.help(); // Show help + } + }); + + // analyze-complexity command + programInstance + .command("analyze-complexity") + .description( + `Analyze tasks and generate expansion recommendations${chalk.reset("")}` + ) + .option( + "-o, --output ", + "Output file path for the report", + "scripts/task-complexity-report.json" + ) + .option( + "-m, --model ", + "LLM model to use for analysis (defaults to configured model)" + ) + .option( + "-t, --threshold ", + "Minimum complexity score to recommend expansion (1-10)", + "5" + ) + .option("-f, --file ", "Path to the tasks file", "tasks/tasks.json") + .option( + "-r, --research", + "Use Perplexity AI for research-backed complexity analysis" + ) + .option( + "-i, --id ", + 'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")' + ) + .option("--from ", "Starting task ID in a range to analyze") + .option("--to ", "Ending task ID in a range to analyze") + .action(async (options) => { + const tasksPath = options.file || "tasks/tasks.json"; + const outputPath = options.output; + const modelOverride = options.model; + const thresholdScore = parseFloat(options.threshold); + const useResearch = options.research || false; + + console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`)); + console.log(chalk.blue(`Output report will be saved to: ${outputPath}`)); + + if (options.id) { + console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`)); + } else if (options.from || options.to) { + const fromStr = options.from ? options.from : "first"; + const toStr = options.to ? options.to : "last"; + console.log( + chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`) + ); + } + + if (useResearch) { + console.log( + chalk.blue( + "Using Perplexity AI for research-backed complexity analysis" + ) + ); + } + + await analyzeTaskComplexity(options); + }); + + // research command + programInstance + .command("research") + .description("Perform AI-powered research queries with project context") + .argument("", "Research prompt to investigate") + .option("--file ", "Path to the tasks file", "tasks/tasks.json") + .option( + "-i, --id ", + 'Comma-separated task/subtask IDs to include as context (e.g., "15,16.2")' + ) + .option( + "-f, --files ", + "Comma-separated file paths to include as context" + ) + .option( + "-c, --context ", + "Additional custom context to include in the research prompt" + ) + .option( + "-t, --tree", + "Include project file tree structure in the research context" + ) + .option( + "-s, --save ", + "Save research results to the specified task/subtask(s)" + ) + .option( + "-d, --detail ", + "Output detail level: low, medium, high", + "medium" + ) + .action(async (prompt, options) => { + // Parameter validation + if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) { + console.error( + chalk.red("Error: Research prompt is required and cannot be empty") + ); + process.exit(1); + } + + // Validate detail level + const validDetailLevels = ["low", "medium", "high"]; + if ( + options.detail && + !validDetailLevels.includes(options.detail.toLowerCase()) + ) { + console.error( + chalk.red( + `Error: Detail level must be one of: ${validDetailLevels.join(", ")}` + ) + ); + process.exit(1); + } + + // Validate and parse task IDs if provided + let taskIds = []; + if (options.id) { + try { + taskIds = options.id.split(",").map((id) => { + const trimmedId = id.trim(); + // Support both task IDs (e.g., "15") and subtask IDs (e.g., "15.2") + if (!/^\d+(\.\d+)?$/.test(trimmedId)) { + throw new Error( + `Invalid task ID format: "${trimmedId}". Expected format: "15" or "15.2"` + ); + } + return trimmedId; + }); + } catch (error) { + console.error(chalk.red(`Error parsing task IDs: ${error.message}`)); + process.exit(1); + } + } + + // Validate and parse file paths if provided + let filePaths = []; + if (options.files) { + try { + filePaths = options.files.split(",").map((filePath) => { + const trimmedPath = filePath.trim(); + if (trimmedPath.length === 0) { + throw new Error("Empty file path provided"); + } + return trimmedPath; + }); + } catch (error) { + console.error( + chalk.red(`Error parsing file paths: ${error.message}`) + ); + process.exit(1); + } + } + + // Validate save option if provided + if (options.save) { + const saveTarget = options.save.trim(); + if (saveTarget.length === 0) { + console.error(chalk.red("Error: Save target cannot be empty")); + process.exit(1); + } + // Check if it's a valid file path (basic validation) + if (saveTarget.includes("..") || saveTarget.startsWith("/")) { + console.error( + chalk.red( + 'Error: Save path must be relative and cannot contain ".."' + ) + ); + process.exit(1); + } + } + + // Determine project root and tasks file path + const projectRoot = findProjectRoot() || "."; + const tasksPath = + options.file || path.join(projectRoot, "tasks", "tasks.json"); + + // Validate tasks file exists if task IDs are specified + if (taskIds.length > 0) { + try { + const tasksData = readJSON(tasksPath); + if (!tasksData || !tasksData.tasks) { + console.error( + chalk.red(`Error: No valid tasks found in ${tasksPath}`) + ); + process.exit(1); + } + } catch (error) { + console.error( + chalk.red(`Error reading tasks file: ${error.message}`) + ); + process.exit(1); + } + } + + // Validate file paths exist if specified + if (filePaths.length > 0) { + for (const filePath of filePaths) { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(projectRoot, filePath); + if (!fs.existsSync(fullPath)) { + console.error(chalk.red(`Error: File not found: ${filePath}`)); + process.exit(1); + } + } + } + + // Create validated parameters object + const validatedParams = { + prompt: prompt.trim(), + taskIds: taskIds, + filePaths: filePaths, + customContext: options.context ? options.context.trim() : null, + includeProjectTree: !!options.tree, + saveTarget: options.save ? options.save.trim() : null, + detailLevel: options.detail ? options.detail.toLowerCase() : "medium", + tasksPath: tasksPath, + projectRoot: projectRoot, + }; + + // Display what we're about to do + console.log(chalk.blue(`Researching: "${validatedParams.prompt}"`)); + + if (validatedParams.taskIds.length > 0) { + console.log( + chalk.gray(`Task context: ${validatedParams.taskIds.join(", ")}`) + ); + } + + if (validatedParams.filePaths.length > 0) { + console.log( + chalk.gray(`File context: ${validatedParams.filePaths.join(", ")}`) + ); + } + + if (validatedParams.customContext) { + console.log( + chalk.gray( + `Custom context: ${validatedParams.customContext.substring(0, 50)}${validatedParams.customContext.length > 50 ? "..." : ""}` + ) + ); + } + + if (validatedParams.includeProjectTree) { + console.log(chalk.gray("Including project file tree")); + } + + console.log(chalk.gray(`Detail level: ${validatedParams.detailLevel}`)); + + try { + // Import the research function + const { performResearch } = await import("./task-manager/research.js"); + + // Prepare research options + const researchOptions = { + taskIds: validatedParams.taskIds, + filePaths: validatedParams.filePaths, + customContext: validatedParams.customContext || "", + includeProjectTree: validatedParams.includeProjectTree, + detailLevel: validatedParams.detailLevel, + projectRoot: validatedParams.projectRoot, + }; + + // Execute research + const result = await performResearch( + validatedParams.prompt, + researchOptions, + { + commandName: "research", + outputType: "cli", + }, + "text" + ); + + // Save results if requested + if (validatedParams.saveTarget) { + const saveContent = `# Research Query: ${validatedParams.prompt} **Detail Level:** ${result.detailLevel} **Context Size:** ${result.contextSize} characters @@ -1642,964 +1642,962 @@ function registerCommands(programInstance) { ${result.result} `; - fs.writeFileSync(validatedParams.saveTarget, saveContent, 'utf-8'); - console.log( - chalk.green(`\nšŸ’¾ Results saved to: ${validatedParams.saveTarget}`) - ); - } - } catch (error) { - console.error(chalk.red(`\nāŒ Research failed: ${error.message}`)); - process.exit(1); - } - }); - - // clear-subtasks command - programInstance - .command('clear-subtasks') - .description('Clear subtasks from specified tasks') - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-i, --id ', - 'Task IDs (comma-separated) to clear subtasks from' - ) - .option('--all', 'Clear subtasks from all tasks') - .action(async (options) => { - const tasksPath = options.file; - const taskIds = options.id; - const all = options.all; - - if (!taskIds && !all) { - console.error( - chalk.red( - 'Error: Please specify task IDs with --id= or use --all to clear all tasks' - ) - ); - process.exit(1); - } - - if (all) { - // If --all is specified, get all task IDs - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - console.error(chalk.red('Error: No valid tasks found')); - process.exit(1); - } - const allIds = data.tasks.map((t) => t.id).join(','); - clearSubtasks(tasksPath, allIds); - } else { - clearSubtasks(tasksPath, taskIds); - } - }); - - // add-task command - programInstance - .command('add-task') - .description('Add a new task using AI, optionally providing manual details') - .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-p, --prompt ', - 'Description of the task to add (required if not using manual fields)' - ) - .option('-t, --title ', 'Task title (for manual task creation)') - .option( - '-d, --description <description>', - 'Task description (for manual task creation)' - ) - .option( - '--details <details>', - 'Implementation details (for manual task creation)' - ) - .option( - '--dependencies <dependencies>', - 'Comma-separated list of task IDs this task depends on' - ) - .option( - '--priority <priority>', - 'Task priority (high, medium, low)', - 'medium' - ) - .option( - '-r, --research', - 'Whether to use research capabilities for task creation' - ) - .action(async (options) => { - const isManualCreation = options.title && options.description; - - // Validate that either prompt or title+description are provided - if (!options.prompt && !isManualCreation) { - console.error( - chalk.red( - 'Error: Either --prompt or both --title and --description must be provided' - ) - ); - process.exit(1); - } - - const tasksPath = - options.file || - path.join(findProjectRoot() || '.', 'tasks', 'tasks.json') || // Ensure tasksPath is also relative to a found root or current dir - 'tasks/tasks.json'; - - // Correctly determine projectRoot - const projectRoot = findProjectRoot(); - - let manualTaskData = null; - if (isManualCreation) { - manualTaskData = { - title: options.title, - description: options.description, - details: options.details || '', - testStrategy: options.testStrategy || '' - }; - // Restore specific logging for manual creation - console.log( - chalk.blue(`Creating task manually with title: "${options.title}"`) - ); - } else { - // Restore specific logging for AI creation - console.log( - chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`) - ); - } - - // Log dependencies and priority if provided (restored) - const dependenciesArray = options.dependencies - ? options.dependencies.split(',').map((id) => id.trim()) - : []; - if (dependenciesArray.length > 0) { - console.log( - chalk.blue(`Dependencies: [${dependenciesArray.join(', ')}]`) - ); - } - if (options.priority) { - console.log(chalk.blue(`Priority: ${options.priority}`)); - } - - const context = { - projectRoot, - commandName: 'add-task', - outputType: 'cli' - }; - - try { - const { newTaskId, telemetryData } = await addTask( - tasksPath, - options.prompt, - dependenciesArray, - options.priority, - context, - 'text', - manualTaskData, - options.research - ); - - // addTask handles detailed CLI success logging AND telemetry display when outputFormat is 'text' - // No need to call displayAiUsageSummary here anymore. - } catch (error) { - console.error(chalk.red(`Error adding task: ${error.message}`)); - if (error.details) { - console.error(chalk.red(error.details)); - } - process.exit(1); - } - }); - - // next command - programInstance - .command('next') - .description( - `Show the next task to work on based on dependencies and status${chalk.reset('')}` - ) - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-r, --report <report>', - 'Path to the complexity report file', - 'scripts/task-complexity-report.json' - ) - .action(async (options) => { - const tasksPath = options.file; - const reportPath = options.report; - await displayNextTask(tasksPath, reportPath); - }); - - // show command - programInstance - .command('show') - .description( - `Display detailed information about one or more tasks${chalk.reset('')}` - ) - .argument('[id]', 'Task ID(s) to show (comma-separated for multiple)') - .option( - '-i, --id <id>', - 'Task ID(s) to show (comma-separated for multiple)' - ) - .option('-s, --status <status>', 'Filter subtasks by status') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-r, --report <report>', - 'Path to the complexity report file', - 'scripts/task-complexity-report.json' - ) - .action(async (taskId, options) => { - const idArg = taskId || options.id; - const statusFilter = options.status; - - if (!idArg) { - console.error(chalk.red('Error: Please provide a task ID')); - process.exit(1); - } - - const tasksPath = options.file; - const reportPath = options.report; - - // Check if multiple IDs are provided (comma-separated) - const taskIds = idArg - .split(',') - .map((id) => id.trim()) - .filter((id) => id.length > 0); - - if (taskIds.length > 1) { - // Multiple tasks - use compact summary view with interactive drill-down - await displayMultipleTasksSummary( - tasksPath, - taskIds, - reportPath, - statusFilter - ); - } else { - // Single task - use detailed view - await displayTaskById(tasksPath, taskIds[0], reportPath, statusFilter); - } - }); - - // add-dependency command - programInstance - .command('add-dependency') - .description('Add a dependency to a task') - .option('-i, --id <id>', 'Task ID to add dependency to') - .option('-d, --depends-on <id>', 'Task ID that will become a dependency') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (options) => { - const tasksPath = options.file; - const taskId = options.id; - const dependencyId = options.dependsOn; - - if (!taskId || !dependencyId) { - console.error( - chalk.red('Error: Both --id and --depends-on are required') - ); - process.exit(1); - } - - // Handle subtask IDs correctly by preserving the string format for IDs containing dots - // Only use parseInt for simple numeric IDs - const formattedTaskId = taskId.includes('.') - ? taskId - : parseInt(taskId, 10); - const formattedDependencyId = dependencyId.includes('.') - ? dependencyId - : parseInt(dependencyId, 10); - - await addDependency(tasksPath, formattedTaskId, formattedDependencyId); - }); - - // remove-dependency command - programInstance - .command('remove-dependency') - .description('Remove a dependency from a task') - .option('-i, --id <id>', 'Task ID to remove dependency from') - .option('-d, --depends-on <id>', 'Task ID to remove as a dependency') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (options) => { - const tasksPath = options.file; - const taskId = options.id; - const dependencyId = options.dependsOn; - - if (!taskId || !dependencyId) { - console.error( - chalk.red('Error: Both --id and --depends-on are required') - ); - process.exit(1); - } - - // Handle subtask IDs correctly by preserving the string format for IDs containing dots - // Only use parseInt for simple numeric IDs - const formattedTaskId = taskId.includes('.') - ? taskId - : parseInt(taskId, 10); - const formattedDependencyId = dependencyId.includes('.') - ? dependencyId - : parseInt(dependencyId, 10); - - await removeDependency(tasksPath, formattedTaskId, formattedDependencyId); - }); - - // validate-dependencies command - programInstance - .command('validate-dependencies') - .description( - `Identify invalid dependencies without fixing them${chalk.reset('')}` - ) - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (options) => { - await validateDependenciesCommand(options.file); - }); - - // fix-dependencies command - programInstance - .command('fix-dependencies') - .description(`Fix invalid dependencies automatically${chalk.reset('')}`) - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .action(async (options) => { - await fixDependenciesCommand(options.file); - }); - - // complexity-report command - programInstance - .command('complexity-report') - .description(`Display the complexity analysis report${chalk.reset('')}`) - .option( - '-f, --file <file>', - 'Path to the report file', - 'scripts/task-complexity-report.json' - ) - .action(async (options) => { - await displayComplexityReport(options.file); - }); - - // add-subtask command - programInstance - .command('add-subtask') - .description('Add a subtask to an existing task') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option('-p, --parent <id>', 'Parent task ID (required)') - .option('-i, --task-id <id>', 'Existing task ID to convert to subtask') - .option( - '-t, --title <title>', - 'Title for the new subtask (when creating a new subtask)' - ) - .option('-d, --description <text>', 'Description for the new subtask') - .option('--details <text>', 'Implementation details for the new subtask') - .option( - '--dependencies <ids>', - 'Comma-separated list of dependency IDs for the new subtask' - ) - .option('-s, --status <status>', 'Status for the new subtask', 'pending') - .option('--skip-generate', 'Skip regenerating task files') - .action(async (options) => { - const tasksPath = options.file; - const parentId = options.parent; - const existingTaskId = options.taskId; - const generateFiles = !options.skipGenerate; - - if (!parentId) { - console.error( - chalk.red( - 'Error: --parent parameter is required. Please provide a parent task ID.' - ) - ); - showAddSubtaskHelp(); - process.exit(1); - } - - // Parse dependencies if provided - let dependencies = []; - if (options.dependencies) { - dependencies = options.dependencies.split(',').map((id) => { - // Handle both regular IDs and dot notation - return id.includes('.') ? id.trim() : parseInt(id.trim(), 10); - }); - } - - try { - if (existingTaskId) { - // Convert existing task to subtask - console.log( - chalk.blue( - `Converting task ${existingTaskId} to a subtask of ${parentId}...` - ) - ); - await addSubtask( - tasksPath, - parentId, - existingTaskId, - null, - generateFiles - ); - console.log( - chalk.green( - `āœ“ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}` - ) - ); - } else if (options.title) { - // Create new subtask with provided data - console.log( - chalk.blue(`Creating new subtask for parent task ${parentId}...`) - ); - - const newSubtaskData = { - title: options.title, - description: options.description || '', - details: options.details || '', - status: options.status || 'pending', - dependencies: dependencies - }; - - const subtask = await addSubtask( - tasksPath, - parentId, - null, - newSubtaskData, - generateFiles - ); - console.log( - chalk.green( - `āœ“ New subtask ${parentId}.${subtask.id} successfully created` - ) - ); - - // Display success message and suggested next steps - console.log( - boxen( - chalk.white.bold( - `Subtask ${parentId}.${subtask.id} Added Successfully` - ) + - '\n\n' + - chalk.white(`Title: ${subtask.title}`) + - '\n' + - chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + - '\n' + - (dependencies.length > 0 - ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + - '\n' - : '') + - '\n' + - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks` - ) + - '\n' + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it` - ), - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } else { - console.error( - chalk.red('Error: Either --task-id or --title must be provided.') - ); - console.log( - boxen( - chalk.white.bold('Usage Examples:') + - '\n\n' + - chalk.white('Convert existing task to subtask:') + - '\n' + - chalk.yellow( - ` task-master add-subtask --parent=5 --task-id=8` - ) + - '\n\n' + - chalk.white('Create new subtask:') + - '\n' + - chalk.yellow( - ` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"` - ) + - '\n\n', - { padding: 1, borderColor: 'blue', borderStyle: 'round' } - ) - ); - process.exit(1); - } - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - showAddSubtaskHelp(); - process.exit(1); - } - }) - .on('error', function (err) { - console.error(chalk.red(`Error: ${err.message}`)); - showAddSubtaskHelp(); - process.exit(1); - }); - - // Helper function to show add-subtask command help - function showAddSubtaskHelp() { - console.log( - boxen( - chalk.white.bold('Add Subtask Command Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master add-subtask --parent=<id> [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -p, --parent <id> Parent task ID (required)\n' + - ' -i, --task-id <id> Existing task ID to convert to subtask\n' + - ' -t, --title <title> Title for the new subtask\n' + - ' -d, --description <text> Description for the new subtask\n' + - ' --details <text> Implementation details for the new subtask\n' + - ' --dependencies <ids> Comma-separated list of dependency IDs\n' + - ' -s, --status <status> Status for the new subtask (default: "pending")\n' + - ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + - ' --skip-generate Skip regenerating task files\n\n' + - chalk.cyan('Examples:') + - '\n' + - ' task-master add-subtask --parent=5 --task-id=8\n' + - ' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"', - { padding: 1, borderColor: 'blue', borderStyle: 'round' } - ) - ); - } - - // remove-subtask command - programInstance - .command('remove-subtask') - .description('Remove a subtask from its parent task') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '-i, --id <id>', - 'Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)' - ) - .option( - '-c, --convert', - 'Convert the subtask to a standalone task instead of deleting it' - ) - .option('--skip-generate', 'Skip regenerating task files') - .action(async (options) => { - const tasksPath = options.file; - const subtaskIds = options.id; - const convertToTask = options.convert || false; - const generateFiles = !options.skipGenerate; - - if (!subtaskIds) { - console.error( - chalk.red( - 'Error: --id parameter is required. Please provide subtask ID(s) in format "parentId.subtaskId".' - ) - ); - showRemoveSubtaskHelp(); - process.exit(1); - } - - try { - // Split by comma to support multiple subtask IDs - const subtaskIdArray = subtaskIds.split(',').map((id) => id.trim()); - - for (const subtaskId of subtaskIdArray) { - // Validate subtask ID format - if (!subtaskId.includes('.')) { - console.error( - chalk.red( - `Error: Subtask ID "${subtaskId}" must be in format "parentId.subtaskId"` - ) - ); - showRemoveSubtaskHelp(); - process.exit(1); - } - - console.log(chalk.blue(`Removing subtask ${subtaskId}...`)); - if (convertToTask) { - console.log( - chalk.blue('The subtask will be converted to a standalone task') - ); - } - - const result = await removeSubtask( - tasksPath, - subtaskId, - convertToTask, - generateFiles - ); - - if (convertToTask && result) { - // Display success message and next steps for converted task - console.log( - boxen( - chalk.white.bold( - `Subtask ${subtaskId} Converted to Task #${result.id}` - ) + - '\n\n' + - chalk.white(`Title: ${result.title}`) + - '\n' + - chalk.white(`Status: ${getStatusWithColor(result.status)}`) + - '\n' + - chalk.white( - `Dependencies: ${result.dependencies.join(', ')}` - ) + - '\n\n' + - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task` - ) + - '\n' + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it` - ), - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } else { - // Display success message for deleted subtask - console.log( - boxen( - chalk.white.bold(`Subtask ${subtaskId} Removed`) + - '\n\n' + - chalk.white('The subtask has been successfully deleted.'), - { - padding: 1, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); - } - } - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - showRemoveSubtaskHelp(); - process.exit(1); - } - }) - .on('error', function (err) { - console.error(chalk.red(`Error: ${err.message}`)); - showRemoveSubtaskHelp(); - process.exit(1); - }); - - // Helper function to show remove-subtask command help - function showRemoveSubtaskHelp() { - console.log( - boxen( - chalk.white.bold('Remove Subtask Command Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' + - ' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' + - ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + - ' --skip-generate Skip regenerating task files\n\n' + - chalk.cyan('Examples:') + - '\n' + - ' task-master remove-subtask --id=5.2\n' + - ' task-master remove-subtask --id=5.2,6.3,7.1\n' + - ' task-master remove-subtask --id=5.2 --convert', - { padding: 1, borderColor: 'blue', borderStyle: 'round' } - ) - ); - } - - // remove-task command - programInstance - .command('remove-task') - .description('Remove one or more tasks or subtasks permanently') - .description('Remove one or more tasks or subtasks permanently') - .option( - '-i, --id <ids>', - 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")' - ) - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option('-y, --yes', 'Skip confirmation prompt', false) - .action(async (options) => { - const tasksPath = options.file; - const taskIdsString = options.id; - - if (!taskIdsString) { - console.error(chalk.red('Error: Task ID(s) are required')); - console.error( - chalk.yellow( - 'Usage: task-master remove-task --id=<taskId1,taskId2...>' - ) - ); - process.exit(1); - } - - const taskIdsToRemove = taskIdsString - .split(',') - .map((id) => id.trim()) - .filter(Boolean); - - if (taskIdsToRemove.length === 0) { - console.error(chalk.red('Error: No valid task IDs provided.')); - process.exit(1); - } - - try { - // Read data once for checks and confirmation - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - console.error( - chalk.red(`Error: No valid tasks found in ${tasksPath}`) - ); - process.exit(1); - } - - const existingTasksToRemove = []; - const nonExistentIds = []; - let totalSubtasksToDelete = 0; - const dependentTaskMessages = []; - - for (const taskId of taskIdsToRemove) { - if (!taskExists(data.tasks, taskId)) { - nonExistentIds.push(taskId); - } else { - // Correctly extract the task object from the result of findTaskById - const findResult = findTaskById(data.tasks, taskId); - const taskObject = findResult.task; // Get the actual task/subtask object - - if (taskObject) { - existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object - - // If it's a main task, count its subtasks and check dependents - if (!taskObject.isSubtask) { - // Check the actual task object - if (taskObject.subtasks && taskObject.subtasks.length > 0) { - totalSubtasksToDelete += taskObject.subtasks.length; - } - const dependentTasks = data.tasks.filter( - (t) => - t.dependencies && - t.dependencies.includes(parseInt(taskId, 10)) - ); - if (dependentTasks.length > 0) { - dependentTaskMessages.push( - ` - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(', ')})` - ); - } - } - } else { - // Handle case where findTaskById returned null for the task property (should be rare) - nonExistentIds.push(`${taskId} (error finding details)`); - } - } - } - - if (nonExistentIds.length > 0) { - console.warn( - chalk.yellow( - `Warning: The following task IDs were not found: ${nonExistentIds.join(', ')}` - ) - ); - } - - if (existingTasksToRemove.length === 0) { - console.log(chalk.blue('No existing tasks found to remove.')); - process.exit(0); - } - - // Skip confirmation if --yes flag is provided - if (!options.yes) { - console.log(); - console.log( - chalk.red.bold( - `āš ļø WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):` - ) - ); - console.log(); - - existingTasksToRemove.forEach(({ id, task }) => { - if (!task) return; // Should not happen due to taskExists check, but safeguard - if (task.isSubtask) { - // Subtask - title is directly on the task object - console.log( - chalk.white(` Subtask ${id}: ${task.title || '(no title)'}`) - ); - // Optionally show parent context if available - if (task.parentTask) { - console.log( - chalk.gray( - ` (Parent: ${task.parentTask.id} - ${task.parentTask.title || '(no title)'})` - ) - ); - } - } else { - // Main task - title is directly on the task object - console.log( - chalk.white.bold(` Task ${id}: ${task.title || '(no title)'}`) - ); - } - }); - - if (totalSubtasksToDelete > 0) { - console.log( - chalk.yellow( - `āš ļø This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!` - ) - ); - } - - if (dependentTaskMessages.length > 0) { - console.log( - chalk.yellow( - 'āš ļø Warning: Dependencies on the following tasks will be removed:' - ) - ); - dependentTaskMessages.forEach((msg) => - console.log(chalk.yellow(msg)) - ); - } - - console.log(); - - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.red.bold( - `Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?` - ), - default: false - } - ]); - - if (!confirm) { - console.log(chalk.blue('Task deletion cancelled.')); - process.exit(0); - } - } - - const indicator = startLoadingIndicator( - `Removing ${existingTasksToRemove.length} task(s)/subtask(s)...` - ); - - // Use the string of existing IDs for the core function - const existingIdsString = existingTasksToRemove - .map(({ id }) => id) - .join(','); - const result = await removeTask(tasksPath, existingIdsString); - - stopLoadingIndicator(indicator); - - if (result.success) { - console.log( - boxen( - chalk.green( - `Successfully removed ${result.removedTasks.length} task(s)/subtask(s).` - ) + - (result.message ? `\n\nDetails:\n${result.message}` : '') + - (result.error - ? `\n\nWarnings:\n${chalk.yellow(result.error)}` - : ''), - { padding: 1, borderColor: 'green', borderStyle: 'round' } - ) - ); - } else { - console.error( - boxen( - chalk.red( - `Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).` - ) + - (result.message ? `\n\nDetails:\n${result.message}` : '') + - (result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ''), - { - padding: 1, - borderColor: 'red', - borderStyle: 'round' - } - ) - ); - process.exit(1); // Exit with error code if any part failed - } - - // Log any initially non-existent IDs again for clarity - if (nonExistentIds.length > 0) { - console.warn( - chalk.yellow( - `Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(', ')}` - ) - ); - - // Exit with error if any removals failed - if (result.removedTasks.length === 0) { - process.exit(1); - } - } - } catch (error) { - console.error( - chalk.red(`Error: ${error.message || 'An unknown error occurred'}`) - ); - process.exit(1); - } - }); - - // init command (Directly calls the implementation from init.js) - programInstance - .command('init') - .description('Initialize a new project with Task Master structure') - .option('-y, --yes', 'Skip prompts and use default values') - .option('-n, --name <name>', 'Project name') - .option('-d, --description <description>', 'Project description') - .option('-v, --version <version>', 'Project version', '0.1.0') // Set default here - .option('-a, --author <author>', 'Author name') - .option('--skip-install', 'Skip installing dependencies') - .option('--dry-run', 'Show what would be done without making changes') - .option('--aliases', 'Add shell aliases (tm, taskmaster)') - .action(async (cmdOptions) => { - // cmdOptions contains parsed arguments - try { - console.log('DEBUG: Running init command action in commands.js'); - console.log( - 'DEBUG: Options received by action:', - JSON.stringify(cmdOptions) - ); - // Directly call the initializeProject function, passing the parsed options - await initializeProject(cmdOptions); - // initializeProject handles its own flow, including potential process.exit() - } catch (error) { - console.error( - chalk.red(`Error during initialization: ${error.message}`) - ); - process.exit(1); - } - }); - - // models command - programInstance - .command('models') - .description('Manage AI model configurations') - .option( - '--set-main <model_id>', - 'Set the primary model for task generation/updates' - ) - .option( - '--set-research <model_id>', - 'Set the model for research-backed operations' - ) - .option( - '--set-fallback <model_id>', - 'Set the model to use if the primary fails' - ) - .option('--setup', 'Run interactive setup to configure models') - .option( - '--openrouter', - 'Allow setting a custom OpenRouter model ID (use with --set-*) ' - ) - .option( - '--ollama', - 'Allow setting a custom Ollama model ID (use with --set-*) ' - ) - .option( - '--bedrock', - 'Allow setting a custom Bedrock model ID (use with --set-*) ' - ) - .addHelpText( - 'after', - ` + fs.writeFileSync(validatedParams.saveTarget, saveContent, "utf-8"); + console.log( + chalk.green(`\nšŸ’¾ Results saved to: ${validatedParams.saveTarget}`) + ); + } + } catch (error) { + console.error(chalk.red(`\nāŒ Research failed: ${error.message}`)); + process.exit(1); + } + }); + + // clear-subtasks command + programInstance + .command("clear-subtasks") + .description("Clear subtasks from specified tasks") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "-i, --id <ids>", + "Task IDs (comma-separated) to clear subtasks from" + ) + .option("--all", "Clear subtasks from all tasks") + .action(async (options) => { + const tasksPath = options.file; + const taskIds = options.id; + const all = options.all; + + if (!taskIds && !all) { + console.error( + chalk.red( + "Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks" + ) + ); + process.exit(1); + } + + if (all) { + // If --all is specified, get all task IDs + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + console.error(chalk.red("Error: No valid tasks found")); + process.exit(1); + } + const allIds = data.tasks.map((t) => t.id).join(","); + clearSubtasks(tasksPath, allIds); + } else { + clearSubtasks(tasksPath, taskIds); + } + }); + + // add-task command + programInstance + .command("add-task") + .description("Add a new task using AI, optionally providing manual details") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "-p, --prompt <prompt>", + "Description of the task to add (required if not using manual fields)" + ) + .option("-t, --title <title>", "Task title (for manual task creation)") + .option( + "-d, --description <description>", + "Task description (for manual task creation)" + ) + .option( + "--details <details>", + "Implementation details (for manual task creation)" + ) + .option( + "--dependencies <dependencies>", + "Comma-separated list of task IDs this task depends on" + ) + .option( + "--priority <priority>", + "Task priority (high, medium, low)", + "medium" + ) + .option( + "-r, --research", + "Whether to use research capabilities for task creation" + ) + .action(async (options) => { + const isManualCreation = options.title && options.description; + + // Validate that either prompt or title+description are provided + if (!options.prompt && !isManualCreation) { + console.error( + chalk.red( + "Error: Either --prompt or both --title and --description must be provided" + ) + ); + process.exit(1); + } + + const tasksPath = + options.file || + path.join(findProjectRoot() || ".", "tasks", "tasks.json") || // Ensure tasksPath is also relative to a found root or current dir + "tasks/tasks.json"; + + // Correctly determine projectRoot + const projectRoot = findProjectRoot(); + + let manualTaskData = null; + if (isManualCreation) { + manualTaskData = { + title: options.title, + description: options.description, + details: options.details || "", + testStrategy: options.testStrategy || "", + }; + // Restore specific logging for manual creation + console.log( + chalk.blue(`Creating task manually with title: "${options.title}"`) + ); + } else { + // Restore specific logging for AI creation + console.log( + chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`) + ); + } + + // Log dependencies and priority if provided (restored) + const dependenciesArray = options.dependencies + ? options.dependencies.split(",").map((id) => id.trim()) + : []; + if (dependenciesArray.length > 0) { + console.log( + chalk.blue(`Dependencies: [${dependenciesArray.join(", ")}]`) + ); + } + if (options.priority) { + console.log(chalk.blue(`Priority: ${options.priority}`)); + } + + const context = { + projectRoot, + commandName: "add-task", + outputType: "cli", + }; + + try { + const { newTaskId, telemetryData } = await addTask( + tasksPath, + options.prompt, + dependenciesArray, + options.priority, + context, + "text", + manualTaskData, + options.research + ); + + // addTask handles detailed CLI success logging AND telemetry display when outputFormat is 'text' + // No need to call displayAiUsageSummary here anymore. + } catch (error) { + // addTask already handles error reporting for CLI output + // Just exit with error code + process.exit(1); + } + }); + + // next command + programInstance + .command("next") + .description( + `Show the next task to work on based on dependencies and status${chalk.reset("")}` + ) + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "-r, --report <report>", + "Path to the complexity report file", + "scripts/task-complexity-report.json" + ) + .action(async (options) => { + const tasksPath = options.file; + const reportPath = options.report; + await displayNextTask(tasksPath, reportPath); + }); + + // show command + programInstance + .command("show") + .description( + `Display detailed information about one or more tasks${chalk.reset("")}` + ) + .argument("[id]", "Task ID(s) to show (comma-separated for multiple)") + .option( + "-i, --id <id>", + "Task ID(s) to show (comma-separated for multiple)" + ) + .option("-s, --status <status>", "Filter subtasks by status") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "-r, --report <report>", + "Path to the complexity report file", + "scripts/task-complexity-report.json" + ) + .action(async (taskId, options) => { + const idArg = taskId || options.id; + const statusFilter = options.status; + + if (!idArg) { + console.error(chalk.red("Error: Please provide a task ID")); + process.exit(1); + } + + const tasksPath = options.file; + const reportPath = options.report; + + // Check if multiple IDs are provided (comma-separated) + const taskIds = idArg + .split(",") + .map((id) => id.trim()) + .filter((id) => id.length > 0); + + if (taskIds.length > 1) { + // Multiple tasks - use compact summary view with interactive drill-down + await displayMultipleTasksSummary( + tasksPath, + taskIds, + reportPath, + statusFilter + ); + } else { + // Single task - use detailed view + await displayTaskById(tasksPath, taskIds[0], reportPath, statusFilter); + } + }); + + // add-dependency command + programInstance + .command("add-dependency") + .description("Add a dependency to a task") + .option("-i, --id <id>", "Task ID to add dependency to") + .option("-d, --depends-on <id>", "Task ID that will become a dependency") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .action(async (options) => { + const tasksPath = options.file; + const taskId = options.id; + const dependencyId = options.dependsOn; + + if (!taskId || !dependencyId) { + console.error( + chalk.red("Error: Both --id and --depends-on are required") + ); + process.exit(1); + } + + // Handle subtask IDs correctly by preserving the string format for IDs containing dots + // Only use parseInt for simple numeric IDs + const formattedTaskId = taskId.includes(".") + ? taskId + : parseInt(taskId, 10); + const formattedDependencyId = dependencyId.includes(".") + ? dependencyId + : parseInt(dependencyId, 10); + + await addDependency(tasksPath, formattedTaskId, formattedDependencyId); + }); + + // remove-dependency command + programInstance + .command("remove-dependency") + .description("Remove a dependency from a task") + .option("-i, --id <id>", "Task ID to remove dependency from") + .option("-d, --depends-on <id>", "Task ID to remove as a dependency") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .action(async (options) => { + const tasksPath = options.file; + const taskId = options.id; + const dependencyId = options.dependsOn; + + if (!taskId || !dependencyId) { + console.error( + chalk.red("Error: Both --id and --depends-on are required") + ); + process.exit(1); + } + + // Handle subtask IDs correctly by preserving the string format for IDs containing dots + // Only use parseInt for simple numeric IDs + const formattedTaskId = taskId.includes(".") + ? taskId + : parseInt(taskId, 10); + const formattedDependencyId = dependencyId.includes(".") + ? dependencyId + : parseInt(dependencyId, 10); + + await removeDependency(tasksPath, formattedTaskId, formattedDependencyId); + }); + + // validate-dependencies command + programInstance + .command("validate-dependencies") + .description( + `Identify invalid dependencies without fixing them${chalk.reset("")}` + ) + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .action(async (options) => { + await validateDependenciesCommand(options.file); + }); + + // fix-dependencies command + programInstance + .command("fix-dependencies") + .description(`Fix invalid dependencies automatically${chalk.reset("")}`) + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .action(async (options) => { + await fixDependenciesCommand(options.file); + }); + + // complexity-report command + programInstance + .command("complexity-report") + .description(`Display the complexity analysis report${chalk.reset("")}`) + .option( + "-f, --file <file>", + "Path to the report file", + "scripts/task-complexity-report.json" + ) + .action(async (options) => { + await displayComplexityReport(options.file); + }); + + // add-subtask command + programInstance + .command("add-subtask") + .description("Add a subtask to an existing task") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option("-p, --parent <id>", "Parent task ID (required)") + .option("-i, --task-id <id>", "Existing task ID to convert to subtask") + .option( + "-t, --title <title>", + "Title for the new subtask (when creating a new subtask)" + ) + .option("-d, --description <text>", "Description for the new subtask") + .option("--details <text>", "Implementation details for the new subtask") + .option( + "--dependencies <ids>", + "Comma-separated list of dependency IDs for the new subtask" + ) + .option("-s, --status <status>", "Status for the new subtask", "pending") + .option("--skip-generate", "Skip regenerating task files") + .action(async (options) => { + const tasksPath = options.file; + const parentId = options.parent; + const existingTaskId = options.taskId; + const generateFiles = !options.skipGenerate; + + if (!parentId) { + console.error( + chalk.red( + "Error: --parent parameter is required. Please provide a parent task ID." + ) + ); + showAddSubtaskHelp(); + process.exit(1); + } + + // Parse dependencies if provided + let dependencies = []; + if (options.dependencies) { + dependencies = options.dependencies.split(",").map((id) => { + // Handle both regular IDs and dot notation + return id.includes(".") ? id.trim() : parseInt(id.trim(), 10); + }); + } + + try { + if (existingTaskId) { + // Convert existing task to subtask + console.log( + chalk.blue( + `Converting task ${existingTaskId} to a subtask of ${parentId}...` + ) + ); + await addSubtask( + tasksPath, + parentId, + existingTaskId, + null, + generateFiles + ); + console.log( + chalk.green( + `āœ“ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}` + ) + ); + } else if (options.title) { + // Create new subtask with provided data + console.log( + chalk.blue(`Creating new subtask for parent task ${parentId}...`) + ); + + const newSubtaskData = { + title: options.title, + description: options.description || "", + details: options.details || "", + status: options.status || "pending", + dependencies: dependencies, + }; + + const subtask = await addSubtask( + tasksPath, + parentId, + null, + newSubtaskData, + generateFiles + ); + console.log( + chalk.green( + `āœ“ New subtask ${parentId}.${subtask.id} successfully created` + ) + ); + + // Display success message and suggested next steps + console.log( + boxen( + chalk.white.bold( + `Subtask ${parentId}.${subtask.id} Added Successfully` + ) + + "\n\n" + + chalk.white(`Title: ${subtask.title}`) + + "\n" + + chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + + "\n" + + (dependencies.length > 0 + ? chalk.white(`Dependencies: ${dependencies.join(", ")}`) + + "\n" + : "") + + "\n" + + chalk.white.bold("Next Steps:") + + "\n" + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks` + ) + + "\n" + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it` + ), + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } else { + console.error( + chalk.red("Error: Either --task-id or --title must be provided.") + ); + console.log( + boxen( + chalk.white.bold("Usage Examples:") + + "\n\n" + + chalk.white("Convert existing task to subtask:") + + "\n" + + chalk.yellow( + ` task-master add-subtask --parent=5 --task-id=8` + ) + + "\n\n" + + chalk.white("Create new subtask:") + + "\n" + + chalk.yellow( + ` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"` + ) + + "\n\n", + { padding: 1, borderColor: "blue", borderStyle: "round" } + ) + ); + process.exit(1); + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + showAddSubtaskHelp(); + process.exit(1); + } + }) + .on("error", function (err) { + console.error(chalk.red(`Error: ${err.message}`)); + showAddSubtaskHelp(); + process.exit(1); + }); + + // Helper function to show add-subtask command help + function showAddSubtaskHelp() { + console.log( + boxen( + chalk.white.bold("Add Subtask Command Help") + + "\n\n" + + chalk.cyan("Usage:") + + "\n" + + ` task-master add-subtask --parent=<id> [options]\n\n` + + chalk.cyan("Options:") + + "\n" + + " -p, --parent <id> Parent task ID (required)\n" + + " -i, --task-id <id> Existing task ID to convert to subtask\n" + + " -t, --title <title> Title for the new subtask\n" + + " -d, --description <text> Description for the new subtask\n" + + " --details <text> Implementation details for the new subtask\n" + + " --dependencies <ids> Comma-separated list of dependency IDs\n" + + ' -s, --status <status> Status for the new subtask (default: "pending")\n' + + ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + + " --skip-generate Skip regenerating task files\n\n" + + chalk.cyan("Examples:") + + "\n" + + " task-master add-subtask --parent=5 --task-id=8\n" + + ' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"', + { padding: 1, borderColor: "blue", borderStyle: "round" } + ) + ); + } + + // remove-subtask command + programInstance + .command("remove-subtask") + .description("Remove a subtask from its parent task") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "-i, --id <id>", + 'Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)' + ) + .option( + "-c, --convert", + "Convert the subtask to a standalone task instead of deleting it" + ) + .option("--skip-generate", "Skip regenerating task files") + .action(async (options) => { + const tasksPath = options.file; + const subtaskIds = options.id; + const convertToTask = options.convert || false; + const generateFiles = !options.skipGenerate; + + if (!subtaskIds) { + console.error( + chalk.red( + 'Error: --id parameter is required. Please provide subtask ID(s) in format "parentId.subtaskId".' + ) + ); + showRemoveSubtaskHelp(); + process.exit(1); + } + + try { + // Split by comma to support multiple subtask IDs + const subtaskIdArray = subtaskIds.split(",").map((id) => id.trim()); + + for (const subtaskId of subtaskIdArray) { + // Validate subtask ID format + if (!subtaskId.includes(".")) { + console.error( + chalk.red( + `Error: Subtask ID "${subtaskId}" must be in format "parentId.subtaskId"` + ) + ); + showRemoveSubtaskHelp(); + process.exit(1); + } + + console.log(chalk.blue(`Removing subtask ${subtaskId}...`)); + if (convertToTask) { + console.log( + chalk.blue("The subtask will be converted to a standalone task") + ); + } + + const result = await removeSubtask( + tasksPath, + subtaskId, + convertToTask, + generateFiles + ); + + if (convertToTask && result) { + // Display success message and next steps for converted task + console.log( + boxen( + chalk.white.bold( + `Subtask ${subtaskId} Converted to Task #${result.id}` + ) + + "\n\n" + + chalk.white(`Title: ${result.title}`) + + "\n" + + chalk.white(`Status: ${getStatusWithColor(result.status)}`) + + "\n" + + chalk.white( + `Dependencies: ${result.dependencies.join(", ")}` + ) + + "\n\n" + + chalk.white.bold("Next Steps:") + + "\n" + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task` + ) + + "\n" + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it` + ), + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } else { + // Display success message for deleted subtask + console.log( + boxen( + chalk.white.bold(`Subtask ${subtaskId} Removed`) + + "\n\n" + + chalk.white("The subtask has been successfully deleted."), + { + padding: 1, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); + } + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + showRemoveSubtaskHelp(); + process.exit(1); + } + }) + .on("error", function (err) { + console.error(chalk.red(`Error: ${err.message}`)); + showRemoveSubtaskHelp(); + process.exit(1); + }); + + // Helper function to show remove-subtask command help + function showRemoveSubtaskHelp() { + console.log( + boxen( + chalk.white.bold("Remove Subtask Command Help") + + "\n\n" + + chalk.cyan("Usage:") + + "\n" + + ` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` + + chalk.cyan("Options:") + + "\n" + + ' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' + + " -c, --convert Convert the subtask to a standalone task instead of deleting it\n" + + ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + + " --skip-generate Skip regenerating task files\n\n" + + chalk.cyan("Examples:") + + "\n" + + " task-master remove-subtask --id=5.2\n" + + " task-master remove-subtask --id=5.2,6.3,7.1\n" + + " task-master remove-subtask --id=5.2 --convert", + { padding: 1, borderColor: "blue", borderStyle: "round" } + ) + ); + } + + // remove-task command + programInstance + .command("remove-task") + .description("Remove one or more tasks or subtasks permanently") + .description("Remove one or more tasks or subtasks permanently") + .option( + "-i, --id <ids>", + 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")' + ) + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option("-y, --yes", "Skip confirmation prompt", false) + .action(async (options) => { + const tasksPath = options.file; + const taskIdsString = options.id; + + if (!taskIdsString) { + console.error(chalk.red("Error: Task ID(s) are required")); + console.error( + chalk.yellow( + "Usage: task-master remove-task --id=<taskId1,taskId2...>" + ) + ); + process.exit(1); + } + + const taskIdsToRemove = taskIdsString + .split(",") + .map((id) => id.trim()) + .filter(Boolean); + + if (taskIdsToRemove.length === 0) { + console.error(chalk.red("Error: No valid task IDs provided.")); + process.exit(1); + } + + try { + // Read data once for checks and confirmation + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + console.error( + chalk.red(`Error: No valid tasks found in ${tasksPath}`) + ); + process.exit(1); + } + + const existingTasksToRemove = []; + const nonExistentIds = []; + let totalSubtasksToDelete = 0; + const dependentTaskMessages = []; + + for (const taskId of taskIdsToRemove) { + if (!taskExists(data.tasks, taskId)) { + nonExistentIds.push(taskId); + } else { + // Correctly extract the task object from the result of findTaskById + const findResult = findTaskById(data.tasks, taskId); + const taskObject = findResult.task; // Get the actual task/subtask object + + if (taskObject) { + existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object + + // If it's a main task, count its subtasks and check dependents + if (!taskObject.isSubtask) { + // Check the actual task object + if (taskObject.subtasks && taskObject.subtasks.length > 0) { + totalSubtasksToDelete += taskObject.subtasks.length; + } + const dependentTasks = data.tasks.filter( + (t) => + t.dependencies && + t.dependencies.includes(parseInt(taskId, 10)) + ); + if (dependentTasks.length > 0) { + dependentTaskMessages.push( + ` - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(", ")})` + ); + } + } + } else { + // Handle case where findTaskById returned null for the task property (should be rare) + nonExistentIds.push(`${taskId} (error finding details)`); + } + } + } + + if (nonExistentIds.length > 0) { + console.warn( + chalk.yellow( + `Warning: The following task IDs were not found: ${nonExistentIds.join(", ")}` + ) + ); + } + + if (existingTasksToRemove.length === 0) { + console.log(chalk.blue("No existing tasks found to remove.")); + process.exit(0); + } + + // Skip confirmation if --yes flag is provided + if (!options.yes) { + console.log(); + console.log( + chalk.red.bold( + `āš ļø WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):` + ) + ); + console.log(); + + existingTasksToRemove.forEach(({ id, task }) => { + if (!task) return; // Should not happen due to taskExists check, but safeguard + if (task.isSubtask) { + // Subtask - title is directly on the task object + console.log( + chalk.white(` Subtask ${id}: ${task.title || "(no title)"}`) + ); + // Optionally show parent context if available + if (task.parentTask) { + console.log( + chalk.gray( + ` (Parent: ${task.parentTask.id} - ${task.parentTask.title || "(no title)"})` + ) + ); + } + } else { + // Main task - title is directly on the task object + console.log( + chalk.white.bold(` Task ${id}: ${task.title || "(no title)"}`) + ); + } + }); + + if (totalSubtasksToDelete > 0) { + console.log( + chalk.yellow( + `āš ļø This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!` + ) + ); + } + + if (dependentTaskMessages.length > 0) { + console.log( + chalk.yellow( + "āš ļø Warning: Dependencies on the following tasks will be removed:" + ) + ); + dependentTaskMessages.forEach((msg) => + console.log(chalk.yellow(msg)) + ); + } + + console.log(); + + const { confirm } = await inquirer.prompt([ + { + type: "confirm", + name: "confirm", + message: chalk.red.bold( + `Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?` + ), + default: false, + }, + ]); + + if (!confirm) { + console.log(chalk.blue("Task deletion cancelled.")); + process.exit(0); + } + } + + const indicator = startLoadingIndicator( + `Removing ${existingTasksToRemove.length} task(s)/subtask(s)...` + ); + + // Use the string of existing IDs for the core function + const existingIdsString = existingTasksToRemove + .map(({ id }) => id) + .join(","); + const result = await removeTask(tasksPath, existingIdsString); + + stopLoadingIndicator(indicator); + + if (result.success) { + console.log( + boxen( + chalk.green( + `Successfully removed ${result.removedTasks.length} task(s)/subtask(s).` + ) + + (result.message ? `\n\nDetails:\n${result.message}` : "") + + (result.error + ? `\n\nWarnings:\n${chalk.yellow(result.error)}` + : ""), + { padding: 1, borderColor: "green", borderStyle: "round" } + ) + ); + } else { + console.error( + boxen( + chalk.red( + `Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).` + ) + + (result.message ? `\n\nDetails:\n${result.message}` : "") + + (result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ""), + { + padding: 1, + borderColor: "red", + borderStyle: "round", + } + ) + ); + process.exit(1); // Exit with error code if any part failed + } + + // Log any initially non-existent IDs again for clarity + if (nonExistentIds.length > 0) { + console.warn( + chalk.yellow( + `Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(", ")}` + ) + ); + + // Exit with error if any removals failed + if (result.removedTasks.length === 0) { + process.exit(1); + } + } + } catch (error) { + console.error( + chalk.red(`Error: ${error.message || "An unknown error occurred"}`) + ); + process.exit(1); + } + }); + + // init command (Directly calls the implementation from init.js) + programInstance + .command("init") + .description("Initialize a new project with Task Master structure") + .option("-y, --yes", "Skip prompts and use default values") + .option("-n, --name <name>", "Project name") + .option("-d, --description <description>", "Project description") + .option("-v, --version <version>", "Project version", "0.1.0") // Set default here + .option("-a, --author <author>", "Author name") + .option("--skip-install", "Skip installing dependencies") + .option("--dry-run", "Show what would be done without making changes") + .option("--aliases", "Add shell aliases (tm, taskmaster)") + .action(async (cmdOptions) => { + // cmdOptions contains parsed arguments + try { + console.log("DEBUG: Running init command action in commands.js"); + console.log( + "DEBUG: Options received by action:", + JSON.stringify(cmdOptions) + ); + // Directly call the initializeProject function, passing the parsed options + await initializeProject(cmdOptions); + // initializeProject handles its own flow, including potential process.exit() + } catch (error) { + console.error( + chalk.red(`Error during initialization: ${error.message}`) + ); + process.exit(1); + } + }); + + // models command + programInstance + .command("models") + .description("Manage AI model configurations") + .option( + "--set-main <model_id>", + "Set the primary model for task generation/updates" + ) + .option( + "--set-research <model_id>", + "Set the model for research-backed operations" + ) + .option( + "--set-fallback <model_id>", + "Set the model to use if the primary fails" + ) + .option("--setup", "Run interactive setup to configure models") + .option( + "--openrouter", + "Allow setting a custom OpenRouter model ID (use with --set-*) " + ) + .option( + "--ollama", + "Allow setting a custom Ollama model ID (use with --set-*) " + ) + .option( + "--bedrock", + "Allow setting a custom Bedrock model ID (use with --set-*) " + ) + .addHelpText( + "after", + ` Examples: $ task-master models # View current configuration $ task-master models --set-main gpt-4o # Set main model (provider inferred) @@ -2609,336 +2607,336 @@ Examples: $ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role $ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role $ task-master models --setup # Run interactive setup` - ) - .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Validate flags: cannot use multiple provider flags simultaneously - const providerFlags = [ - options.openrouter, - options.ollama, - options.bedrock - ].filter(Boolean).length; - if (providerFlags > 1) { - console.error( - chalk.red( - 'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock) simultaneously.' - ) - ); - process.exit(1); - } + ) + .action(async (options) => { + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red("Error: Could not find project root.")); + process.exit(1); + } + // Validate flags: cannot use multiple provider flags simultaneously + const providerFlags = [ + options.openrouter, + options.ollama, + options.bedrock, + ].filter(Boolean).length; + if (providerFlags > 1) { + console.error( + chalk.red( + "Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock) simultaneously." + ) + ); + process.exit(1); + } - // Determine the primary action based on flags - const isSetup = options.setup; - const isSetOperation = - options.setMain || options.setResearch || options.setFallback; + // Determine the primary action based on flags + const isSetup = options.setup; + const isSetOperation = + options.setMain || options.setResearch || options.setFallback; - // --- Execute Action --- + // --- Execute Action --- - if (isSetup) { - // Action 1: Run Interactive Setup - console.log(chalk.blue('Starting interactive model setup...')); // Added feedback - try { - await runInteractiveSetup(projectRoot); - // runInteractiveSetup logs its own completion/error messages - } catch (setupError) { - console.error( - chalk.red('\\nInteractive setup failed unexpectedly:'), - setupError.message - ); - } - // --- IMPORTANT: Exit after setup --- - return; // Stop execution here - } + if (isSetup) { + // Action 1: Run Interactive Setup + console.log(chalk.blue("Starting interactive model setup...")); // Added feedback + try { + await runInteractiveSetup(projectRoot); + // runInteractiveSetup logs its own completion/error messages + } catch (setupError) { + console.error( + chalk.red("\\nInteractive setup failed unexpectedly:"), + setupError.message + ); + } + // --- IMPORTANT: Exit after setup --- + return; // Stop execution here + } - if (isSetOperation) { - // Action 2: Perform Direct Set Operations - let updateOccurred = false; // Track if any update actually happened + if (isSetOperation) { + // Action 2: Perform Direct Set Operations + let updateOccurred = false; // Track if any update actually happened - if (options.setMain) { - const result = await setModel('main', options.setMain, { - projectRoot, - providerHint: options.openrouter - ? 'openrouter' - : options.ollama - ? 'ollama' - : options.bedrock - ? 'bedrock' - : undefined - }); - if (result.success) { - console.log(chalk.green(`āœ… ${result.data.message}`)); - if (result.data.warning) - console.log(chalk.yellow(result.data.warning)); - updateOccurred = true; - } else { - console.error( - chalk.red(`āŒ Error setting main model: ${result.error.message}`) - ); - } - } - if (options.setResearch) { - const result = await setModel('research', options.setResearch, { - projectRoot, - providerHint: options.openrouter - ? 'openrouter' - : options.ollama - ? 'ollama' - : options.bedrock - ? 'bedrock' - : undefined - }); - if (result.success) { - console.log(chalk.green(`āœ… ${result.data.message}`)); - if (result.data.warning) - console.log(chalk.yellow(result.data.warning)); - updateOccurred = true; - } else { - console.error( - chalk.red( - `āŒ Error setting research model: ${result.error.message}` - ) - ); - } - } - if (options.setFallback) { - const result = await setModel('fallback', options.setFallback, { - projectRoot, - providerHint: options.openrouter - ? 'openrouter' - : options.ollama - ? 'ollama' - : options.bedrock - ? 'bedrock' - : undefined - }); - if (result.success) { - console.log(chalk.green(`āœ… ${result.data.message}`)); - if (result.data.warning) - console.log(chalk.yellow(result.data.warning)); - updateOccurred = true; - } else { - console.error( - chalk.red( - `āŒ Error setting fallback model: ${result.error.message}` - ) - ); - } - } + if (options.setMain) { + const result = await setModel("main", options.setMain, { + projectRoot, + providerHint: options.openrouter + ? "openrouter" + : options.ollama + ? "ollama" + : options.bedrock + ? "bedrock" + : undefined, + }); + if (result.success) { + console.log(chalk.green(`āœ… ${result.data.message}`)); + if (result.data.warning) + console.log(chalk.yellow(result.data.warning)); + updateOccurred = true; + } else { + console.error( + chalk.red(`āŒ Error setting main model: ${result.error.message}`) + ); + } + } + if (options.setResearch) { + const result = await setModel("research", options.setResearch, { + projectRoot, + providerHint: options.openrouter + ? "openrouter" + : options.ollama + ? "ollama" + : options.bedrock + ? "bedrock" + : undefined, + }); + if (result.success) { + console.log(chalk.green(`āœ… ${result.data.message}`)); + if (result.data.warning) + console.log(chalk.yellow(result.data.warning)); + updateOccurred = true; + } else { + console.error( + chalk.red( + `āŒ Error setting research model: ${result.error.message}` + ) + ); + } + } + if (options.setFallback) { + const result = await setModel("fallback", options.setFallback, { + projectRoot, + providerHint: options.openrouter + ? "openrouter" + : options.ollama + ? "ollama" + : options.bedrock + ? "bedrock" + : undefined, + }); + if (result.success) { + console.log(chalk.green(`āœ… ${result.data.message}`)); + if (result.data.warning) + console.log(chalk.yellow(result.data.warning)); + updateOccurred = true; + } else { + console.error( + chalk.red( + `āŒ Error setting fallback model: ${result.error.message}` + ) + ); + } + } - // Optional: Add a final confirmation if any update occurred - if (updateOccurred) { - console.log(chalk.blue('\nModel configuration updated.')); - } else { - console.log( - chalk.yellow( - '\nNo model configuration changes were made (or errors occurred).' - ) - ); - } + // Optional: Add a final confirmation if any update occurred + if (updateOccurred) { + console.log(chalk.blue("\nModel configuration updated.")); + } else { + console.log( + chalk.yellow( + "\nNo model configuration changes were made (or errors occurred)." + ) + ); + } - // --- IMPORTANT: Exit after set operations --- - return; // Stop execution here - } + // --- IMPORTANT: Exit after set operations --- + return; // Stop execution here + } - // Action 3: Display Full Status (Only runs if no setup and no set flags) - console.log(chalk.blue('Fetching current model configuration...')); // Added feedback - const configResult = await getModelConfiguration({ projectRoot }); - const availableResult = await getAvailableModelsList({ projectRoot }); - const apiKeyStatusResult = await getApiKeyStatusReport({ projectRoot }); + // Action 3: Display Full Status (Only runs if no setup and no set flags) + console.log(chalk.blue("Fetching current model configuration...")); // Added feedback + const configResult = await getModelConfiguration({ projectRoot }); + const availableResult = await getAvailableModelsList({ projectRoot }); + const apiKeyStatusResult = await getApiKeyStatusReport({ projectRoot }); - // 1. Display Active Models - if (!configResult.success) { - console.error( - chalk.red( - `āŒ Error fetching configuration: ${configResult.error.message}` - ) - ); - } else { - displayModelConfiguration( - configResult.data, - availableResult.data?.models || [] - ); - } + // 1. Display Active Models + if (!configResult.success) { + console.error( + chalk.red( + `āŒ Error fetching configuration: ${configResult.error.message}` + ) + ); + } else { + displayModelConfiguration( + configResult.data, + availableResult.data?.models || [] + ); + } - // 2. Display API Key Status - if (apiKeyStatusResult.success) { - displayApiKeyStatus(apiKeyStatusResult.data.report); - } else { - console.error( - chalk.yellow( - `āš ļø Warning: Could not display API Key status: ${apiKeyStatusResult.error.message}` - ) - ); - } + // 2. Display API Key Status + if (apiKeyStatusResult.success) { + displayApiKeyStatus(apiKeyStatusResult.data.report); + } else { + console.error( + chalk.yellow( + `āš ļø Warning: Could not display API Key status: ${apiKeyStatusResult.error.message}` + ) + ); + } - // 3. Display Other Available Models (Filtered) - if (availableResult.success) { - const activeIds = configResult.success - ? [ - configResult.data.activeModels.main.modelId, - configResult.data.activeModels.research.modelId, - configResult.data.activeModels.fallback?.modelId - ].filter(Boolean) - : []; - const displayableAvailable = availableResult.data.models.filter( - (m) => !activeIds.includes(m.modelId) && !m.modelId.startsWith('[') - ); - displayAvailableModels(displayableAvailable); - } else { - console.error( - chalk.yellow( - `āš ļø Warning: Could not display available models: ${availableResult.error.message}` - ) - ); - } + // 3. Display Other Available Models (Filtered) + if (availableResult.success) { + const activeIds = configResult.success + ? [ + configResult.data.activeModels.main.modelId, + configResult.data.activeModels.research.modelId, + configResult.data.activeModels.fallback?.modelId, + ].filter(Boolean) + : []; + const displayableAvailable = availableResult.data.models.filter( + (m) => !activeIds.includes(m.modelId) && !m.modelId.startsWith("[") + ); + displayAvailableModels(displayableAvailable); + } else { + console.error( + chalk.yellow( + `āš ļø Warning: Could not display available models: ${availableResult.error.message}` + ) + ); + } - // 4. Conditional Hint if Config File is Missing - const configExists = isConfigFilePresent(projectRoot); - if (!configExists) { - console.log( - chalk.yellow( - "\\nHint: Run 'task-master models --setup' to create or update your configuration." - ) - ); - } - // --- IMPORTANT: Exit after displaying status --- - return; // Stop execution here - }); + // 4. Conditional Hint if Config File is Missing + const configExists = isConfigFilePresent(projectRoot); + if (!configExists) { + console.log( + chalk.yellow( + "\\nHint: Run 'task-master models --setup' to create or update your configuration." + ) + ); + } + // --- IMPORTANT: Exit after displaying status --- + return; // Stop execution here + }); - // move-task command - programInstance - .command('move') - .description('Move a task or subtask to a new position') - .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') - .option( - '--from <id>', - 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")' - ) - .option( - '--to <id>', - 'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated' - ) - .action(async (options) => { - const tasksPath = options.file; - const sourceId = options.from; - const destinationId = options.to; + // move-task command + programInstance + .command("move") + .description("Move a task or subtask to a new position") + .option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json") + .option( + "--from <id>", + 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")' + ) + .option( + "--to <id>", + 'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated' + ) + .action(async (options) => { + const tasksPath = options.file; + const sourceId = options.from; + const destinationId = options.to; - if (!sourceId || !destinationId) { - console.error( - chalk.red('Error: Both --from and --to parameters are required') - ); - console.log( - chalk.yellow( - 'Usage: task-master move --from=<sourceId> --to=<destinationId>' - ) - ); - process.exit(1); - } + if (!sourceId || !destinationId) { + console.error( + chalk.red("Error: Both --from and --to parameters are required") + ); + console.log( + chalk.yellow( + "Usage: task-master move --from=<sourceId> --to=<destinationId>" + ) + ); + process.exit(1); + } - // Check if we're moving multiple tasks (comma-separated IDs) - const sourceIds = sourceId.split(',').map((id) => id.trim()); - const destinationIds = destinationId.split(',').map((id) => id.trim()); + // Check if we're moving multiple tasks (comma-separated IDs) + const sourceIds = sourceId.split(",").map((id) => id.trim()); + const destinationIds = destinationId.split(",").map((id) => id.trim()); - // Validate that the number of source and destination IDs match - if (sourceIds.length !== destinationIds.length) { - console.error( - chalk.red( - 'Error: The number of source and destination IDs must match' - ) - ); - console.log( - chalk.yellow('Example: task-master move --from=5,6,7 --to=10,11,12') - ); - process.exit(1); - } + // Validate that the number of source and destination IDs match + if (sourceIds.length !== destinationIds.length) { + console.error( + chalk.red( + "Error: The number of source and destination IDs must match" + ) + ); + console.log( + chalk.yellow("Example: task-master move --from=5,6,7 --to=10,11,12") + ); + process.exit(1); + } - // If moving multiple tasks - if (sourceIds.length > 1) { - console.log( - chalk.blue( - `Moving multiple tasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...` - ) - ); + // If moving multiple tasks + if (sourceIds.length > 1) { + console.log( + chalk.blue( + `Moving multiple tasks: ${sourceIds.join(", ")} to ${destinationIds.join(", ")}...` + ) + ); - try { - // Read tasks data once to validate destination IDs - const tasksData = readJSON(tasksPath); - if (!tasksData || !tasksData.tasks) { - console.error( - chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`) - ); - process.exit(1); - } + try { + // Read tasks data once to validate destination IDs + const tasksData = readJSON(tasksPath); + if (!tasksData || !tasksData.tasks) { + console.error( + chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`) + ); + process.exit(1); + } - // Move tasks one by one - for (let i = 0; i < sourceIds.length; i++) { - const fromId = sourceIds[i]; - const toId = destinationIds[i]; + // Move tasks one by one + for (let i = 0; i < sourceIds.length; i++) { + const fromId = sourceIds[i]; + const toId = destinationIds[i]; - // Skip if source and destination are the same - if (fromId === toId) { - console.log( - chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`) - ); - continue; - } + // Skip if source and destination are the same + if (fromId === toId) { + console.log( + chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`) + ); + continue; + } - console.log( - chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`) - ); - try { - await moveTask( - tasksPath, - fromId, - toId, - i === sourceIds.length - 1 - ); - console.log( - chalk.green( - `āœ“ Successfully moved task/subtask ${fromId} to ${toId}` - ) - ); - } catch (error) { - console.error( - chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`) - ); - // Continue with the next task rather than exiting - } - } - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - process.exit(1); - } - } else { - // Moving a single task (existing logic) - console.log( - chalk.blue(`Moving task/subtask ${sourceId} to ${destinationId}...`) - ); + console.log( + chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`) + ); + try { + await moveTask( + tasksPath, + fromId, + toId, + i === sourceIds.length - 1 + ); + console.log( + chalk.green( + `āœ“ Successfully moved task/subtask ${fromId} to ${toId}` + ) + ); + } catch (error) { + console.error( + chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`) + ); + // Continue with the next task rather than exiting + } + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } + } else { + // Moving a single task (existing logic) + console.log( + chalk.blue(`Moving task/subtask ${sourceId} to ${destinationId}...`) + ); - try { - const result = await moveTask( - tasksPath, - sourceId, - destinationId, - true - ); - console.log( - chalk.green( - `āœ“ Successfully moved task/subtask ${sourceId} to ${destinationId}` - ) - ); - } catch (error) { - console.error(chalk.red(`Error: ${error.message}`)); - process.exit(1); - } - } - }); + try { + const result = await moveTask( + tasksPath, + sourceId, + destinationId, + true + ); + console.log( + chalk.green( + `āœ“ Successfully moved task/subtask ${sourceId} to ${destinationId}` + ) + ); + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } + } + }); - return programInstance; + return programInstance; } /** @@ -2946,42 +2944,42 @@ Examples: * @returns {Object} Configured Commander program */ function setupCLI() { - // Create a new program instance - const programInstance = program - .name('dev') - .description('AI-driven development task management') - .version(() => { - // Read version directly from package.json ONLY - try { - const packageJsonPath = path.join(process.cwd(), 'package.json'); - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse( - fs.readFileSync(packageJsonPath, 'utf8') - ); - return packageJson.version; - } - } catch (error) { - // Silently fall back to 'unknown' - log( - 'warn', - 'Could not read package.json for version info in .version()' - ); - } - return 'unknown'; // Default fallback if package.json fails - }) - .helpOption('-h, --help', 'Display help') - .addHelpCommand(false); // Disable default help command + // Create a new program instance + const programInstance = program + .name("dev") + .description("AI-driven development task management") + .version(() => { + // Read version directly from package.json ONLY + try { + const packageJsonPath = path.join(process.cwd(), "package.json"); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, "utf8") + ); + return packageJson.version; + } + } catch (error) { + // Silently fall back to 'unknown' + log( + "warn", + "Could not read package.json for version info in .version()" + ); + } + return "unknown"; // Default fallback if package.json fails + }) + .helpOption("-h, --help", "Display help") + .addHelpCommand(false); // Disable default help command - // Modify the help option to use your custom display - programInstance.helpInformation = () => { - displayHelp(); - return ''; - }; + // Modify the help option to use your custom display + programInstance.helpInformation = () => { + displayHelp(); + return ""; + }; - // Register commands - registerCommands(programInstance); + // Register commands + registerCommands(programInstance); - return programInstance; + return programInstance; } /** @@ -2989,74 +2987,74 @@ function setupCLI() { * @returns {Promise<{currentVersion: string, latestVersion: string, needsUpdate: boolean}>} */ async function checkForUpdate() { - // Get current version from package.json ONLY - const currentVersion = getTaskMasterVersion(); + // Get current version from package.json ONLY + const currentVersion = getTaskMasterVersion(); - return new Promise((resolve) => { - // Get the latest version from npm registry - const options = { - hostname: 'registry.npmjs.org', - path: '/task-master-ai', - method: 'GET', - headers: { - Accept: 'application/vnd.npm.install-v1+json' // Lightweight response - } - }; + return new Promise((resolve) => { + // Get the latest version from npm registry + const options = { + hostname: "registry.npmjs.org", + path: "/task-master-ai", + method: "GET", + headers: { + Accept: "application/vnd.npm.install-v1+json", // Lightweight response + }, + }; - const req = https.request(options, (res) => { - let data = ''; + const req = https.request(options, (res) => { + let data = ""; - res.on('data', (chunk) => { - data += chunk; - }); + res.on("data", (chunk) => { + data += chunk; + }); - res.on('end', () => { - try { - const npmData = JSON.parse(data); - const latestVersion = npmData['dist-tags']?.latest || currentVersion; + res.on("end", () => { + try { + const npmData = JSON.parse(data); + const latestVersion = npmData["dist-tags"]?.latest || currentVersion; - // Compare versions - const needsUpdate = - compareVersions(currentVersion, latestVersion) < 0; + // Compare versions + const needsUpdate = + compareVersions(currentVersion, latestVersion) < 0; - resolve({ - currentVersion, - latestVersion, - needsUpdate - }); - } catch (error) { - log('debug', `Error parsing npm response: ${error.message}`); - resolve({ - currentVersion, - latestVersion: currentVersion, - needsUpdate: false - }); - } - }); - }); + resolve({ + currentVersion, + latestVersion, + needsUpdate, + }); + } catch (error) { + log("debug", `Error parsing npm response: ${error.message}`); + resolve({ + currentVersion, + latestVersion: currentVersion, + needsUpdate: false, + }); + } + }); + }); - req.on('error', (error) => { - log('debug', `Error checking for updates: ${error.message}`); - resolve({ - currentVersion, - latestVersion: currentVersion, - needsUpdate: false - }); - }); + req.on("error", (error) => { + log("debug", `Error checking for updates: ${error.message}`); + resolve({ + currentVersion, + latestVersion: currentVersion, + needsUpdate: false, + }); + }); - // Set a timeout to avoid hanging if npm is slow - req.setTimeout(3000, () => { - req.abort(); - log('debug', 'Update check timed out'); - resolve({ - currentVersion, - latestVersion: currentVersion, - needsUpdate: false - }); - }); + // Set a timeout to avoid hanging if npm is slow + req.setTimeout(3000, () => { + req.abort(); + log("debug", "Update check timed out"); + resolve({ + currentVersion, + latestVersion: currentVersion, + needsUpdate: false, + }); + }); - req.end(); - }); + req.end(); + }); } /** @@ -3066,18 +3064,18 @@ async function checkForUpdate() { * @returns {number} -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2 */ function compareVersions(v1, v2) { - const v1Parts = v1.split('.').map((p) => parseInt(p, 10)); - const v2Parts = v2.split('.').map((p) => parseInt(p, 10)); + const v1Parts = v1.split(".").map((p) => parseInt(p, 10)); + const v2Parts = v2.split(".").map((p) => parseInt(p, 10)); - for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { - const v1Part = v1Parts[i] || 0; - const v2Part = v2Parts[i] || 0; + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; - if (v1Part < v2Part) return -1; - if (v1Part > v2Part) return 1; - } + if (v1Part < v2Part) return -1; + if (v1Part > v2Part) return 1; + } - return 0; + return 0; } /** @@ -3086,18 +3084,18 @@ function compareVersions(v1, v2) { * @param {string} latestVersion - Latest version */ function displayUpgradeNotification(currentVersion, latestVersion) { - const message = boxen( - `${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` + - `Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`, - { - padding: 1, - margin: { top: 1, bottom: 1 }, - borderColor: 'yellow', - borderStyle: 'round' - } - ); + const message = boxen( + `${chalk.blue.bold("Update Available!")} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` + + `Run ${chalk.cyan("npm i task-master-ai@latest -g")} to update to the latest version with new features and bug fixes.`, + { + padding: 1, + margin: { top: 1, bottom: 1 }, + borderColor: "yellow", + borderStyle: "round", + } + ); - console.log(message); + console.log(message); } /** @@ -3105,97 +3103,97 @@ function displayUpgradeNotification(currentVersion, latestVersion) { * @param {Array} argv - Command-line arguments */ async function runCLI(argv = process.argv) { - try { - // Display banner if not in a pipe - if (process.stdout.isTTY) { - displayBanner(); - } + try { + // Display banner if not in a pipe + if (process.stdout.isTTY) { + displayBanner(); + } - // If no arguments provided, show help - if (argv.length <= 2) { - displayHelp(); - process.exit(0); - } + // If no arguments provided, show help + if (argv.length <= 2) { + displayHelp(); + process.exit(0); + } - // Start the update check in the background - don't await yet - const updateCheckPromise = checkForUpdate(); + // Start the update check in the background - don't await yet + const updateCheckPromise = checkForUpdate(); - // Setup and parse - // NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config - // This means the ConfigurationError might be thrown here if .taskmasterconfig is missing. - const programInstance = setupCLI(); - await programInstance.parseAsync(argv); + // Setup and parse + // NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config + // This means the ConfigurationError might be thrown here if .taskmasterconfig is missing. + const programInstance = setupCLI(); + await programInstance.parseAsync(argv); - // After command execution, check if an update is available - const updateInfo = await updateCheckPromise; - if (updateInfo.needsUpdate) { - displayUpgradeNotification( - updateInfo.currentVersion, - updateInfo.latestVersion - ); - } - } catch (error) { - // ** Specific catch block for missing configuration file ** - if (error instanceof ConfigurationError) { - console.error( - boxen( - chalk.red.bold('Configuration Update Required!') + - '\n\n' + - chalk.white('Taskmaster now uses the ') + - chalk.yellow.bold('.taskmasterconfig') + - chalk.white( - ' file in your project root for AI model choices and settings.\n\n' + - 'This file appears to be ' - ) + - chalk.red.bold('missing') + - chalk.white('. No worries though.\n\n') + - chalk.cyan.bold('To create this file, run the interactive setup:') + - '\n' + - chalk.green(' task-master models --setup') + - '\n\n' + - chalk.white.bold('Key Points:') + - '\n' + - chalk.white('* ') + - chalk.yellow.bold('.taskmasterconfig') + - chalk.white( - ': Stores your AI model settings (do not manually edit)\n' - ) + - chalk.white('* ') + - chalk.yellow.bold('.env & .mcp.json') + - chalk.white(': Still used ') + - chalk.red.bold('only') + - chalk.white(' for your AI provider API keys.\n\n') + - chalk.cyan( - '`task-master models` to check your config & available models\n' - ) + - chalk.cyan( - '`task-master models --setup` to adjust the AI models used by Taskmaster' - ), - { - padding: 1, - margin: { top: 1 }, - borderColor: 'red', - borderStyle: 'round' - } - ) - ); - } else { - // Generic error handling for other errors - console.error(chalk.red(`Error: ${error.message}`)); - if (getDebugFlag()) { - console.error(error); - } - } + // After command execution, check if an update is available + const updateInfo = await updateCheckPromise; + if (updateInfo.needsUpdate) { + displayUpgradeNotification( + updateInfo.currentVersion, + updateInfo.latestVersion + ); + } + } catch (error) { + // ** Specific catch block for missing configuration file ** + if (error instanceof ConfigurationError) { + console.error( + boxen( + chalk.red.bold("Configuration Update Required!") + + "\n\n" + + chalk.white("Taskmaster now uses the ") + + chalk.yellow.bold(".taskmasterconfig") + + chalk.white( + " file in your project root for AI model choices and settings.\n\n" + + "This file appears to be " + ) + + chalk.red.bold("missing") + + chalk.white(". No worries though.\n\n") + + chalk.cyan.bold("To create this file, run the interactive setup:") + + "\n" + + chalk.green(" task-master models --setup") + + "\n\n" + + chalk.white.bold("Key Points:") + + "\n" + + chalk.white("* ") + + chalk.yellow.bold(".taskmasterconfig") + + chalk.white( + ": Stores your AI model settings (do not manually edit)\n" + ) + + chalk.white("* ") + + chalk.yellow.bold(".env & .mcp.json") + + chalk.white(": Still used ") + + chalk.red.bold("only") + + chalk.white(" for your AI provider API keys.\n\n") + + chalk.cyan( + "`task-master models` to check your config & available models\n" + ) + + chalk.cyan( + "`task-master models --setup` to adjust the AI models used by Taskmaster" + ), + { + padding: 1, + margin: { top: 1 }, + borderColor: "red", + borderStyle: "round", + } + ) + ); + } else { + // Generic error handling for other errors + console.error(chalk.red(`Error: ${error.message}`)); + if (getDebugFlag()) { + console.error(error); + } + } - process.exit(1); - } + process.exit(1); + } } export { - registerCommands, - setupCLI, - runCLI, - checkForUpdate, - compareVersions, - displayUpgradeNotification + registerCommands, + setupCLI, + runCLI, + checkForUpdate, + compareVersions, + displayUpgradeNotification, }; diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index f9e3e82b..5f0ff448 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -65,7 +65,7 @@ const defaultConfig = { }, }, account: { - userId: null, + userId: "1234567890", // Placeholder that triggers auth/init userEmail: "", mode: "byok", telemetryEnabled: false, @@ -710,9 +710,9 @@ function isConfigFilePresent(explicitRoot = null) { /** * Gets the user ID from the configuration. - * Sets a default value if none exists and saves the config. + * Returns a placeholder that triggers auth/init if no real userId exists. * @param {string|null} explicitRoot - Optional explicit path to the project root. - * @returns {string} The user ID (never null). + * @returns {string|null} The user ID or placeholder, or null if auth unavailable. */ function getUserId(explicitRoot = null) { const config = getConfig(explicitRoot); @@ -722,25 +722,14 @@ function getUserId(explicitRoot = null) { config.account = { ...defaultConfig.account }; } - // If userId exists, return it - if (config.account.userId) { + // If userId exists and is not the placeholder, return it + if (config.account.userId && config.account.userId !== "1234567890") { return config.account.userId; } - // Set default userId if none exists - const defaultUserId = "1234567890"; - config.account.userId = defaultUserId; - - // Save the updated config - const success = writeConfig(config, explicitRoot); - if (!success) { - log( - "warn", - "Failed to write updated configuration with new userId. Please let the developers know." - ); - } - - return defaultUserId; + // If userId is null or the placeholder, return the placeholder + // This signals to other code that auth/init needs to be attempted + return "1234567890"; } /** diff --git a/scripts/modules/task-manager/add-task.js b/scripts/modules/task-manager/add-task.js index 1d0e13ec..a8969c37 100644 --- a/scripts/modules/task-manager/add-task.js +++ b/scripts/modules/task-manager/add-task.js @@ -1,40 +1,40 @@ -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, - 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, + 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)" + ), }); /** @@ -57,843 +57,843 @@ const AiTaskDataSchema = z.object({ * @returns {Promise<object>} 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') + '\n', { - 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: 2 }, // Title is most important - { name: 'description', weight: 1.5 }, // Description is next - { name: 'details', weight: 0.8 }, // Details is less 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 15 matches - limit: 15 - }; - - // 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); - - // Also look for tasks with similar purposes or categories - const purposeCategories = [ - { pattern: /(command|cli|flag)/i, label: 'CLI commands' }, - { pattern: /(task|subtask|add)/i, label: 'Task management' }, - { pattern: /(dependency|depend)/i, label: 'Dependency handling' }, - { pattern: /(AI|model|prompt)/i, label: 'AI integration' }, - { pattern: /(UI|display|show)/i, label: 'User interface' }, - { pattern: /(schedule|time|cron)/i, label: 'Scheduling' }, // Added scheduling category - { pattern: /(config|setting|option)/i, label: 'Configuration' } // Added configuration category - ]; - - promptCategory = purposeCategories.find((cat) => - cat.pattern.test(prompt) - ); - const categoryTasks = promptCategory - ? data.tasks - .filter( - (t) => - promptCategory.pattern.test(t.title) || - promptCategory.pattern.test(t.description) || - (t.details && promptCategory.pattern.test(t.details)) - ) - .filter((t) => !relatedTasks.some((rt) => rt.id === t.id)) - .slice(0, 3) - : []; - - // 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 (categoryTasks.length > 0) { - contextTasks += `\n\nTasks related to ${promptCategory.label}:\n${categoryTasks - .map((t) => `- 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, 5), - ...categoryTasks.slice(0, 2) - ]; - uniqueDetailedTasks = Array.from( - new Map(allDetailedTasks.map((t) => [t.id, t])).values() - ).slice(0, 8); - - 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 = promptCategory - ? data.tasks.filter( - (t) => - promptCategory.pattern.test(t.title) || - promptCategory.pattern.test(t.description) - ) - : []; - - let commonDeps = []; // Initialize commonDeps - - if (similarPurposeTasks.length > 0) { - contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : '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, 5); - - 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( - ` Fuzzy 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, 5).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, 3).forEach((t) => { - console.log( - chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) - ); - }); - } - - if (promptCategory && categoryTasks.length > 0) { - console.log( - chalk.gray(`\n Tasks related to ${promptCategory.label}:`) - ); - categoryTasks.forEach((t) => { - console.log( - chalk.magenta(` • 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 - )}` + - (promptCategory - ? `\n${chalk.cyan('Category detected: ')}${chalk.yellow(promptCategory.label)}` - : ''), - { - 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") + "\n", { + 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: 2 }, // Title is most important + { name: "description", weight: 1.5 }, // Description is next + { name: "details", weight: 0.8 }, // Details is less 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 15 matches + limit: 15, + }; + + // 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); + + // Also look for tasks with similar purposes or categories + const purposeCategories = [ + { pattern: /(command|cli|flag)/i, label: "CLI commands" }, + { pattern: /(task|subtask|add)/i, label: "Task management" }, + { pattern: /(dependency|depend)/i, label: "Dependency handling" }, + { pattern: /(AI|model|prompt)/i, label: "AI integration" }, + { pattern: /(UI|display|show)/i, label: "User interface" }, + { pattern: /(schedule|time|cron)/i, label: "Scheduling" }, // Added scheduling category + { pattern: /(config|setting|option)/i, label: "Configuration" }, // Added configuration category + ]; + + promptCategory = purposeCategories.find((cat) => + cat.pattern.test(prompt) + ); + const categoryTasks = promptCategory + ? data.tasks + .filter( + (t) => + promptCategory.pattern.test(t.title) || + promptCategory.pattern.test(t.description) || + (t.details && promptCategory.pattern.test(t.details)) + ) + .filter((t) => !relatedTasks.some((rt) => rt.id === t.id)) + .slice(0, 3) + : []; + + // 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 (categoryTasks.length > 0) { + contextTasks += `\n\nTasks related to ${promptCategory.label}:\n${categoryTasks + .map((t) => `- 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, 5), + ...categoryTasks.slice(0, 2), + ]; + uniqueDetailedTasks = Array.from( + new Map(allDetailedTasks.map((t) => [t.id, t])).values() + ).slice(0, 8); + + 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 = promptCategory + ? data.tasks.filter( + (t) => + promptCategory.pattern.test(t.title) || + promptCategory.pattern.test(t.description) + ) + : []; + + let commonDeps = []; // Initialize commonDeps + + if (similarPurposeTasks.length > 0) { + contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : "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, 5); + + 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( + ` Fuzzy 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, 5).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, 3).forEach((t) => { + console.log( + chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) + ); + }); + } + + if (promptCategory && categoryTasks.length > 0) { + console.log( + chalk.gray(`\n Tasks related to ${promptCategory.label}:`) + ); + categoryTasks.forEach((t) => { + console.log( + chalk.magenta(` • 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 + )}` + + (promptCategory + ? `\n${chalk.cyan("Category detected: ")}${chalk.yellow(promptCategory.label)}` + : ""), + { + 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", @@ -903,22 +903,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. @@ -928,281 +928,281 @@ 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'); - } catch (error) { - report( - `DEBUG: generateObjectService caught error: ${error.message}`, - 'debug' - ); - report(`Error generating task with AI: ${error.message}`, 'error'); - if (loadingIndicator) stopLoadingIndicator(loadingIndicator); - throw error; // Re-throw error after logging - } finally { - report('DEBUG: generateObjectService finally block reached.', 'debug'); - if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops - } - // --- End Refactored AI Interaction --- - } + report("Successfully generated task data from AI.", "success"); + } catch (error) { + report( + `DEBUG: generateObjectService caught error: ${error.message}`, + "debug" + ); + // Don't log user-facing error here - main catch block handles it + if (loadingIndicator) stopLoadingIndicator(loadingIndicator); + throw error; // Re-throw error after logging + } finally { + report("DEBUG: generateObjectService finally block reached.", "debug"); + if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops + } + // --- 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/user-management.js b/scripts/modules/user-management.js index 8b824908..1eb82030 100644 --- a/scripts/modules/user-management.js +++ b/scripts/modules/user-management.js @@ -68,7 +68,7 @@ async function registerUserWithGateway(email = null, explicitRoot = null) { /** * Updates the user configuration with gateway registration results * @param {string} userId - User ID from gateway - * @param {string} token - API token from gateway + * @param {string} token - User authentication token from gateway (stored in .env) * @param {string} mode - User mode ('byok' or 'hosted') * @param {string|null} explicitRoot - Optional explicit project root path * @returns {boolean} Success status @@ -86,7 +86,7 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) { config.account.userId = userId; config.account.mode = mode; // 'byok' or 'hosted' - // Write API token to .env file (not config) + // Write user authentication token to .env file (not config) if (token) { writeApiKeyToEnv(token, explicitRoot); } @@ -107,8 +107,9 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) { } /** - * Writes the API token to the .env file - * @param {string} token - API token to write + * Writes the user authentication token to the .env file + * This token is used as Bearer auth for gateway API calls + * @param {string} token - Authentication token to write * @param {string|null} explicitRoot - Optional explicit project root path */ function writeApiKeyToEnv(token, explicitRoot = null) { @@ -155,9 +156,9 @@ function writeApiKeyToEnv(token, explicitRoot = null) { // Write updated content fs.writeFileSync(envPath, envContent); - log("info", "API key written to .env file"); + log("info", "User authentication token written to .env file"); } catch (error) { - log("error", `Failed to write API key to .env: ${error.message}`); + log("error", `Failed to write user token to .env: ${error.message}`); } } @@ -303,6 +304,60 @@ async function initializeUser(explicitRoot = null) { } } +/** + * Gets the current user authentication token from .env file + * This is the Bearer token used for gateway API calls + * @param {string|null} explicitRoot - Optional explicit project root path + * @returns {string|null} User authentication token or null if not found + */ +function getUserToken(explicitRoot = null) { + try { + // Determine project root + let rootPath = explicitRoot; + if (!rootPath) { + rootPath = findProjectRoot(); + if (!rootPath) { + log("error", "Could not determine project root for .env file"); + return null; + } + } + + const envPath = path.join(rootPath, ".env"); + if (!fs.existsSync(envPath)) { + return null; + } + + const envContent = fs.readFileSync(envPath, "utf8"); + const lines = envContent.split("\n"); + + for (const line of lines) { + if (line.startsWith("TASKMASTER_API_KEY=")) { + return line.substring("TASKMASTER_API_KEY=".length).trim(); + } + } + + return null; + } catch (error) { + log("error", `Error getting user token from .env: ${error.message}`); + return null; + } +} + +/** + * Gets the current user email from configuration + * @param {string|null} explicitRoot - Optional explicit project root path + * @returns {string|null} User email or null if not found + */ +function getUserEmail(explicitRoot = null) { + try { + const config = getConfig(explicitRoot); + return config?.global?.email || null; + } catch (error) { + log("error", `Error getting user email: ${error.message}`); + return null; + } +} + export { registerUserWithGateway, updateUserConfig, @@ -312,4 +367,6 @@ export { isByokMode, setupUser, initializeUser, + getUserToken, + getUserEmail, }; diff --git a/tasks/tasks.json b/tasks/tasks.json index a05076e3..51a468bb 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -6278,6 +6278,19 @@ ], "priority": "medium", "subtasks": [] + }, + { + "id": 94, + "title": "Enhance Models Command with Mode Selection", + "description": "Improve the `task-master models --setup` command to allow users to choose between BYOK (Bring Your Own Key) mode and hosted mode, not just configure AI models. Currently, users can only change modes during initial project setup, but they should be able to switch between modes at any time.", + "details": "**Current State:**\n- Mode selection (BYOK vs hosted) only available during `task-master init` process\n- `task-master models --setup` only configures AI model selections within current mode\n- Mode is stored in `.taskmasterconfig` under `config.account.mode`\n- Error messages suggest using `task-master models --setup` to switch to BYOK mode, but this doesn't actually work\n\n**Required Implementation:**\n\n1. **Update `runInteractiveSetup()` function in `scripts/modules/commands.js`:**\n - Add mode selection as first step before model configuration\n - Show current mode and allow switching\n - Use same UI pattern as `scripts/init.js` `selectAccessMode()` function\n - Option to keep current mode or change to the other mode\n\n2. **Add mode switching logic:**\n - If switching from hosted to BYOK: Clear hosted tokens, show API key setup instructions\n - If switching from BYOK to hosted: Register with gateway, get user token\n - Update `config.account.mode` in `.taskmasterconfig`\n - Update `config.account.telemetryEnabled` (true for hosted, false for BYOK)\n\n3. **Add non-interactive mode switching flags:**\n - Add `--switch-to-byok` flag to models command\n - Add `--switch-to-hosted` flag to models command \n - Allow direct mode switching without interactive prompts\n\n4. **Update command help and examples:**\n - Add examples showing mode switching\n - Update description to mention mode selection capability\n\n**Technical Details:**\n\n- Import needed functions from `user-management.js`: `updateUserConfig`, `getUserMode`, `registerUserWithGateway`\n- Use existing `selectAccessMode()` logic from `scripts/init.js` as reference\n- Ensure mode changes are reflected immediately in subsequent AI calls\n- Handle gateway registration gracefully with proper error messages\n- Show clear confirmation of mode changes with next steps\n\n**User Experience Flow:**\n```\n$ task-master models --setup\nāœ“ Current Mode: BYOK\n? Do you want to:\n > Keep current mode (BYOK) and configure models\n Switch to Hosted mode\n Cancel\n\n[If switching to hosted]\nšŸŽÆ Hosted API Gateway Selected\n→ Registering with gateway...\nāœ… Successfully switched to hosted mode\n→ All AI models are now available through the gateway\n→ No API keys needed in .env file\n\n[Then continue with model selection as normal]\n```\n\n**Files to Modify:**\n- `scripts/modules/commands.js` (main implementation)\n- `scripts/modules/task-manager/models.js` (if needed for MCP support)\n- Tests in `tests/integration/cli/` for new functionality\n\n**Success Criteria:**\n- Users can switch between BYOK and hosted mode via `task-master models --setup`\n- Non-interactive flags work: `--switch-to-byok` and `--switch-to-hosted`\n- Mode changes persist in `.taskmasterconfig`\n- Error handling for gateway connectivity issues\n- Clear user feedback about mode changes and next steps", + "testStrategy": "**Testing Approach:**\n\n1. **Integration Tests:**\n - Test mode switching from BYOK to hosted and vice versa\n - Verify `.taskmasterconfig` updates correctly with new mode\n - Test non-interactive flags `--switch-to-byok` and `--switch-to-hosted`\n - Test error handling when gateway is unavailable\n\n2. **Manual Testing:**\n - Run `task-master models --setup` in both modes\n - Verify UI flow and user experience\n - Test AI calls work correctly after mode switches\n - Verify API key requirements/removal after mode changes\n\n3. **Unit Tests:**\n - Test mode detection and switching logic\n - Test configuration updates\n - Test error scenarios (gateway down, invalid config)\n\n4. **End-to-End Testing:**\n - Full workflow: init in one mode → switch mode → configure models → test AI calls\n - Verify MCP tool equivalents work correctly after mode changes", + "status": "pending", + "dependencies": [ + 16 + ], + "priority": "high", + "subtasks": [] } ] } \ No newline at end of file diff --git a/tests/unit/scripts/modules/telemetry-enhancements.test.js b/tests/unit/scripts/modules/telemetry-enhancements.test.js index cf02ec06..f931a757 100644 --- a/tests/unit/scripts/modules/telemetry-enhancements.test.js +++ b/tests/unit/scripts/modules/telemetry-enhancements.test.js @@ -25,6 +25,7 @@ jest.unstable_mockModule( getAzureBaseURL: jest.fn(), getVertexProjectId: jest.fn(), getVertexLocation: jest.fn(), + writeConfig: jest.fn(() => true), MODEL_MAP: { openai: [ {