feat: Implement TaskMaster AI Gateway integration with enhanced UX

- Fix Zod schema conversion, update headers, add premium telemetry display, improve user auth flow, and standardize email fields

Functionally complete on this end, mostly polish around user experience and need to add in profile, upgrade/downgrade, etc.

But the AI commands are working off the gateway.
This commit is contained in:
Eyal Toledano
2025-06-01 19:37:12 -04:00
parent 9b87dd23de
commit 2819be51d3
11 changed files with 456 additions and 246 deletions

View File

@@ -26,11 +26,12 @@
"defaultPriority": "medium",
"projectName": "Taskmaster",
"ollamaBaseURL": "http://localhost:11434/api",
"azureBaseURL": "https://your-endpoint.azure.com/"
"azureBaseURL": "https://your-endpoint.azure.com/",
"ollamaBaseUrl": "http://localhost:11434/api"
},
"account": {
"userId": "1234567890",
"userEmail": "",
"email": "",
"mode": "byok",
"telemetryEnabled": true
}

View File

@@ -75,7 +75,8 @@
"ora": "^8.2.0",
"task-master-ai": "^0.15.0",
"uuid": "^11.1.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.5"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -385,51 +385,42 @@ async function initializeProject(options = {}) {
};
}
// NON-INTERACTIVE MODE - Use proper auth/init flow
let userSetupResult;
// NON-INTERACTIVE MODE - Try auth/init gracefully
let userSetupResult = null;
let isGatewayAvailable = false;
// Try to initialize user, but don't throw errors if it fails
try {
// 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")
);
existingUserId = existingConfig.account?.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}`
);
userSetupResult = await initializeUser(process.cwd());
if (userSetupResult.success) {
isGatewayAvailable = true;
if (!isSilentMode()) {
log("info", "Gateway connection successful");
}
} else {
// Create new user through auth/init
userSetupResult = await initializeUser(process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to initialize user: ${userSetupResult.error}`
);
if (!isSilentMode()) {
log("info", "Gateway not available, using BYOK mode");
}
isGatewayAvailable = false;
}
} catch (error) {
log("error", `User initialization failed: ${error.message}`);
throw error; // Don't fall back to random userId!
// Silent failure - gateway not available
if (!isSilentMode()) {
log("info", "Gateway not available, using BYOK mode");
}
isGatewayAvailable = false;
userSetupResult = null;
}
// Create project structure with properly authenticated userId
// Create project structure - always use BYOK for non-interactive mode
// since we don't want to prompt for mode selection
createProjectStructure(
addAliases,
dryRun,
userSetupResult, // Pass the full auth result
"byok", // or determine from result
userSetupResult, // Pass the auth result (may be null)
"byok", // Always use BYOK for non-interactive
null,
userSetupResult.userId || null
userSetupResult?.userId || null
);
} else {
// Interactive logic - NEW FLOW STARTS HERE
@@ -440,7 +431,7 @@ async function initializeProject(options = {}) {
});
try {
// STEP 1: Create/find userId first
// STEP 1: Welcome message
console.log(
boxen(
chalk.blue.bold("🚀 Welcome to Taskmaster AI") +
@@ -455,47 +446,45 @@ async function initializeProject(options = {}) {
)
);
// INTERACTIVE MODE - Also use proper auth/init flow
// STEP 1: Proper user setup
let userSetupResult;
// STEP 2: Try auth/init gracefully to detect gateway availability
let userSetupResult = null;
let isGatewayAvailable = false;
try {
// 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")
);
existingUserId = existingConfig.account?.userId;
}
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}`
);
if (userSetupResult.success) {
isGatewayAvailable = true;
console.log(
boxen(
chalk.green("✅ Gateway Connection Successful") +
"\n\n" +
chalk.white("TaskMaster AI Gateway is available.") +
"\n" +
chalk.white("You can choose between BYOK or Hosted mode."),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "green",
}
)
);
} else {
// Silent failure - gateway not available
isGatewayAvailable = false;
}
} catch (error) {
log("error", `User initialization failed: ${error.message}`);
// Don't fall back to random userId - exit or prompt user
throw error;
// Silent failure - gateway not available
isGatewayAvailable = false;
userSetupResult = null;
}
// STEP 2: Choose AI access method using inquirer
// STEP 3: Choose AI access method (conditional based on gateway availability)
let selectedMode = "byok"; // Default to BYOK
let selectedPlan = null;
if (isGatewayAvailable) {
// Gateway is available, show both options
const modeResponse = await inquirer.prompt([
{
type: "list",
@@ -514,8 +503,7 @@ async function initializeProject(options = {}) {
default: "hosted",
},
]);
const selectedMode = modeResponse.accessMode;
selectedMode = modeResponse.accessMode;
console.log(
boxen(
@@ -543,13 +531,34 @@ async function initializeProject(options = {}) {
)
);
// STEP 3: If hosted mode, handle subscription plan with Stripe simulation
let selectedPlan = null;
// If hosted mode selected, handle subscription plan
if (selectedMode === "hosted") {
selectedPlan = await handleHostedSubscription();
}
} else {
// Gateway not available, silently proceed with BYOK mode
// Show standard BYOK mode message without mentioning gateway failure
console.log(
boxen(
chalk.blue.bold("🔑 BYOK Mode") +
"\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",
}
)
);
selectedMode = "byok";
}
// STEP 4: Continue with aliases (this fixes the hanging issue)
// STEP 4: Continue with aliases
const aliasResponse = await inquirer.prompt([
{
type: "confirm",
@@ -560,7 +569,6 @@ async function initializeProject(options = {}) {
]);
const addAliases = aliasResponse.addAliases;
const dryRun = options.dryRun || false;
// STEP 5: Show overview and continue with project creation
@@ -571,7 +579,7 @@ async function initializeProject(options = {}) {
userSetupResult,
selectedMode,
selectedPlan,
userSetupResult.userId
userSetupResult?.userId || null
);
} catch (error) {
rl.close();
@@ -856,7 +864,7 @@ function configureTaskmasterConfig(
// Store account-specific configuration
config.account.mode = selectedMode;
config.account.userId = userId || null;
config.account.userEmail = gatewayRegistration?.email || "";
config.account.email = gatewayRegistration?.email || "";
config.account.telemetryEnabled = selectedMode === "hosted";
// Store remaining global config items

View File

@@ -43,6 +43,8 @@ import {
VertexAIProvider,
} from "../../src/ai-providers/index.js";
import { zodToJsonSchema } from "zod-to-json-schema";
// Create provider instances
const PROVIDERS = {
anthropic: new AnthropicAIProvider(),
@@ -293,11 +295,11 @@ async function _callGatewayAI(
initialRole
) {
// 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
const gatewayUrl = "http://localhost:4444";
const serviceId = "98fb3198-2dfc-42d1-af53-07b99e4f3bde"; // Hardcoded service ID -- if you change this, the Hosted Gateway will not work
// Get user auth info for headers
const userMgmt = require("./user-management.js");
const userMgmt = await import("./user-management.js");
const userToken = await userMgmt.getUserToken(projectRoot);
const userEmail = await userMgmt.getUserEmail(projectRoot);
@@ -316,6 +318,8 @@ async function _callGatewayAI(
callParams.messages?.find((m) => m.role === "user")?.content || "";
const requestBody = {
provider: providerName,
serviceType,
role: initialRole,
messages: callParams.messages,
modelId,
@@ -326,14 +330,14 @@ async function _callGatewayAI(
temperature: callParams.temperature,
},
...(serviceType === "generateObject" && {
schema: callParams.schema,
schema: zodToJsonSchema(callParams.schema),
objectName: callParams.objectName,
}),
};
const headers = {
"Content-Type": "application/json",
"X-TaskMaster-API-Key": serviceApiKey, // Service-level auth (hardcoded)
"X-TaskMaster-Service-ID": serviceId, // TaskMaster service ID for instance auth
Authorization: `Bearer ${userToken}`, // User-level auth
};
@@ -405,6 +409,10 @@ async function _unifiedServiceRunner(serviceType, params) {
outputType,
projectRoot,
});
if (isHostedMode(projectRoot)) {
log("info", "Communicating with Taskmaster Gateway");
}
}
const effectiveProjectRoot = projectRoot || findProjectRoot();
@@ -426,10 +434,12 @@ async function _unifiedServiceRunner(serviceType, params) {
log("info", "User successfully authenticated with gateway");
} else {
log("warn", `Silent auth/init failed: ${initResult.error}`);
// Silent failure - only log at debug level during init sequence
log("debug", `Silent auth/init failed: ${initResult.error}`);
}
} catch (error) {
log("warn", `Silent auth/init attempt failed: ${error.message}`);
// Silent failure - only log at debug level during init sequence
log("debug", `Silent auth/init attempt failed: ${error.message}`);
}
}

View File

@@ -66,9 +66,9 @@ const defaultConfig = {
},
account: {
userId: "1234567890", // Placeholder that triggers auth/init
userEmail: "",
email: "",
mode: "byok",
telemetryEnabled: false,
telemetryEnabled: true,
},
};
@@ -129,7 +129,6 @@ function _loadAndValidateConfig(explicitRoot = null) {
: { ...defaults.models.fallback },
},
global: { ...defaults.global, ...parsedConfig?.global },
ai: { ...defaults.ai, ...parsedConfig?.ai },
account: { ...defaults.account, ...parsedConfig?.account },
};
configSource = `file (${configPath})`; // Update source info
@@ -756,7 +755,7 @@ function getTelemetryEnabled(explicitRoot = null) {
// Update getUserEmail to use account
function getUserEmail(explicitRoot = null) {
const config = getConfig(explicitRoot);
return config.account?.userEmail || "";
return config.account?.email || "";
}
// Update getMode function to use account

View File

@@ -32,31 +32,24 @@ const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second
/**
* Get telemetry configuration from environment variables only
* @returns {Object} Configuration object with apiKey, userId, and email
* Get telemetry configuration from hardcoded service ID, user token, and config
* @returns {Object} Configuration object with serviceId, apiKey, userId, and email
*/
function getTelemetryConfig() {
// Try environment variables first (includes .env file via resolveEnvVariable)
const envApiKey =
resolveEnvVariable("TASKMASTER_API_KEY") ||
resolveEnvVariable("GATEWAY_API_KEY") ||
resolveEnvVariable("TELEMETRY_API_KEY");
const envUserId =
resolveEnvVariable("TASKMASTER_USER_ID") ||
resolveEnvVariable("GATEWAY_USER_ID") ||
resolveEnvVariable("TELEMETRY_USER_ID");
const envEmail =
resolveEnvVariable("TASKMASTER_USER_EMAIL") ||
resolveEnvVariable("GATEWAY_USER_EMAIL") ||
resolveEnvVariable("TELEMETRY_USER_EMAIL");
// Get the config (which might contain userId)
// Get the config which contains userId and email
const config = getConfig();
// Hardcoded service ID for TaskMaster telemetry service
const hardcodedServiceId = "98fb3198-2dfc-42d1-af53-07b99e4f3bde";
// Get user's API token from .env (managed by user-management.js)
const userApiKey = resolveEnvVariable("TASKMASTER_API_KEY");
return {
apiKey: envApiKey || null, // API key should only come from environment
userId: envUserId || config?.account?.userId || null,
email: envEmail || null,
serviceId: hardcodedServiceId, // Hardcoded service identifier
apiKey: userApiKey || null, // User's Bearer token from .env
userId: config?.account?.userId || null, // From config
email: config?.account?.email || null, // From config
};
}
@@ -119,8 +112,11 @@ export async function registerUserWithGateway(email = null, userId = null) {
*/
export async function submitTelemetryData(telemetryData) {
try {
// Check user opt-out preferences first
if (!getTelemetryEnabled()) {
// Check user opt-out preferences first, but hosted mode always sends telemetry
const config = getConfig();
const isHostedMode = config?.account?.mode === "hosted";
if (!isHostedMode && !getTelemetryEnabled()) {
return {
success: true,
skipped: true,
@@ -138,7 +134,7 @@ export async function submitTelemetryData(telemetryData) {
return {
success: false,
error:
"Telemetry configuration incomplete. Run 'task-master init' and select hosted gateway option, or manually set TASKMASTER_API_KEY, TASKMASTER_USER_ID, and TASKMASTER_USER_EMAIL environment variables",
"Telemetry configuration incomplete. Please ensure you have completed 'task-master init' to set up your user account.",
};
}
@@ -167,8 +163,9 @@ export async function submitTelemetryData(telemetryData) {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${telemetryConfig.apiKey}`, // Use Bearer token format
"X-User-Email": telemetryConfig.email, // Add required email header
"X-Service-ID": telemetryConfig.serviceId, // Hardcoded service ID
Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token
"X-User-Email": telemetryConfig.email, // User's email from config
},
body: JSON.stringify(completeTelemetryData),
});

