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
This commit is contained in:
Eyal Toledano
2025-05-31 19:47:18 -04:00
parent 769275b3bc
commit 9b87dd23de
11 changed files with 4699 additions and 4558 deletions

View File

@@ -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
}
}

134
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (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}`
);
if (gatewayRegistration.success) {
userId = gatewayRegistration.userId;
}
} else {
// Generate fallback userId if gateway unavailable
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
// 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,108 +455,79 @@ 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 (existingUserId) {
userSetupResult = await registerUserWithGateway(null, process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to validate existing user: ${userSetupResult.error}`
);
if (gatewayRegistration.success) {
userId = gatewayRegistration.userId;
}
} else {
// Generate fallback userId if gateway unavailable
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
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)
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"),
// STEP 2: Choose AI access method using inquirer
const modeResponse = await inquirer.prompt([
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "cyan",
title: "🎯 AI Access Setup",
titleAlignment: "center",
}
)
);
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",
},
]);
let choice;
while (true) {
choice = await promptQuestion(
rl,
chalk.cyan.bold("Your choice (1 or 2): ")
);
const selectedMode = modeResponse.accessMode;
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!"
@@ -553,18 +535,44 @@ async function initializeProject(options = {}) {
"\n" +
chalk.dim("Let's set up your subscription plan..."),
{
padding: 0.5,
margin: { top: 0.5, bottom: 0.5 },
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "green",
borderColor: selectedMode === "byok" ? "blue" : "green",
}
)
);
return "hosted";
} else {
console.log(chalk.red("Please enter 1 or 2"));
}
// 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,195 +1145,41 @@ 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() {
console.log(
boxen(
chalk.cyan.bold("🚀 Choose Your AI Access Method") +
"\n\n" +
chalk.white("TaskMaster supports two ways to access AI models:") +
"\n\n" +
chalk.yellow.bold("(1) BYOK - Bring Your Own API Keys") +
"\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"),
// Function to handle hosted subscription with browser pattern
async function handleHostedSubscription() {
const planResponse = await inquirer.prompt([
{
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."),
type: "list",
name: "plan",
message: "Select Your Monthly AI Credit Pack:",
choices: [
{
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 },
borderStyle: "round",
borderColor: "green",
title: "💳 Subscription Plans",
titleAlignment: "center",
}
)
);
const plans = [
{
name: "Starter",
credits: 50,
price: "$5/mo",
perCredit: "$0.10",
value: 1,
name: "50 credits - $5/mo [$0.10 per credit] - Perfect for personal projects",
value: { name: "Starter", credits: 50, price: "$5/mo", value: 1 },
},
{
name: "Popular",
credits: 120,
price: "$10/mo",
perCredit: "$0.083",
value: 2,
name: "120 credits - $10/mo [$0.083 per credit] - Popular choice",
value: { name: "Popular", credits: 120, price: "$10/mo", value: 2 },
},
{
name: "Pro",
credits: 250,
price: "$20/mo",
perCredit: "$0.08",
value: 3,
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",
perCredit: "$0.073",
value: 4,
},
];
},
],
default: 1, // Popular plan
},
]);
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];
const selectedPlan = planResponse.plan;
console.log(
boxen(
@@ -1334,10 +1188,8 @@ async function selectSubscriptionPlan() {
chalk.white(
`${selectedPlan.credits} credits/month for ${selectedPlan.price}`
) +
"\n" +
chalk.gray(`(${selectedPlan.perCredit} per credit)`) +
"\n\n" +
chalk.yellow("🔄 Opening Stripe checkout...") +
chalk.yellow("🔄 Opening browser for Stripe checkout...") +
"\n" +
chalk.dim("Complete your subscription setup in the browser."),
{
@@ -1349,39 +1201,76 @@ async function selectSubscriptionPlan() {
)
);
// 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)"));
// Stripe simulation with browser opening pattern (like Shopify CLI)
await simulateStripeCheckout(selectedPlan);
return selectedPlan;
} else {
console.log(chalk.red("Please enter a number from 1 to 4"));
}
}
}
// 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(`
<html>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #28a745;">✅ Subscription Complete!</h1>
<p>Your ${plan.name} plan (${plan.credits} credits/month) is now active.</p>
<p style="color: #666; margin-top: 30px;">You can close this window and return to your terminal.</p>
</body>
</html>
`);
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

View File

@@ -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<object>} 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,

File diff suppressed because it is too large Load Diff

View File

@@ -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";
}
/**

View File

@@ -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';
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'),
title: z.string().describe("Clear, concise title for the task"),
description: z
.string()
.describe('A one or two sentence description of the task'),
.describe("A one or two sentence description of the task"),
details: z
.string()
.describe('In-depth implementation details, considerations, and guidance'),
.describe("In-depth implementation details, considerations, and guidance"),
testStrategy: z
.string()
.describe('Detailed approach for verifying task completion'),
.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)'
)
"Array of task IDs that this task depends on (must be completed before this task can start)"
),
});
/**
@@ -62,7 +62,7 @@ async function addTask(
dependencies = [],
priority = null,
context = {},
outputFormat = 'text', // Default to text for CLI
outputFormat = "text", // Default to text for CLI
manualTaskData = null,
useResearch = false
) {
@@ -74,27 +74,27 @@ async function addTask(
? 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)
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}`
`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') => {
const report = (message, level = "info") => {
if (mcpLog) {
mcpLog[level](message);
} else if (outputFormat === 'text') {
} else if (outputFormat === "text") {
consoleLog(level, message);
}
};
@@ -156,7 +156,7 @@ async function addTask(
title: task.title,
description: task.description,
status: task.status,
dependencies: dependencyData
dependencies: dependencyData,
};
}
@@ -166,14 +166,14 @@ async function addTask(
// 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');
report("tasks.json not found or invalid. Creating a new one.", "info");
// Create default tasks data structure
data = {
tasks: []
tasks: [],
};
// Ensure the directory exists and write the new file
writeJSON(tasksPath, data);
report('Created new tasks.json file with empty tasks array.', 'info');
report("Created new tasks.json file with empty tasks array.", "info");
}
// Find the highest task ID to determine the next ID
@@ -182,13 +182,13 @@ async function addTask(
const newTaskId = highestId + 1;
// Only show UI box for CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
})
);
}
@@ -202,10 +202,10 @@ async function addTask(
if (invalidDeps.length > 0) {
report(
`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
'warn'
`The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`,
"warn"
);
report('Removing invalid dependencies...', 'info');
report("Removing invalid dependencies...", "info");
dependencies = dependencies.filter(
(depId) => !invalidDeps.includes(depId)
);
@@ -240,28 +240,28 @@ async function addTask(
// Check if manual task data is provided
if (manualTaskData) {
report('Using manually provided task data', 'info');
report("Using manually provided task data", "info");
taskData = manualTaskData;
report('DEBUG: Taking MANUAL task data path.', 'debug');
report("DEBUG: Taking MANUAL task data path.", "debug");
// Basic validation for manual data
if (
!taskData.title ||
typeof taskData.title !== 'string' ||
typeof taskData.title !== "string" ||
!taskData.description ||
typeof taskData.description !== 'string'
typeof taskData.description !== "string"
) {
throw new Error(
'Manual task data must include at least a title and description.'
"Manual task data must include at least a title and description."
);
}
} else {
report('DEBUG: Taking AI task generation path.', 'debug');
report("DEBUG: Taking AI task generation path.", "debug");
// --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
report(`Generating task data with AI with prompt:\n${prompt}`, "info");
// Create context string for task creation prompt
let contextTasks = '';
let contextTasks = "";
// Create a dependency map for better understanding of the task relationships
const taskMap = {};
@@ -272,18 +272,18 @@ async function addTask(
title: t.title,
description: t.description,
dependencies: t.dependencies || [],
status: t.status
status: t.status,
};
});
// CLI-only feedback for the dependency analysis
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
boxen(chalk.cyan.bold('Task Context Analysis') + '\n', {
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'
borderColor: "cyan",
borderStyle: "round",
})
);
}
@@ -314,7 +314,7 @@ async function addTask(
const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id)
);
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
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(
@@ -325,7 +325,7 @@ async function addTask(
contextTasks += `\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
.join("\n")}`;
if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
}
@@ -336,15 +336,15 @@ async function addTask(
for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})`
: '';
: "";
const isDirect = numericDependencies.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
? " [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`;
contextTasks += `Status: ${depTask.status || "pending"}\n`;
contextTasks += `Priority: ${depTask.priority || "medium"}\n`;
// List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) {
@@ -354,7 +354,7 @@ async function addTask(
? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`;
});
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
contextTasks += `Dependencies: ${depDeps.join(", ")}\n`;
} else {
contextTasks += `Dependencies: None\n`;
}
@@ -363,7 +363,7 @@ async function addTask(
if (depTask.details) {
const truncatedDetails =
depTask.details.length > 400
? depTask.details.substring(0, 400) + '... (truncated)'
? depTask.details.substring(0, 400) + "... (truncated)"
: depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -371,19 +371,19 @@ async function addTask(
// Add dependency chain visualization
if (dependencyGraphs.length > 0) {
contextTasks += '\n\nDependency Chain Visualization:';
contextTasks += "\n\nDependency Chain Visualization:";
// Helper function to format dependency chain as text
function formatDependencyChain(
node,
prefix = '',
prefix = "",
isLast = true,
depth = 0
) {
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
if (depth > 3) return ""; // Limit depth to avoid excessive nesting
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
@@ -409,7 +409,7 @@ async function addTask(
}
// Show dependency analysis in CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => {
@@ -449,14 +449,14 @@ async function addTask(
// Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph(
node,
prefix = '',
prefix = "",
isLast = true,
depth = 0
) {
if (depth > 2) return; // Limit depth for display
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
console.log(
chalk.blue(
@@ -492,18 +492,18 @@ async function addTask(
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
{ 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 }
{ 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
limit: 15,
};
// Prepare task data with dependencies expanded as titles for better semantic search
@@ -514,15 +514,15 @@ async function addTask(
? task.dependencies
.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : '';
return depTask ? depTask.title : "";
})
.filter((title) => title)
.join(' ')
: '';
.join(" ")
: "";
return {
...task,
dependencyTitles
dependencyTitles,
};
});
@@ -532,7 +532,7 @@ async function addTask(
// Extract significant words and phrases from the prompt
const promptWords = prompt
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
@@ -598,13 +598,13 @@ async function addTask(
// 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
{ 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) =>
@@ -626,33 +626,33 @@ async function addTask(
if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => {
const relevanceMarker = i < highRelevance.length ? '' : '';
const relevanceMarker = i < highRelevance.length ? "" : "";
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
})
.join('\n')}`;
.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')}`;
.join("\n")}`;
}
if (
recentTasks.length > 0 &&
!contextTasks.includes('Recently created tasks')
!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')}`;
.join("\n")}`;
}
// Add detailed information about the most relevant tasks
const allDetailedTasks = [
...relatedTasks.slice(0, 5),
...categoryTasks.slice(0, 2)
...categoryTasks.slice(0, 2),
];
uniqueDetailedTasks = Array.from(
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
@@ -663,8 +663,8 @@ async function addTask(
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`;
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) => {
@@ -673,13 +673,13 @@ async function addTask(
? `Task ${depId} (${depTask.title})`
: `Task ${depId}`;
});
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
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.substring(0, 400) + "... (truncated)"
: task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -687,7 +687,7 @@ async function addTask(
}
// Add a concise view of the task dependency structure
contextTasks += '\n\nSummary of task dependencies in the project:';
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
@@ -695,7 +695,7 @@ async function addTask(
const relevantPendingTasks = data.tasks
.filter(
(t) =>
(t.status === 'pending' || t.status === 'in-progress') &&
(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(
@@ -709,8 +709,8 @@ async function addTask(
for (const task of relevantPendingTasks) {
const depsStr =
task.dependencies && task.dependencies.length > 0
? task.dependencies.join(', ')
: 'None';
? task.dependencies.join(", ")
: "None";
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
}
@@ -726,7 +726,7 @@ async function addTask(
let commonDeps = []; // Initialize commonDeps
if (similarPurposeTasks.length > 0) {
contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : 'similar'} tasks:`;
contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : "similar"} tasks:`;
// Collect dependencies from similar purpose tasks
const similarDeps = similarPurposeTasks
@@ -746,7 +746,7 @@ async function addTask(
.slice(0, 5);
if (commonDeps.length > 0) {
contextTasks += '\nMost common dependencies for similar tasks:';
contextTasks += "\nMost common dependencies for similar tasks:";
commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
@@ -757,7 +757,7 @@ async function addTask(
}
// Show fuzzy search analysis in CLI mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
console.log(
chalk.gray(
` Fuzzy search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
@@ -825,7 +825,7 @@ async function addTask(
const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id
);
const relevanceIndicator = isHighRelevance ? '' : '';
const relevanceIndicator = isHighRelevance ? "" : "";
console.log(
chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
@@ -853,26 +853,26 @@ async function addTask(
}
// Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === 'text') {
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(
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)}`
: ''),
? `\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'
borderColor: "white",
borderStyle: "round",
}
)
);
@@ -882,15 +882,15 @@ async function addTask(
// 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' +
"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';
"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 = `
@@ -904,7 +904,7 @@ async function addTask(
`;
// Add any manually provided details to the prompt for context
let contextFromArgs = '';
let contextFromArgs = "";
if (manualTaskData?.title)
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
if (manualTaskData?.description)
@@ -918,7 +918,7 @@ async function addTask(
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.
@@ -929,15 +929,15 @@ async function addTask(
`;
// Start the loading indicator - only for text mode
if (outputFormat === 'text') {
if (outputFormat === "text") {
loadingIndicator = startLoadingIndicator(
`Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n`
`Generating new task with ${useResearch ? "Research" : "Main"} AI...\n`
);
}
try {
const serviceRole = useResearch ? 'research' : 'main';
report('DEBUG: Calling generateObjectService...', 'debug');
const serviceRole = useResearch ? "research" : "main";
report("DEBUG: Calling generateObjectService...", "debug");
aiServiceResponse = await generateObjectService({
// Capture the full response
@@ -945,17 +945,17 @@ async function addTask(
session: session,
projectRoot: projectRoot,
schema: AiTaskDataSchema,
objectName: 'newTaskData',
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
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');
report("DEBUG: generateObjectService returned successfully.", "debug");
if (!aiServiceResponse || !aiServiceResponse.mainResult) {
throw new Error(
'AI service did not return the expected object structure.'
"AI service did not return the expected object structure."
);
}
@@ -972,20 +972,20 @@ async function addTask(
) {
taskData = aiServiceResponse.mainResult.object;
} else {
throw new Error('AI service did not return a valid task object.');
throw new Error("AI service did not return a valid task object.");
}
report('Successfully generated task data from AI.', 'success');
report("Successfully generated task data from AI.", "success");
} catch (error) {
report(
`DEBUG: generateObjectService caught error: ${error.message}`,
'debug'
"debug"
);
report(`Error generating task with AI: ${error.message}`, 'error');
// 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');
report("DEBUG: generateObjectService finally block reached.", "debug");
if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops
}
// --- End Refactored AI Interaction ---
@@ -996,14 +996,14 @@ async function addTask(
id: newTaskId,
title: taskData.title,
description: taskData.description,
details: taskData.details || '',
testStrategy: taskData.testStrategy || '',
status: 'pending',
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
subtasks: [], // Initialize with empty subtasks array
};
// Additional check: validate all dependencies in the AI response
@@ -1015,8 +1015,8 @@ async function addTask(
if (!allValidDeps) {
report(
'AI suggested invalid dependencies. Filtering them out...',
'warn'
"AI suggested invalid dependencies. Filtering them out...",
"warn"
);
newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10);
@@ -1028,48 +1028,48 @@ async function addTask(
// Add the task to the tasks array
data.tasks.push(newTask);
report('DEBUG: Writing tasks.json...', 'debug');
report("DEBUG: Writing tasks.json...", "debug");
// Write the updated tasks to the file
writeJSON(tasksPath, data);
report('DEBUG: tasks.json written.', 'debug');
report("DEBUG: tasks.json written.", "debug");
// Generate markdown task files
report('Generating task files...', 'info');
report('DEBUG: Calling generateTaskFiles...', 'debug');
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');
report("DEBUG: generateTaskFiles finished.", "debug");
// Show success message - only for text output (CLI)
if (outputFormat === 'text') {
if (outputFormat === "text") {
const table = new Table({
head: [
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Description')
chalk.cyan.bold("ID"),
chalk.cyan.bold("Title"),
chalk.cyan.bold("Description"),
],
colWidths: [5, 30, 50] // Adjust widths as needed
colWidths: [5, 30, 50], // Adjust widths as needed
});
table.push([
newTask.id,
truncate(newTask.title, 27),
truncate(newTask.description, 47)
truncate(newTask.description, 47),
]);
console.log(chalk.green('✅ New task created successfully:'));
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':
case "high":
return "red";
case "low":
return "gray";
case "medium":
default:
return 'yellow';
return "yellow";
}
};
@@ -1093,49 +1093,49 @@ async function addTask(
});
// Prepare dependency display string
let dependencyDisplay = '';
let dependencyDisplay = "";
if (newTask.dependencies.length > 0) {
dependencyDisplay = chalk.white('Dependencies:') + '\n';
dependencyDisplay = chalk.white("Dependencies:") + "\n";
newTask.dependencies.forEach((dep) => {
const isAiAdded = aiAddedDeps.includes(dep);
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : "";
dependencyDisplay +=
chalk.white(
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
) + '\n';
` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}`
) + "\n";
});
} else {
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
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';
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';
const title = depTask ? truncate(depTask.title, 30) : "Unknown task";
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n";
});
}
// Add dependency analysis summary
let dependencyAnalysis = '';
let dependencyAnalysis = "";
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
dependencyAnalysis =
'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
"\n" + chalk.white.bold("Dependency Analysis:") + "\n";
if (aiAddedDeps.length > 0) {
dependencyAnalysis +=
chalk.green(
`AI identified ${aiAddedDeps.length} additional dependencies`
) + '\n';
) + "\n";
}
if (aiRemovedDeps.length > 0) {
dependencyAnalysis +=
chalk.yellow(
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
) + '\n';
) + "\n";
}
}
@@ -1143,32 +1143,32 @@ async function addTask(
console.log(
boxen(
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
'\n\n' +
"\n\n" +
chalk.white(`Title: ${newTask.title}`) +
'\n' +
"\n" +
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
'\n' +
"\n" +
chalk.white(
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
) +
'\n\n' +
"\n\n" +
dependencyDisplay +
dependencyAnalysis +
'\n' +
chalk.white.bold('Next Steps:') +
'\n' +
"\n" +
chalk.white.bold("Next Steps:") +
"\n" +
chalk.cyan(
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
) +
'\n' +
"\n" +
chalk.cyan(
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
) +
'\n' +
"\n" +
chalk.cyan(
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
{ padding: 1, borderColor: "green", borderStyle: "round" }
)
);
@@ -1176,19 +1176,19 @@ async function addTask(
if (
aiServiceResponse &&
aiServiceResponse.telemetryData &&
(outputType === 'cli' || outputType === 'text')
(outputType === "cli" || outputType === "text")
) {
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
displayAiUsageSummary(aiServiceResponse.telemetryData, "cli");
}
}
report(
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
'debug'
"debug"
);
return {
newTaskId: newTaskId,
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
};
} catch (error) {
// Stop any loading indicator on error
@@ -1196,8 +1196,8 @@ async function addTask(
stopLoadingIndicator(loadingIndicator);
}
report(`Error adding task: ${error.message}`, 'error');
if (outputFormat === 'text') {
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

View File

@@ -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,
};

View File

@@ -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": []
}
]
}

View File

@@ -25,6 +25,7 @@ jest.unstable_mockModule(
getAzureBaseURL: jest.fn(),
getVertexProjectId: jest.fn(),
getVertexLocation: jest.fn(),
writeConfig: jest.fn(() => true),
MODEL_MAP: {
openai: [
{