From 2a07d366be6aeadd14db7de98d1c975d90284200 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Thu, 1 May 2025 13:45:11 -0400 Subject: [PATCH] fix(update): pass projectRoot through update command flow Modified ai-services-unified.js, update.js tool, and update-tasks.js direct function to correctly pass projectRoot. This enables the .env file API key fallback mechanism for the update command when running via MCP, ensuring consistent key resolution with the CLI context. --- .../src/core/direct-functions/update-tasks.js | 12 +++-- mcp-server/src/tools/update.js | 3 +- scripts/modules/ai-services-unified.js | 52 +++++++++++++------ scripts/modules/task-manager/update-tasks.js | 6 ++- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/mcp-server/src/core/direct-functions/update-tasks.js b/mcp-server/src/core/direct-functions/update-tasks.js index 533c9be4..846734a2 100644 --- a/mcp-server/src/core/direct-functions/update-tasks.js +++ b/mcp-server/src/core/direct-functions/update-tasks.js @@ -20,7 +20,7 @@ import { createLogWrapper } from '../../tools/utils.js'; */ export async function updateTasksDirect(args, log, context = {}) { const { session } = context; // Extract session - const { tasksJsonPath, from, prompt, research } = args; + const { tasksJsonPath, from, prompt, research, projectRoot } = args; // Create the standard logger wrapper const logWrapper = { @@ -85,21 +85,23 @@ export async function updateTasksDirect(args, log, context = {}) { const useResearch = research === true; // --- End Input Validation --- - log.info(`Updating tasks from ID ${fromId}. Research: ${useResearch}`); + log.info( + `Updating tasks from ID ${fromId}. Research: ${useResearch}. Project Root: ${projectRoot}` + ); enableSilentMode(); // Enable silent mode try { // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); - // Execute core updateTasks function, passing session context + // Execute core updateTasks function, passing session context AND projectRoot await updateTasks( tasksJsonPath, fromId, prompt, useResearch, - // Pass context with logger wrapper and session - { mcpLog, session }, + // Pass context with logger wrapper, session, AND projectRoot + { mcpLog, session, projectRoot }, 'json' // Explicitly request JSON format for MCP ); diff --git a/mcp-server/src/tools/update.js b/mcp-server/src/tools/update.js index 4fe719c1..e6e0d5e1 100644 --- a/mcp-server/src/tools/update.js +++ b/mcp-server/src/tools/update.js @@ -70,7 +70,8 @@ export function registerUpdateTool(server) { tasksJsonPath: tasksJsonPath, from: args.from, prompt: args.prompt, - research: args.research + research: args.research, + projectRoot: rootFolder }, log, { session } diff --git a/scripts/modules/ai-services-unified.js b/scripts/modules/ai-services-unified.js index 6c2c78dd..fead4ad3 100644 --- a/scripts/modules/ai-services-unified.js +++ b/scripts/modules/ai-services-unified.js @@ -16,7 +16,7 @@ import { getFallbackModelId, getParametersForRole } from './config-manager.js'; -import { log, resolveEnvVariable } from './utils.js'; +import { log, resolveEnvVariable, findProjectRoot } from './utils.js'; import * as anthropic from '../../src/ai-providers/anthropic.js'; import * as perplexity from '../../src/ai-providers/perplexity.js'; @@ -136,10 +136,11 @@ function _extractErrorMessage(error) { * Internal helper to resolve the API key for a given provider. * @param {string} providerName - The name of the provider (lowercase). * @param {object|null} session - Optional MCP session object. + * @param {string|null} projectRoot - Optional project root path for .env fallback. * @returns {string|null} The API key or null if not found/needed. * @throws {Error} If a required API key is missing. */ -function _resolveApiKey(providerName, session) { +function _resolveApiKey(providerName, session, projectRoot = null) { const keyMap = { openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', @@ -163,10 +164,10 @@ function _resolveApiKey(providerName, session) { ); } - const apiKey = resolveEnvVariable(envVarName, session); + const apiKey = resolveEnvVariable(envVarName, session, projectRoot); if (!apiKey) { throw new Error( - `Required API key ${envVarName} for provider '${providerName}' is not set in environment or session.` + `Required API key ${envVarName} for provider '${providerName}' is not set in environment, session, or .env file.` ); } return apiKey; @@ -241,27 +242,35 @@ async function _attemptProviderCallWithRetries( * Base logic for unified service functions. * @param {string} serviceType - Type of service ('generateText', 'streamText', 'generateObject'). * @param {object} params - Original parameters passed to the service function. + * @param {string} [params.projectRoot] - Optional project root path. * @returns {Promise} Result from the underlying provider call. */ async function _unifiedServiceRunner(serviceType, params) { const { role: initialRole, session, + projectRoot, systemPrompt, prompt, schema, objectName, ...restApiParams } = params; - log('info', `${serviceType}Service called`, { role: initialRole }); + log('info', `${serviceType}Service called`, { + role: initialRole, + projectRoot + }); + + // Determine the effective project root (passed in or detected) + const effectiveProjectRoot = projectRoot || findProjectRoot(); let sequence; if (initialRole === 'main') { sequence = ['main', 'fallback', 'research']; - } else if (initialRole === 'fallback') { - sequence = ['fallback', 'research']; } else if (initialRole === 'research') { - sequence = ['research', 'fallback']; + sequence = ['research', 'fallback', 'main']; + } else if (initialRole === 'fallback') { + sequence = ['fallback', 'main', 'research']; } else { log( 'warn', @@ -281,16 +290,16 @@ async function _unifiedServiceRunner(serviceType, params) { log('info', `New AI service call with role: ${currentRole}`); // 1. Get Config: Provider, Model, Parameters for the current role - // Call individual getters based on the current role + // Pass effectiveProjectRoot to config getters if (currentRole === 'main') { - providerName = getMainProvider(); - modelId = getMainModelId(); + providerName = getMainProvider(effectiveProjectRoot); + modelId = getMainModelId(effectiveProjectRoot); } else if (currentRole === 'research') { - providerName = getResearchProvider(); - modelId = getResearchModelId(); + providerName = getResearchProvider(effectiveProjectRoot); + modelId = getResearchModelId(effectiveProjectRoot); } else if (currentRole === 'fallback') { - providerName = getFallbackProvider(); - modelId = getFallbackModelId(); + providerName = getFallbackProvider(effectiveProjectRoot); + modelId = getFallbackModelId(effectiveProjectRoot); } else { log( 'error', @@ -314,7 +323,8 @@ async function _unifiedServiceRunner(serviceType, params) { continue; } - roleParams = getParametersForRole(currentRole); + // Pass effectiveProjectRoot to getParametersForRole + roleParams = getParametersForRole(currentRole, effectiveProjectRoot); // 2. Get Provider Function Set providerFnSet = PROVIDER_FUNCTIONS[providerName?.toLowerCase()]; @@ -345,7 +355,12 @@ async function _unifiedServiceRunner(serviceType, params) { } // 3. Resolve API Key (will throw if required and missing) - apiKey = _resolveApiKey(providerName?.toLowerCase(), session); + // Pass effectiveProjectRoot to _resolveApiKey + apiKey = _resolveApiKey( + providerName?.toLowerCase(), + session, + effectiveProjectRoot + ); // 4. Construct Messages Array const messages = []; @@ -443,6 +458,7 @@ async function _unifiedServiceRunner(serviceType, params) { * @param {object} params - Parameters for the service call. * @param {string} params.role - The initial client role ('main', 'research', 'fallback'). * @param {object} [params.session=null] - Optional MCP session object. + * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback. * @param {string} params.prompt - The prompt for the AI. * @param {string} [params.systemPrompt] - Optional system prompt. * // Other specific generateText params can be included here. @@ -459,6 +475,7 @@ async function generateTextService(params) { * @param {object} params - Parameters for the service call. * @param {string} params.role - The initial client role ('main', 'research', 'fallback'). * @param {object} [params.session=null] - Optional MCP session object. + * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback. * @param {string} params.prompt - The prompt for the AI. * @param {string} [params.systemPrompt] - Optional system prompt. * // Other specific streamText params can be included here. @@ -475,6 +492,7 @@ async function streamTextService(params) { * @param {object} params - Parameters for the service call. * @param {string} params.role - The initial client role ('main', 'research', 'fallback'). * @param {object} [params.session=null] - Optional MCP session object. + * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback. * @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object. * @param {string} params.prompt - The prompt for the AI. * @param {string} [params.systemPrompt] - Optional system prompt. diff --git a/scripts/modules/task-manager/update-tasks.js b/scripts/modules/task-manager/update-tasks.js index d47f256b..acd3f0e1 100644 --- a/scripts/modules/task-manager/update-tasks.js +++ b/scripts/modules/task-manager/update-tasks.js @@ -21,6 +21,7 @@ import { import { getDebugFlag } from '../config-manager.js'; import generateTaskFiles from './generate-task-files.js'; import { generateTextService } from '../ai-services-unified.js'; +import { getModelConfiguration } from './models.js'; // Zod schema for validating the structure of tasks AFTER parsing const updatedTaskSchema = z @@ -173,7 +174,7 @@ async function updateTasks( context = {}, outputFormat = 'text' // Default to text for CLI ) { - const { session, mcpLog } = context; + const { session, mcpLog, projectRoot } = context; // Use mcpLog if available, otherwise use the imported consoleLog function const logFn = mcpLog || consoleLog; // Flag to easily check which logger type we have @@ -312,7 +313,8 @@ The changes described in the prompt should be applied to ALL tasks in the list.` prompt: userPrompt, systemPrompt: systemPrompt, role, - session + session, + projectRoot }); if (isMCP) logFn.info('Successfully received text response'); else