View File

@@ -1513,18 +1513,16 @@ async function displayComplexityReport(reportPath) {
)
);
const readline = require("readline").createInterface({
const readline = await import("readline");
const rl = readline.default.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise((resolve) => {
readline.question(
chalk.cyan("Generate complexity report? (y/n): "),
resolve
);
rl.question(chalk.cyan("Generate complexity report? (y/n): "), resolve);
});
readline.close();
rl.close();
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
// Call the analyze-complexity command
@@ -2029,43 +2027,114 @@ function displayAvailableModels(availableModels) {
* @param {string} outputType - 'cli' or 'mcp' (though typically only called for 'cli').
*/
function displayAiUsageSummary(telemetryData, outputType = "cli") {
if (
(outputType !== "cli" && outputType !== "text") ||
!telemetryData ||
isSilentMode()
) {
return; // Only display for CLI and if data exists and not in silent mode
}
if (!telemetryData || outputType !== "cli") return;
const {
modelUsed,
providerName,
inputTokens,
outputTokens,
totalTokens,
totalCost,
commandName,
providerName,
modelUsed,
inputTokens = 0,
outputTokens = 0,
totalTokens = 0,
totalCost = 0,
accountInfo,
} = telemetryData;
let summary = chalk.bold.blue("AI Usage Summary:") + "\n";
summary += chalk.gray(` Command: ${commandName}\n`);
summary += chalk.gray(` Provider: ${providerName}\n`);
summary += chalk.gray(` Model: ${modelUsed}\n`);
summary += chalk.gray(
` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
);
summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`);
const isHostedMode = !!accountInfo;
// Build the core telemetry content (same for both modes)
let content =
chalk.cyan.bold("AI Usage Summary:") +
"\n" +
chalk.gray(" Command: ") +
chalk.white(commandName) +
"\n" +
chalk.gray(" Provider: ") +
chalk.white(providerName) +
"\n" +
chalk.gray(" Model: ") +
chalk.white(modelUsed) +
"\n" +
chalk.gray(" Tokens: ") +
chalk.white(
`${totalTokens.toLocaleString()} (Input: ${inputTokens.toLocaleString()}, Output: ${outputTokens.toLocaleString()})`
);
if (isHostedMode) {
// Hosted mode - add premium credit information
const creditsUsed = accountInfo.creditsUsed || 0;
const remainingCredits = accountInfo.remainingCredits || 0;
const totalCredits = creditsUsed + remainingCredits;
const remainingPercentage =
totalCredits > 0 ? (remainingCredits / totalCredits) * 100 : 0;
// Create credit bar showing REMAINING credits (goes down as credits are consumed)
const barLength = 20;
const filledLength = Math.round((remainingPercentage / 100) * barLength);
const emptyLength = barLength - filledLength;
const creditBar =
chalk.green("█".repeat(filledLength)) +
chalk.gray("█".repeat(emptyLength));
// Determine footer message based on remaining credits
let footerMessage;
if (remainingPercentage <= 20) {
footerMessage = chalk.yellow(
"⚠️ Running low on credits - consider topping up soon"
);
} else {
footerMessage = chalk.dim("🚀 Powered by hosted AI infrastructure");
}
content +=
"\n" +
chalk.gray(" Credits Used: ") +
chalk.yellow.bold(creditsUsed.toLocaleString()) +
"\n" +
chalk.gray(" Remaining: ") +
chalk.green.bold(remainingCredits.toLocaleString()) +
"\n" +
chalk.gray(" Usage: ") +
`[${creditBar}] ${remainingPercentage.toFixed(1)}%` +
"\n\n" +
footerMessage;
// Premium double border
console.log(
boxen(summary, {
padding: 1,
margin: { top: 1 },
borderColor: "blue",
borderStyle: "round",
title: "💡 Telemetry",
boxen(content, {
padding: { top: 1, bottom: 1, left: 2, right: 2 },
margin: { top: 1, bottom: 1 },
borderStyle: "double",
borderColor: "cyan",
title: chalk.yellow.bold("💎 PREMIUM TELEMETRY"),
titleAlignment: "center",
})
);
} else {
// BYOK mode - add cost and upgrade CTA
content +=
"\n" +
chalk.gray(" Est. Cost: ") +
chalk.white(`$${totalCost.toFixed(6)}`) +
"\n\n" +
chalk.cyan(
"⚡ Upgrade to TaskMaster Premium for instant access to all AI models"
) +
"\n" +
chalk.dim("Learn more: https://taskmaster.ai/premium");
// Regular single border
console.log(
boxen(content, {
padding: { top: 1, bottom: 1, left: 2, right: 2 },
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "blue",
title: chalk.blue.bold("💡 TELEMETRY"),
titleAlignment: "center",
})
);
}
}
/**

View File

@@ -1,7 +1,7 @@
import fs from "fs";
import path from "path";
import { log, findProjectRoot } from "./utils.js";
import { getConfig, writeConfig } from "./config-manager.js";
import { getConfig, writeConfig, getUserId } from "./config-manager.js";
/**
* Registers or finds a user via the gateway's /auth/init endpoint
@@ -14,8 +14,18 @@ async function registerUserWithGateway(email = null, explicitRoot = null) {
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// Email is optional - only send if provided
const requestBody = email ? { email } : {};
// Check for existing userId and email to pass to gateway
const existingUserId = getUserId(explicitRoot);
const existingEmail = email || getUserEmail(explicitRoot);
// Build request body with existing values (gateway can handle userId for existing users)
const requestBody = {};
if (existingUserId && existingUserId !== "1234567890") {
requestBody.userId = existingUserId;
}
if (existingEmail) {
requestBody.email = existingEmail;
}
const response = await fetch(`${gatewayUrl}/auth/init`, {
method: "POST",
@@ -70,10 +80,17 @@ async function registerUserWithGateway(email = null, explicitRoot = null) {
* @param {string} userId - User ID from gateway
* @param {string} token - User authentication token from gateway (stored in .env)
* @param {string} mode - User mode ('byok' or 'hosted')
* @param {string|null} email - Optional user email to save
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} Success status
*/
function updateUserConfig(userId, token, mode, explicitRoot = null) {
function updateUserConfig(
userId,
token,
mode,
email = null,
explicitRoot = null
) {
try {
const config = getConfig(explicitRoot);
@@ -82,10 +99,20 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) {
config.account = {};
}
// Ensure global section exists for email
if (!config.global) {
config.global = {};
}
// Update user configuration in account section
config.account.userId = userId;
config.account.mode = mode; // 'byok' or 'hosted'
// Save email if provided
if (email) {
config.account.email = email;
}
// Write user authentication token to .env file (not config)
if (token) {
writeApiKeyToEnv(token, explicitRoot);
@@ -94,7 +121,11 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) {
// Save updated config
const success = writeConfig(config, explicitRoot);
if (success) {
log("info", `User configuration updated: userId=${userId}, mode=${mode}`);
const emailInfo = email ? `, email=${email}` : "";
log(
"info",
`User configuration updated: userId=${userId}, mode=${mode}${emailInfo}`
);
} else {
log("error", "Failed to write updated user configuration");
}
@@ -219,10 +250,12 @@ async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
};
}
// Step 2: Update config with userId and mode
const configResult = await updateUserConfig(
// Step 2: Update config with userId, mode, and email
const configResult = updateUserConfig(
registrationResult.userId,
registrationResult.token,
mode,
email,
explicitRoot
);
@@ -261,22 +294,20 @@ async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
*/
async function initializeUser(explicitRoot = null) {
try {
// Register with gateway without email
// Try to register with gateway without email
const result = await registerUserWithGateway(null, explicitRoot);
if (!result.success) {
return {
success: false,
userId: "",
error: result.error,
};
}
// If gateway call succeeded, use the returned values
if (result.success) {
// Update config with userId, token, and preserve existing mode (or default)
const existingMode = getUserMode(explicitRoot);
const modeToUse = existingMode !== "unknown" ? existingMode : "byok";
// Update config with userId, token, and default hosted mode
const configResult = updateUserConfig(
result.userId,
result.token, // Include the token parameter
"hosted", // Default to hosted mode until user chooses plan
result.token,
modeToUse,
null,
explicitRoot
);
@@ -295,6 +326,27 @@ async function initializeUser(explicitRoot = null) {
? "New user registered with gateway"
: "Existing user found in gateway",
};
}
// Gateway call failed - check if we have existing credentials to use
const existingUserId = getUserId(explicitRoot);
const existingToken = getUserToken(explicitRoot);
if (existingUserId && existingUserId !== "1234567890" && existingToken) {
// We have existing credentials, use them (gateway unavailable scenario)
return {
success: true,
userId: existingUserId,
message: "Gateway unavailable, using existing user credentials",
};
}
// No existing credentials and gateway failed
return {
success: false,
userId: "",
error: result.error,
};
} catch (error) {
return {
success: false,
@@ -351,7 +403,7 @@ function getUserToken(explicitRoot = null) {
function getUserEmail(explicitRoot = null) {
try {
const config = getConfig(explicitRoot);
return config?.global?.email || null;
return config?.account?.email || null;
} catch (error) {
log("error", `Error getting user email: ${error.message}`);
return null;

View File

@@ -6291,6 +6291,77 @@
],
"priority": "high",
"subtasks": []
},
{
"id": 95,
"title": "Implement Stripe Integration and Account Mode Synchronization",
"description": "Implement client-side account status synchronization by calling a user profile endpoint during TaskMaster initialization to retrieve current account status from the server and update the local .taskmasterconfig file accordingly",
"status": "pending",
"dependencies": [],
"priority": "high",
"details": "Add functionality to TaskMaster's initialization process that calls a user profile endpoint to fetch the current account status (including subscription status determined by server-side Stripe webhook integration). Update the local .taskmasterconfig file's account.mode field based on the server response. This ensures that the client always has the most current account status without needing to handle Stripe webhooks directly, as the server/gateway will manage the Stripe integration and webhook processing.",
"testStrategy": "Test the profile endpoint call during init, verify .taskmasterconfig updates correctly based on different account statuses, and ensure proper error handling when the endpoint is unavailable",
"subtasks": [
{
"id": "95-1",
"title": "Create user profile endpoint client function",
"description": "Implement a client function to call the user profile endpoint and retrieve account status",
"status": "pending"
},
{
"id": "95-2",
"title": "Integrate profile endpoint call into TaskMaster init process",
"description": "Add the profile endpoint call to the TaskMaster initialization sequence",
"status": "pending"
},
{
"id": "95-3",
"title": "Update .taskmasterconfig with server account status",
"description": "Implement logic to update the local .taskmasterconfig file's account.mode field based on the profile endpoint response",
"status": "pending"
},
{
"id": "95-4",
"title": "Add error handling for profile endpoint failures",
"description": "Implement proper error handling when the profile endpoint is unavailable or returns errors",
"status": "pending"
},
{
"id": "95-5",
"title": "Test account status synchronization",
"description": "Create tests to verify that account status is properly synchronized between server and client during initialization",
"status": "pending"
}
]
},
{
"id": 96,
"title": "Create Test Task Generation and Validation System",
"description": "Implement a system for generating test tasks with proper validation, structure, and integration with the existing task management framework.",
"details": "Build a comprehensive test task generation system that includes:\n\n1. Test Task Template System:\n - Create standardized templates for different types of test tasks (unit, integration, e2e)\n - Implement template variables for dynamic content generation\n - Support for test-specific metadata (test type, coverage areas, execution time)\n\n2. Test Task Validation:\n - Validate test task structure against schema requirements\n - Ensure test tasks have proper dependencies on implementation tasks\n - Verify test strategy completeness and specificity\n - Check for required test metadata fields\n\n3. Integration with Task Management:\n - Extend existing task creation commands to support test task generation\n - Add test task filtering and categorization in list commands\n - Implement test task execution tracking and results storage\n - Support for test task hierarchies and grouping\n\n4. Test Task Automation:\n - Auto-generate test tasks when implementation tasks are created\n - Implement test task dependency resolution based on implementation dependencies\n - Add support for test task scheduling and execution workflows\n - Create test task reporting and metrics collection\n\n5. CLI Integration:\n - Add 'create-test-task' command with options for test type and target functionality\n - Implement test task status tracking (pending, running, passed, failed)\n - Add test task execution commands with result reporting\n - Support for bulk test task operations",
"testStrategy": "Verify the test task generation system by:\n\n1. Template Validation:\n - Create test tasks using each template type and verify proper structure\n - Test template variable substitution with various input scenarios\n - Validate generated test tasks against the task schema\n\n2. Integration Testing:\n - Test creation of test tasks through CLI commands\n - Verify test task filtering and listing functionality\n - Test dependency resolution between test tasks and implementation tasks\n - Validate test task execution workflow end-to-end\n\n3. Validation Testing:\n - Test validation rules with valid and invalid test task structures\n - Verify error handling for malformed test tasks\n - Test schema compliance for all generated test tasks\n\n4. Automation Testing:\n - Test auto-generation of test tasks when implementation tasks are created\n - Verify dependency propagation from implementation to test tasks\n - Test bulk operations on multiple test tasks\n\n5. CLI Testing:\n - Test all new CLI commands with various parameter combinations\n - Verify proper error messages and help documentation\n - Test JSON output format for programmatic usage\n - Validate test task status tracking and reporting functionality",
"status": "pending",
"dependencies": [
1,
3,
22
],
"priority": "medium",
"subtasks": []
},
{
"id": 97,
"title": "Implement Test Task Execution and Reporting Framework",
"description": "Create a comprehensive framework for executing test tasks and generating detailed reports on test results, coverage, and performance metrics.",
"details": "Build a robust test execution and reporting system that integrates with the existing test task generation framework:\n\n1. Test Execution Engine:\n - Implement a test runner that can execute different types of test tasks (unit, integration, e2e)\n - Support parallel and sequential test execution modes\n - Handle test timeouts and resource management\n - Provide real-time progress updates during test execution\n - Support test filtering by tags, priority, or test type\n\n2. Test Result Collection:\n - Capture test outcomes (pass/fail/skip) with detailed error messages\n - Record execution times and performance metrics\n - Collect code coverage data when applicable\n - Track test dependencies and execution order\n - Store test artifacts (logs, screenshots, generated files)\n\n3. Reporting System:\n - Generate comprehensive HTML reports with interactive charts\n - Create JSON output for CI/CD integration\n - Implement console reporting with color-coded results\n - Support custom report templates and formats\n - Include trend analysis for test performance over time\n\n4. Integration Features:\n - Connect with existing task management system to update task statuses\n - Support CI/CD pipeline integration with exit codes\n - Implement webhook notifications for test completion\n - Provide API endpoints for external tool integration\n\n5. Configuration Management:\n - Allow customizable test execution settings\n - Support environment-specific test configurations\n - Implement test suite organization and grouping\n - Enable selective test execution based on criteria",
"testStrategy": "Verify the test execution framework through comprehensive validation:\n\n1. Unit Testing:\n - Test the test runner engine with mock test cases\n - Validate result collection accuracy with known outcomes\n - Test report generation with various data scenarios\n - Verify configuration parsing and validation\n\n2. Integration Testing:\n - Execute real test suites and verify accurate result capture\n - Test parallel execution with multiple test types\n - Validate report generation with actual test data\n - Test integration with task management system updates\n\n3. Performance Testing:\n - Measure execution overhead and resource usage\n - Test with large test suites to verify scalability\n - Validate timeout handling and resource cleanup\n - Test concurrent execution limits and stability\n\n4. End-to-End Validation:\n - Run complete test cycles from execution to reporting\n - Verify CI/CD integration with sample pipelines\n - Test webhook notifications and external integrations\n - Validate report accuracy against manual test execution\n\n5. Error Handling:\n - Test behavior with failing tests and system errors\n - Verify graceful handling of resource constraints\n - Test recovery from interrupted test executions\n - Validate error reporting and logging accuracy",
"status": "pending",
"dependencies": [
96,
22
],
"priority": "medium",
"subtasks": []
}
]
}

View File

@@ -120,12 +120,14 @@ describe("TaskMaster Init Configuration Tests", () => {
it("should store API key in .env file (NOT config)", () => {
// Create .env with API key
const envContent =
"TASKMASTER_API_KEY=test-api-key-123\nOTHER_VAR=value\n";
"TASKMASTER_SERVICE_ID=test-api-key-123\nOTHER_VAR=value\n";
fs.writeFileSync(envPath, envContent);
// Test that API key is in .env
const envFileContent = fs.readFileSync(envPath, "utf8");
expect(envFileContent).toContain("TASKMASTER_API_KEY=test-api-key-123");
expect(envFileContent).toContain(
"TASKMASTER_SERVICE_ID=test-api-key-123"
);
// Test that API key is NOT in config
const config = {
@@ -145,7 +147,7 @@ describe("TaskMaster Init Configuration Tests", () => {
describe("Telemetry configuration", () => {
it("should get API key from .env file", async () => {
// Create .env with API key
const envContent = "TASKMASTER_API_KEY=env-api-key-456\n";
const envContent = "TASKMASTER_SERVICE_ID=env-api-key-456\n";
fs.writeFileSync(envPath, envContent);
// Test reading API key from .env
@@ -153,7 +155,7 @@ describe("TaskMaster Init Configuration Tests", () => {
"../../scripts/modules/utils.js"
);
const apiKey = resolveEnvVariable(
"TASKMASTER_API_KEY",
"TASKMASTER_SERVICE_ID",
null,
testProjectDir
);
@@ -163,13 +165,13 @@ describe("TaskMaster Init Configuration Tests", () => {
it("should prioritize environment variables", async () => {
// Clean up any existing env var first
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
// Set environment variable
process.env.TASKMASTER_API_KEY = "process-env-key";
process.env.TASKMASTER_SERVICE_ID = "process-env-key";
// Also create .env file
const envContent = "TASKMASTER_API_KEY=file-env-key\n";
const envContent = "TASKMASTER_SERVICE_ID=file-env-key\n";
fs.writeFileSync(envPath, envContent);
const { resolveEnvVariable } = await import(
@@ -177,13 +179,13 @@ describe("TaskMaster Init Configuration Tests", () => {
);
// Test with explicit projectRoot to avoid caching issues
const apiKey = resolveEnvVariable("TASKMASTER_API_KEY");
const apiKey = resolveEnvVariable("TASKMASTER_SERVICE_ID");
// Should prioritize process.env over .env file
expect(apiKey).toBe("process-env-key");
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
});
});

View File

@@ -65,7 +65,7 @@ describe("Telemetry Submission Service", () => {
});
// Mock environment variables for telemetry config
process.env.TASKMASTER_API_KEY = "test-api-key";
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
// Mock successful response
@@ -108,7 +108,7 @@ describe("Telemetry Submission Service", () => {
expect(sentData.fullOutput).toEqual({ debug: "should-be-sent" });
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
@@ -120,7 +120,7 @@ describe("Telemetry Submission Service", () => {
});
// Mock environment variables
process.env.TASKMASTER_API_KEY = "test-api-key";
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
// Mock 3 network failures then final HTTP error
@@ -144,7 +144,7 @@ describe("Telemetry Submission Service", () => {
expect(global.fetch).toHaveBeenCalledTimes(3);
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
}, 10000);
@@ -156,7 +156,7 @@ describe("Telemetry Submission Service", () => {
});
// Mock environment variables
process.env.TASKMASTER_API_KEY = "test-api-key";
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
global.fetch.mockRejectedValue(new Error("Network failure"));
@@ -176,7 +176,7 @@ describe("Telemetry Submission Service", () => {
expect(global.fetch).toHaveBeenCalledTimes(3); // All retries attempted
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
}, 10000);
@@ -220,7 +220,7 @@ describe("Telemetry Submission Service", () => {
});
// Mock environment variables so config is valid
process.env.TASKMASTER_API_KEY = "test-api-key";
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
const invalidTelemetryData = {
@@ -235,7 +235,7 @@ describe("Telemetry Submission Service", () => {
expect(global.fetch).not.toHaveBeenCalled();
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
@@ -247,7 +247,7 @@ describe("Telemetry Submission Service", () => {
});
// Mock environment variables with invalid API key
process.env.TASKMASTER_API_KEY = "invalid-key";
process.env.TASKMASTER_SERVICE_ID = "invalid-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
global.fetch.mockResolvedValueOnce({
@@ -272,7 +272,7 @@ describe("Telemetry Submission Service", () => {
expect(global.fetch).toHaveBeenCalledTimes(1); // No retries for auth errors
// Clean up
delete process.env.TASKMASTER_API_KEY;
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
});