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:
@@ -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
134
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
573
scripts/init.js
573
scripts/init.js
@@ -22,10 +22,16 @@ import chalk from "chalk";
|
||||
import figlet from "figlet";
|
||||
import boxen from "boxen";
|
||||
import gradient from "gradient-string";
|
||||
import inquirer from "inquirer";
|
||||
import open from "open";
|
||||
import express from "express";
|
||||
import { isSilentMode } from "./modules/utils.js";
|
||||
import { convertAllCursorRulesToRooRules } from "./modules/rule-transformer.js";
|
||||
import { execSync } from "child_process";
|
||||
import { registerUserWithGateway } from "./modules/telemetry-submission.js";
|
||||
import {
|
||||
initializeUser,
|
||||
registerUserWithGateway,
|
||||
} from "./modules/user-management.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -379,46 +385,51 @@ async function initializeProject(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// STEP 1: Create/find userId first (MCP/non-interactive mode)
|
||||
let userId = null;
|
||||
let gatewayRegistration = null;
|
||||
// NON-INTERACTIVE MODE - Use proper auth/init flow
|
||||
let userSetupResult;
|
||||
|
||||
try {
|
||||
// Try to get existing userId from config if it exists
|
||||
// Check if existing config has userId
|
||||
const existingConfigPath = path.join(process.cwd(), ".taskmasterconfig");
|
||||
let existingUserId = null;
|
||||
|
||||
if (fs.existsSync(existingConfigPath)) {
|
||||
const existingConfig = JSON.parse(
|
||||
fs.readFileSync(existingConfigPath, "utf8")
|
||||
);
|
||||
userId = existingConfig.account?.userId;
|
||||
const existingUserEmail = existingConfig.account?.userEmail;
|
||||
existingUserId = existingConfig.account?.userId;
|
||||
}
|
||||
|
||||
// Pass existing data to gateway for validation/lookup
|
||||
gatewayRegistration = await registerUserWithGateway(
|
||||
existingUserEmail || tempEmail,
|
||||
userId
|
||||
);
|
||||
|
||||
if (gatewayRegistration.success) {
|
||||
userId = gatewayRegistration.userId;
|
||||
} else {
|
||||
// Generate fallback userId if gateway unavailable
|
||||
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
if (existingUserId) {
|
||||
// Validate existing userId through auth/init
|
||||
userSetupResult = await registerUserWithGateway(null, process.cwd());
|
||||
if (!userSetupResult.success) {
|
||||
throw new Error(
|
||||
`Failed to validate existing user: ${userSetupResult.error}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new user through auth/init
|
||||
userSetupResult = await initializeUser(process.cwd());
|
||||
if (!userSetupResult.success) {
|
||||
throw new Error(
|
||||
`Failed to initialize user: ${userSetupResult.error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Generate fallback userId on any error
|
||||
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
log("error", `User initialization failed: ${error.message}`);
|
||||
throw error; // Don't fall back to random userId!
|
||||
}
|
||||
|
||||
// For non-interactive mode, default to BYOK mode with proper userId
|
||||
// Create project structure with properly authenticated userId
|
||||
createProjectStructure(
|
||||
addAliases,
|
||||
dryRun,
|
||||
gatewayRegistration,
|
||||
"byok",
|
||||
userSetupResult, // Pass the full auth result
|
||||
"byok", // or determine from result
|
||||
null,
|
||||
userId
|
||||
userSetupResult.userId || null
|
||||
);
|
||||
} else {
|
||||
// Interactive logic - NEW FLOW STARTS HERE
|
||||
@@ -444,127 +455,124 @@ async function initializeProject(options = {}) {
|
||||
)
|
||||
);
|
||||
|
||||
// Generate or retrieve userId from gateway
|
||||
let userId = null;
|
||||
let gatewayRegistration = null;
|
||||
// INTERACTIVE MODE - Also use proper auth/init flow
|
||||
// STEP 1: Proper user setup
|
||||
let userSetupResult;
|
||||
|
||||
try {
|
||||
// Try to get existing userId from config if it exists
|
||||
// Same logic as non-interactive mode
|
||||
const existingConfigPath = path.join(
|
||||
process.cwd(),
|
||||
".taskmasterconfig"
|
||||
);
|
||||
let existingUserId = null;
|
||||
|
||||
if (fs.existsSync(existingConfigPath)) {
|
||||
const existingConfig = JSON.parse(
|
||||
fs.readFileSync(existingConfigPath, "utf8")
|
||||
);
|
||||
userId = existingConfig.account?.userId;
|
||||
const existingUserEmail = existingConfig.account?.userEmail;
|
||||
existingUserId = existingConfig.account?.userId;
|
||||
}
|
||||
|
||||
// Pass existing data to gateway for validation/lookup
|
||||
gatewayRegistration = await registerUserWithGateway(
|
||||
existingUserEmail || tempEmail,
|
||||
userId
|
||||
);
|
||||
|
||||
if (gatewayRegistration.success) {
|
||||
userId = gatewayRegistration.userId;
|
||||
} else {
|
||||
// Generate fallback userId if gateway unavailable
|
||||
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
if (existingUserId) {
|
||||
userSetupResult = await registerUserWithGateway(null, process.cwd());
|
||||
if (!userSetupResult.success) {
|
||||
throw new Error(
|
||||
`Failed to validate existing user: ${userSetupResult.error}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
userSetupResult = await initializeUser(process.cwd());
|
||||
if (!userSetupResult.success) {
|
||||
throw new Error(
|
||||
`Failed to initialize user: ${userSetupResult.error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Generate fallback userId on any error
|
||||
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
log("error", `User initialization failed: ${error.message}`);
|
||||
// Don't fall back to random userId - exit or prompt user
|
||||
throw error;
|
||||
}
|
||||
|
||||
// STEP 2: Choose AI access method (MAIN DECISION)
|
||||
// STEP 2: Choose AI access method using inquirer
|
||||
const modeResponse = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "accessMode",
|
||||
message: "Choose Your AI Access Method:",
|
||||
choices: [
|
||||
{
|
||||
name: "🔑 BYOK - Bring Your Own API Keys (You manage API keys & billing)",
|
||||
value: "byok",
|
||||
},
|
||||
{
|
||||
name: "🎯 Hosted API Gateway - All models, no keys needed (Recommended)",
|
||||
value: "hosted",
|
||||
},
|
||||
],
|
||||
default: "hosted",
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedMode = modeResponse.accessMode;
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold("Choose Your AI Access Method") +
|
||||
"\n\n" +
|
||||
chalk.cyan.bold("(1) BYOK - Bring Your Own API Keys") +
|
||||
"\n" +
|
||||
chalk.white(
|
||||
" → You manage API keys & billing with AI providers"
|
||||
) +
|
||||
"\n" +
|
||||
chalk.white(" → Pay provider directly based on token usage") +
|
||||
"\n" +
|
||||
chalk.white(
|
||||
" → Requires setup with each provider individually"
|
||||
) +
|
||||
"\n\n" +
|
||||
chalk.green.bold("(2) Hosted API Gateway") +
|
||||
" " +
|
||||
chalk.yellow.bold("(Recommended)") +
|
||||
"\n" +
|
||||
chalk.white(" → Use any model, zero API keys needed") +
|
||||
"\n" +
|
||||
chalk.white(" → Flat, credit-based pricing with no surprises") +
|
||||
"\n" +
|
||||
chalk.white(" → Support the development of Taskmaster"),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderStyle: "round",
|
||||
borderColor: "cyan",
|
||||
title: "🎯 AI Access Setup",
|
||||
titleAlignment: "center",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
let choice;
|
||||
while (true) {
|
||||
choice = await promptQuestion(
|
||||
rl,
|
||||
chalk.cyan.bold("Your choice (1 or 2): ")
|
||||
);
|
||||
|
||||
if (choice === "1" || choice.toLowerCase() === "byok") {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.blue.bold("🔑 BYOK Mode Selected") +
|
||||
selectedMode === "byok"
|
||||
? chalk.blue.bold("🔑 BYOK Mode Selected") +
|
||||
"\n\n" +
|
||||
chalk.white("You'll manage your own API keys and billing.") +
|
||||
"\n" +
|
||||
chalk.white("After setup, add your API keys to ") +
|
||||
chalk.cyan(".env") +
|
||||
chalk.white(" file."),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderStyle: "round",
|
||||
borderColor: "blue",
|
||||
}
|
||||
)
|
||||
);
|
||||
return "byok";
|
||||
} else if (choice === "2" || choice.toLowerCase() === "hosted") {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold("🎯 Hosted API Gateway Selected") +
|
||||
chalk.white(" file.")
|
||||
: chalk.green.bold("🎯 Hosted API Gateway Selected") +
|
||||
"\n\n" +
|
||||
chalk.white(
|
||||
"All AI models available instantly - no API keys needed!"
|
||||
) +
|
||||
"\n" +
|
||||
chalk.dim("Let's set up your subscription plan..."),
|
||||
{
|
||||
padding: 0.5,
|
||||
margin: { top: 0.5, bottom: 0.5 },
|
||||
borderStyle: "round",
|
||||
borderColor: "green",
|
||||
}
|
||||
)
|
||||
);
|
||||
return "hosted";
|
||||
} else {
|
||||
console.log(chalk.red("Please enter 1 or 2"));
|
||||
}
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderStyle: "round",
|
||||
borderColor: selectedMode === "byok" ? "blue" : "green",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// STEP 3: If hosted mode, handle subscription plan with Stripe simulation
|
||||
let selectedPlan = null;
|
||||
if (selectedMode === "hosted") {
|
||||
selectedPlan = await handleHostedSubscription();
|
||||
}
|
||||
|
||||
// STEP 4: Continue with aliases (this fixes the hanging issue)
|
||||
const aliasResponse = await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "addAliases",
|
||||
message: "Add shell aliases (tm, taskmaster) for easier access?",
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const addAliases = aliasResponse.addAliases;
|
||||
|
||||
const dryRun = options.dryRun || false;
|
||||
|
||||
// STEP 5: Show overview and continue with project creation
|
||||
rl.close();
|
||||
createProjectStructure(
|
||||
addAliases,
|
||||
dryRun,
|
||||
userSetupResult,
|
||||
selectedMode,
|
||||
selectedPlan,
|
||||
userSetupResult.userId
|
||||
);
|
||||
} catch (error) {
|
||||
rl.close();
|
||||
log("error", `Error during initialization process: ${error.message}`);
|
||||
@@ -1137,251 +1145,132 @@ function setupMCPConfiguration(targetDir) {
|
||||
log("info", "MCP server will use the installed task-master-ai package");
|
||||
}
|
||||
|
||||
// Function to let user choose between BYOK and Hosted API Gateway
|
||||
async function selectAccessMode() {
|
||||
// Function to handle hosted subscription with browser pattern
|
||||
async function handleHostedSubscription() {
|
||||
const planResponse = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "plan",
|
||||
message: "Select Your Monthly AI Credit Pack:",
|
||||
choices: [
|
||||
{
|
||||
name: "50 credits - $5/mo [$0.10 per credit] - Perfect for personal projects",
|
||||
value: { name: "Starter", credits: 50, price: "$5/mo", value: 1 },
|
||||
},
|
||||
{
|
||||
name: "120 credits - $10/mo [$0.083 per credit] - Popular choice",
|
||||
value: { name: "Popular", credits: 120, price: "$10/mo", value: 2 },
|
||||
},
|
||||
{
|
||||
name: "250 credits - $20/mo [$0.08 per credit] - Great value",
|
||||
value: { name: "Pro", credits: 250, price: "$20/mo", value: 3 },
|
||||
},
|
||||
{
|
||||
name: "550 credits - $40/mo [$0.073 per credit] - Best value",
|
||||
value: {
|
||||
name: "Enterprise",
|
||||
credits: 550,
|
||||
price: "$40/mo",
|
||||
value: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
default: 1, // Popular plan
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedPlan = planResponse.plan;
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.cyan.bold("🚀 Choose Your AI Access Method") +
|
||||
chalk.green.bold(`✅ Selected: ${selectedPlan.name} Plan`) +
|
||||
"\n\n" +
|
||||
chalk.white("TaskMaster supports two ways to access AI models:") +
|
||||
chalk.white(
|
||||
`${selectedPlan.credits} credits/month for ${selectedPlan.price}`
|
||||
) +
|
||||
"\n\n" +
|
||||
chalk.yellow.bold("(1) BYOK - Bring Your Own API Keys") +
|
||||
chalk.yellow("🔄 Opening browser for Stripe checkout...") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Use your existing provider accounts") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Pay providers directly") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Full control over billing & usage") +
|
||||
"\n" +
|
||||
chalk.dim(" → Best for: Teams with existing AI accounts") +
|
||||
"\n\n" +
|
||||
chalk.green.bold("(2) Hosted API Gateway") +
|
||||
chalk.yellow(" (Recommended)") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ No API keys required") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Access all supported models instantly") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Simple credit-based billing") +
|
||||
"\n" +
|
||||
chalk.white(" ✓ Better rates through volume pricing") +
|
||||
"\n" +
|
||||
chalk.dim(" → Best for: Getting started quickly"),
|
||||
chalk.dim("Complete your subscription setup in the browser."),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderStyle: "round",
|
||||
borderColor: "cyan",
|
||||
title: "🎯 AI Access Configuration",
|
||||
titleAlignment: "center",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
let choice;
|
||||
while (true) {
|
||||
choice = await promptQuestion(
|
||||
rl,
|
||||
chalk.cyan("Your choice") +
|
||||
chalk.gray(" (1 for BYOK, 2 for Hosted)") +
|
||||
": "
|
||||
);
|
||||
|
||||
if (choice === "1" || choice.toLowerCase() === "byok") {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.blue.bold("🔑 BYOK Mode Selected") +
|
||||
"\n\n" +
|
||||
chalk.white("You'll configure your own AI provider API keys.") +
|
||||
"\n" +
|
||||
chalk.dim("The setup will guide you through model configuration."),
|
||||
{
|
||||
padding: 0.5,
|
||||
margin: { top: 0.5, bottom: 0.5 },
|
||||
borderStyle: "round",
|
||||
borderColor: "blue",
|
||||
}
|
||||
)
|
||||
);
|
||||
return "byok";
|
||||
} else if (choice === "2" || choice.toLowerCase() === "hosted") {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold("🎯 Hosted API Gateway Selected") +
|
||||
"\n\n" +
|
||||
chalk.white(
|
||||
"All AI models available instantly - no API keys needed!"
|
||||
) +
|
||||
"\n" +
|
||||
chalk.dim("Let's set up your subscription plan..."),
|
||||
{
|
||||
padding: 0.5,
|
||||
margin: { top: 0.5, bottom: 0.5 },
|
||||
borderStyle: "round",
|
||||
borderColor: "green",
|
||||
}
|
||||
)
|
||||
);
|
||||
return "hosted";
|
||||
} else {
|
||||
console.log(chalk.red("Please enter 1 or 2"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to let user select a subscription plan
|
||||
async function selectSubscriptionPlan() {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.cyan.bold("💳 Select Your Monthly AI Credit Pack") +
|
||||
"\n\n" +
|
||||
chalk.white("Choose the plan that fits your usage:") +
|
||||
"\n\n" +
|
||||
chalk.white("(1) ") +
|
||||
chalk.yellow.bold("50 credits") +
|
||||
chalk.white(" - ") +
|
||||
chalk.green("$5/mo") +
|
||||
chalk.gray(" [$0.10 per credit]") +
|
||||
"\n" +
|
||||
chalk.dim(" → Perfect for: Personal projects, light usage") +
|
||||
"\n\n" +
|
||||
chalk.white("(2) ") +
|
||||
chalk.yellow.bold("120 credits") +
|
||||
chalk.white(" - ") +
|
||||
chalk.green("$10/mo") +
|
||||
chalk.gray(" [$0.083 per credit]") +
|
||||
chalk.cyan.bold(" ← Popular") +
|
||||
"\n" +
|
||||
chalk.dim(" → Perfect for: Active development, small teams") +
|
||||
"\n\n" +
|
||||
chalk.white("(3) ") +
|
||||
chalk.yellow.bold("250 credits") +
|
||||
chalk.white(" - ") +
|
||||
chalk.green("$20/mo") +
|
||||
chalk.gray(" [$0.08 per credit]") +
|
||||
chalk.blue.bold(" ← Great Value") +
|
||||
"\n" +
|
||||
chalk.dim(" → Perfect for: Professional development, medium teams") +
|
||||
"\n\n" +
|
||||
chalk.white("(4) ") +
|
||||
chalk.yellow.bold("550 credits") +
|
||||
chalk.white(" - ") +
|
||||
chalk.green("$40/mo") +
|
||||
chalk.gray(" [$0.073 per credit]") +
|
||||
chalk.magenta.bold(" ← Best Value") +
|
||||
"\n" +
|
||||
chalk.dim(" → Perfect for: Heavy usage, large teams, enterprises") +
|
||||
"\n\n" +
|
||||
chalk.blue("💡 ") +
|
||||
chalk.white("Credits roll over month-to-month. Cancel anytime."),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
margin: { top: 0.5, bottom: 0.5 },
|
||||
borderStyle: "round",
|
||||
borderColor: "green",
|
||||
title: "💳 Subscription Plans",
|
||||
titleAlignment: "center",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: "Starter",
|
||||
credits: 50,
|
||||
price: "$5/mo",
|
||||
perCredit: "$0.10",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: "Popular",
|
||||
credits: 120,
|
||||
price: "$10/mo",
|
||||
perCredit: "$0.083",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
credits: 250,
|
||||
price: "$20/mo",
|
||||
perCredit: "$0.08",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: "Enterprise",
|
||||
credits: 550,
|
||||
price: "$40/mo",
|
||||
perCredit: "$0.073",
|
||||
value: 4,
|
||||
},
|
||||
];
|
||||
// Stripe simulation with browser opening pattern (like Shopify CLI)
|
||||
await simulateStripeCheckout(selectedPlan);
|
||||
|
||||
let choice;
|
||||
while (true) {
|
||||
choice = await promptQuestion(
|
||||
rl,
|
||||
chalk.cyan("Your choice") + chalk.gray(" (1-4)") + ": "
|
||||
);
|
||||
|
||||
const planIndex = parseInt(choice) - 1;
|
||||
if (planIndex >= 0 && planIndex < plans.length) {
|
||||
const selectedPlan = plans[planIndex];
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold(`✅ Selected: ${selectedPlan.name} Plan`) +
|
||||
"\n\n" +
|
||||
chalk.white(
|
||||
`${selectedPlan.credits} credits/month for ${selectedPlan.price}`
|
||||
) +
|
||||
"\n" +
|
||||
chalk.gray(`(${selectedPlan.perCredit} per credit)`) +
|
||||
"\n\n" +
|
||||
chalk.yellow("🔄 Opening Stripe checkout...") +
|
||||
"\n" +
|
||||
chalk.dim("Complete your subscription setup in the browser."),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 0.5, bottom: 0.5 },
|
||||
borderStyle: "round",
|
||||
borderColor: "green",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: Integrate with actual Stripe checkout
|
||||
// For now, simulate the process
|
||||
console.log(chalk.yellow("\n⏳ Simulating Stripe checkout process..."));
|
||||
console.log(chalk.green("✅ Subscription setup complete! (Simulated)"));
|
||||
|
||||
return selectedPlan;
|
||||
} else {
|
||||
console.log(chalk.red("Please enter a number from 1 to 4"));
|
||||
}
|
||||
}
|
||||
return selectedPlan;
|
||||
}
|
||||
|
||||
// Function to create or retrieve user ID
|
||||
async function getOrCreateUserId() {
|
||||
// Try to find existing userId first
|
||||
const existingConfig = path.join(process.cwd(), ".taskmasterconfig");
|
||||
if (fs.existsSync(existingConfig)) {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(existingConfig, "utf8"));
|
||||
if (config.userId) {
|
||||
log("info", `Using existing user ID: ${config.userId}`);
|
||||
return config.userId;
|
||||
}
|
||||
} catch (error) {
|
||||
log("warn", "Could not read existing config, creating new user ID");
|
||||
}
|
||||
// Stripe checkout simulation with browser pattern
|
||||
async function simulateStripeCheckout(plan) {
|
||||
console.log(chalk.yellow("\n⏳ Starting Stripe checkout process..."));
|
||||
|
||||
// Start a simple HTTP server to handle the callback
|
||||
const app = express();
|
||||
let server;
|
||||
let checkoutComplete = false;
|
||||
|
||||
// For demo/testing, we'll use a simple success simulation
|
||||
const checkoutUrl = `https://example-stripe-simulation.com/checkout?plan=${plan.value}&return_url=http://localhost:3333/success`;
|
||||
|
||||
app.get("/success", (req, res) => {
|
||||
checkoutComplete = true;
|
||||
res.send(`
|
||||
<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
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,6 +25,7 @@ jest.unstable_mockModule(
|
||||
getAzureBaseURL: jest.fn(),
|
||||
getVertexProjectId: jest.fn(),
|
||||
getVertexLocation: jest.fn(),
|
||||
writeConfig: jest.fn(() => true),
|
||||
MODEL_MAP: {
|
||||
openai: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user