diff --git a/.vibe-config.json b/.vibe-config.json new file mode 100644 index 0000000..ff344ec --- /dev/null +++ b/.vibe-config.json @@ -0,0 +1,24 @@ +{ + "version": "1.0.0", + "setupDate": "2025-08-14T18:25:58.699Z", + "unified": { + "enabled": true, + "projectRoot": "/tmp/cli-files-test", + "autoDetection": "true" + }, + "directories": { + "output": "./VibeCoderOutput", + "codeMap": "/tmp/cli-files-test", + "taskManager": "/tmp/cli-files-test" + }, + "security": { + "mode": "strict" + }, + "models": { + "gemini": "google/gemini-2.5-flash-preview-05-20", + "perplexity": "perplexity/sonar" + }, + "api": { + "baseUrl": "https://openrouter.ai/api/v1" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bc788..8bc855d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.5] - 2025-08-14 + +### Fixed +- **Critical CLI Onboarding Loop Bug** + - CLI now correctly detects user's project directory using `process.cwd()` + - Resolved persistent first-run detection issues that caused repeated onboarding + - Configuration files now save to user's working directory instead of package directory + +### Added +- **Context-Aware Configuration System** + - Enhanced `TransportContext` pattern for CLI vs Server differentiation + - Dual-location configuration saving (user directory + package fallback) for reliability + - Intelligent path resolution based on transport type + - Enhanced error handling for configuration persistence + +- **Improved CLI Auto-Detection** + - CLI automatically detects project root from current working directory + - Enhanced `OpenRouterConfigManager` with context-aware path resolution + - Updated `SetupWizard` with transport context support + - Users can now run `vibe` from any directory in their project + +### Changed +- **Enhanced Setup Wizard** + - SetupWizard constructor now accepts optional `TransportContext` + - Factory function `createSetupWizard()` for context-aware initialization + - Improved file path resolution methods for different transport types + +- **Developer Experience** + - Comprehensive test coverage for CLI onboarding flows + - Enhanced error messages and debugging information + - Better validation of configuration file locations + ## [0.2.4] - 2025-08-14 ### Added diff --git a/README.md b/README.md index 10a78ec..c9a1ec9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,37 @@ Vibe Coder is an MCP (Model Context Protocol) server designed to supercharge your AI assistant (like Cursor, Cline AI, or Claude Desktop) with powerful tools for software development. It helps with research, planning, generating requirements, creating starter projects, and more! +## šŸ†• What's New in Version 0.2.5 + +### šŸ”§ Critical CLI Bug Fixes & Enhancements +- **āœ… Fixed CLI Onboarding Loop Bug** + - CLI now correctly detects user's project directory using `process.cwd()` + - Resolves persistent first-run detection issues + - Configuration files now save to user's working directory instead of package directory + +- **šŸŽÆ Context-Aware Configuration System** + - Enhanced `TransportContext` pattern for CLI vs Server differentiation + - Dual-location configuration saving (user directory + package fallback) + - Intelligent path resolution based on transport type + - Maintains backward compatibility for all existing setups + +- **šŸ“ Auto-Detection Improvements** + - CLI automatically detects project root from current working directory + - Enhanced `OpenRouterConfigManager` with context-aware path resolution + - Updated `SetupWizard` with transport context support + - Users can now run `vibe` from any directory in their project + +- **šŸ’¾ Enhanced File Management** + - CLI saves all configuration files (`.env`, `llm_config.json`, `.vibe-config.json`) to user directory + - Improved error handling for configuration persistence + - Better validation of configuration file locations + +### Developer Experience Improvements +- Comprehensive test coverage for CLI onboarding flows +- Enhanced error messages and debugging information +- Improved setup wizard user experience +- Better documentation of CLI behavior and configuration + ## šŸ†• What's New in Version 0.2.3 ### Major Features diff --git a/RELEASE_NOTES_v0.2.4.md b/RELEASE_NOTES_v0.2.4.md deleted file mode 100644 index bdc5edb..0000000 --- a/RELEASE_NOTES_v0.2.4.md +++ /dev/null @@ -1,177 +0,0 @@ -# Release Notes - v0.2.4 - -## šŸŽ‰ Highlights - -This release introduces **Unified Project Root Configuration**, significantly simplifying the onboarding experience for both CLI and MCP users. The update brings intelligent auto-detection capabilities, enhanced documentation, and improved developer experience across all tools. - -## šŸš€ Major Features - -### Unified Project Root Configuration -- **Single Configuration Point**: Replaced multiple tool-specific environment variables with a unified `VIBE_PROJECT_ROOT` variable -- **Auto-Detection for CLI Users**: Automatic project root detection when using the `vibe` command - zero configuration needed! -- **Transport Context Awareness**: Different behavior based on transport type (CLI vs MCP client) -- **Backward Compatibility**: All legacy environment variables continue to work as fallbacks - -### Enhanced Setup Wizard -- **Interactive Configuration**: Step-by-step setup process with intelligent defaults -- **Auto-Detection Options**: Toggle automatic project root detection -- **Comprehensive Validation**: Ensures all configurations are valid before saving -- **Improved User Experience**: Clear prompts and helpful descriptions for all settings - -## šŸ”§ Improvements - -### Documentation Enhancements -- **Comprehensive README Updates**: - - Added quick start guides for different user types - - Included detailed configuration examples - - Updated tool descriptions with latest features - - Added troubleshooting section - -- **Tool-Specific Documentation**: - - Enhanced README files for code-map-generator, context-curator, and vibe-task-manager - - Added unified configuration examples - - Clarified security boundaries and permissions - -### Configuration Management -- **Environment Variables**: - - Simplified `.env.example` with clear sections and descriptions - - Added `.env.template` for setup wizard - - Removed redundant configuration options - - Added support for `VIBE_USE_PROJECT_ROOT_AUTO_DETECTION` - -- **MCP Configuration**: - - Updated `example_claude_desktop_config.json` with unified variables - - Improved configuration validation - - Enhanced error messages for misconfiguration - -### Security and DRY Compliance -- **UnifiedSecurityConfigManager Enhancement**: - - Added transport context support - - Implemented directory resolution priority chain - - Maintained backward compatibility - - Followed DRY principles by enhancing existing services - -### Developer Experience -- **CLI Improvements**: - - Fixed CLI initialization sequence - - Added proper service initialization order - - Improved error handling and logging - - Enhanced version display functionality - -- **Logging Enhancements**: - - Added structured logging with context - - Improved error messages - - Added debug logging for configuration resolution - - Enhanced transport-specific logging - -## šŸ› Bug Fixes - -- Fixed CLI configuration persistence issues -- Resolved version display problems in CLI -- Fixed initialization sequence for singleton services -- Corrected directory resolution in various transport contexts -- Fixed security boundary validation in unified configuration - -## šŸ“¦ Dependencies - -- All dependencies remain stable -- No breaking changes in external APIs -- Maintained compatibility with all MCP clients - -## šŸ”„ Migration Guide - -### For MCP Users (Claude Desktop, Cline, etc.) - -#### Option 1: Simplified Configuration (Recommended) -```json -{ - "mcpServers": { - "vibe-coder-mcp": { - "command": "npx", - "args": ["vibe-coder-mcp"], - "env": { - "VIBE_PROJECT_ROOT": "/path/to/your/project", - "OPENROUTER_API_KEY": "your-api-key" - } - } - } -} -``` - -#### Option 2: Legacy Configuration (Still Supported) -Your existing configuration with multiple environment variables will continue to work. - -### For CLI Users - -#### New Simplified Usage -```bash -# Navigate to your project directory -cd /path/to/your/project - -# Run with auto-detection (no configuration needed!) -npx vibe-coder-mcp --interactive - -# Or use the shorthand -vibe -``` - -#### First-Time Setup -```bash -# Run the setup wizard -npx vibe-coder-mcp --setup - -# Follow the prompts to configure: -# - Enable auto-detection (recommended) -# - Set your OpenRouter API key -# - Configure other optional settings -``` - -## šŸ“ Changelog Summary - -### Added -- Unified project root configuration system -- Auto-detection for CLI users -- Transport context awareness -- Enhanced setup wizard -- Comprehensive documentation updates -- Improved logging and error messages - -### Changed -- Simplified environment variable structure -- Enhanced UnifiedSecurityConfigManager -- Improved CLI initialization sequence -- Updated all tool READMEs -- Refined configuration templates - -### Removed -- Redundant `package-security-fix.sh` script -- Outdated configuration examples -- Unnecessary complexity in directory resolution - -### Fixed -- CLI configuration persistence -- Version display issues -- Service initialization order -- Directory resolution edge cases -- Security boundary validation - -## šŸŽÆ What's Next - -- Further CLI enhancements -- Additional tool integrations -- Performance optimizations -- Extended documentation and tutorials - -## šŸ’” Breaking Changes - -None - This release maintains full backward compatibility. - -## šŸ™ Acknowledgments - -Thanks to all contributors and users who provided feedback to improve the onboarding experience! - ---- - -**Installation**: `npm install -g vibe-coder-mcp@0.2.4` -**NPX Usage**: `npx vibe-coder-mcp@0.2.4` -**Repository**: [GitHub - Vibe-Coder-MCP](https://github.com/freshtechbro/vibe-coder-mcp) \ No newline at end of file diff --git a/package.json b/package.json index c149506..ed6072f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibe-coder-mcp", - "version": "0.2.4", + "version": "0.2.5", "description": "Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.", "main": "build/index.js", "type": "module", diff --git a/setup.bat b/setup.bat index 90c7c2a..dcbfb1a 100644 --- a/setup.bat +++ b/setup.bat @@ -1,5 +1,5 @@ @echo off -REM Setup script for Vibe Coder MCP Server (Production Ready v2.3) +REM Setup script for Vibe Coder MCP Server (Production Ready v2.5) setlocal enabledelayedexpansion REM Color codes for Windows (using PowerShell for colored output) @@ -9,7 +9,7 @@ set "YELLOW=[33m" set "BLUE=[34m" set "NC=[0m" -echo Setting up Vibe Coder MCP Server v2.3... +echo Setting up Vibe Coder MCP Server v2.5... echo ================================================== echo Production-ready MCP server with complete agent integration echo Multi-transport support • Real-time notifications • Dynamic port allocation diff --git a/setup.sh b/setup.sh index c093282..ad06dea 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Setup script for Vibe Coder MCP Server (Production Ready v2.3) +# Setup script for Vibe Coder MCP Server (Production Ready v2.5) set -e # Exit immediately if a command exits with a non-zero status. # Color codes for better output @@ -26,7 +26,7 @@ print_info() { echo -e "${BLUE}ℹ${NC} $1" } -echo "Setting up Vibe Coder MCP Server v2.3..." +echo "Setting up Vibe Coder MCP Server v2.5..." echo "==================================================" echo "Production-ready MCP server with complete agent integration" echo "Multi-transport support • Real-time notifications • Dynamic port allocation" @@ -382,7 +382,7 @@ fi echo "" print_status "Setup completed successfully!" echo "==================================================" -echo "Vibe Coder MCP Server v2.3 (Production Ready) is now set up with complete agent integration:" +echo "Vibe Coder MCP Server v2.5 (Production Ready) is now set up with complete agent integration:" echo "" echo "šŸ“‹ PLANNING & DOCUMENTATION TOOLS:" echo " - Research Manager (research-manager) - AI-powered research with Perplexity Sonar" diff --git a/src/setup-wizard.ts b/src/setup-wizard.ts index 2352e3a..faafe7d 100644 --- a/src/setup-wizard.ts +++ b/src/setup-wizard.ts @@ -16,6 +16,7 @@ import logger from './logger.js'; import { OpenRouterConfigManager } from './utils/openrouter-config-manager.js'; import { UserConfigManager } from './utils/user-config-manager.js'; import { ConfigValidator } from './utils/config-validator.js'; +import { TransportContext } from './index-with-setup.js'; // Get project root directory const __filename = fileURLToPath(import.meta.url); @@ -103,62 +104,117 @@ export class SetupWizard { private userConfigManager: UserConfigManager; private configValidator: ConfigValidator; private isInteractive: boolean; + private transportContext?: TransportContext; - constructor() { - this.envPath = path.join(projectRoot, '.env'); - this.configPath = path.join(projectRoot, '.vibe-config.json'); + constructor(transportContext?: TransportContext) { + this.transportContext = transportContext; + this.envPath = this.resolveEnvPath(); + this.configPath = this.resolveConfigPath(); this.userConfigManager = UserConfigManager.getInstance(); this.configValidator = ConfigValidator.getInstance(); this.isInteractive = process.stdin.isTTY && !process.env.CI; } /** - * Check if this is the first run using multiple indicators + * Resolve .env file path based on transport context + * For CLI: Save to user's working directory + * For Server: Save to package directory (existing behavior) + */ + private resolveEnvPath(): string { + if (this.transportContext?.transportType === 'cli' && this.transportContext.workingDirectory) { + return path.join(this.transportContext.workingDirectory, '.env'); + } + return path.join(projectRoot, '.env'); + } + + /** + * Resolve config file path based on transport context + * For CLI: Save to user's working directory + * For Server: Save to package directory (existing behavior) + */ + private resolveConfigPath(): string { + if (this.transportContext?.transportType === 'cli' && this.transportContext.workingDirectory) { + return path.join(this.transportContext.workingDirectory, '.vibe-config.json'); + } + return path.join(projectRoot, '.vibe-config.json'); + } + + /** + * Check if this is the first run using context-aware detection + * For CLI: Checks user's working directory for configuration + * For Server: Checks package directory for configuration (existing behavior) */ async isFirstRun(): Promise { - // First, check if .env file exists and load it if not already loaded - // This ensures we don't miss existing configuration - if (await fs.pathExists(this.envPath) && !process.env.OPENROUTER_API_KEY) { - try { - // Load environment variables from .env file - const dotenv = await import('dotenv'); - dotenv.config({ path: this.envPath }); - logger.debug('Loaded .env file in isFirstRun check'); - } catch (error) { - logger.warn({ err: error }, 'Failed to load .env in isFirstRun check'); + try { + // For CLI transport, use the OpenRouterConfigManager's context-aware method + if (this.transportContext?.transportType === 'cli') { + const configManager = OpenRouterConfigManager.getInstance(); + const hasValidConfig = await configManager.hasValidConfigInUserProject(this.transportContext); + + logger.info({ + transportType: 'cli', + workingDirectory: this.transportContext.workingDirectory, + hasValidConfig, + isFirstRun: !hasValidConfig + }, 'CLI first-run detection completed'); + + return !hasValidConfig; } - } - - // Check multiple conditions for robust detection - const checks = [ - // 1. Check for API key in environment (after loading .env if it exists) - !process.env.OPENROUTER_API_KEY, + + // For server transports, use existing logic with package directory + await this.loadAndCheckPackageConfig(); - // 2. Check for .env file in project - !await fs.pathExists(this.envPath), + const checks = [ + // 1. Check for API key in environment + !process.env.OPENROUTER_API_KEY, + + // 2. Check for .env file in package directory + !await fs.pathExists(this.envPath), + + // 3. Check for llm_config.json in package directory + !await fs.pathExists(path.join(projectRoot, 'llm_config.json')), + + // 4. Check for user config directory + !await fs.pathExists(this.userConfigManager.getUserConfigDir()) + ]; - // 3. Check for llm_config.json - !await fs.pathExists(path.join(projectRoot, 'llm_config.json')), + // For server: ALL conditions must be satisfied (not first run) + const isFirstRun = checks.some(check => check); - // 4. Check for user config directory - !await fs.pathExists(this.userConfigManager.getUserConfigDir()) - ]; - - // If any check fails, consider it first run - const isFirstRun = checks.some(check => check); - - if (isFirstRun) { logger.info({ + transportType: this.transportContext?.transportType || 'server', checks: { hasApiKey: !checks[0], hasEnvFile: !checks[1], hasLlmConfig: !checks[2], hasUserConfig: !checks[3] - } - }, 'First run detected'); + }, + isFirstRun + }, 'Server first-run detection completed'); + + return isFirstRun; + + } catch (error) { + logger.error({ err: error, context: this.transportContext }, 'Error during first-run detection'); + // Default to first run if detection fails to ensure setup runs + return true; + } + } + + /** + * Load and check package configuration for server transports + */ + private async loadAndCheckPackageConfig(): Promise { + // Load .env from package directory if it exists and API key not already loaded + if (await fs.pathExists(this.envPath) && !process.env.OPENROUTER_API_KEY) { + try { + const dotenv = await import('dotenv'); + dotenv.config({ path: this.envPath }); + logger.debug({ envPath: this.envPath }, 'Loaded .env file for server first-run check'); + } catch (error) { + logger.warn({ err: error, envPath: this.envPath }, 'Failed to load .env file for server first-run check'); + } } - - return isFirstRun; } /** @@ -434,6 +490,81 @@ export class SetupWizard { await fs.writeJson(this.configPath, configData, { spaces: 2 }); } + /** + * Create .env file at specific path (helper for dual-location saving) + */ + private async createEnvFileAtPath(config: SetupConfig, envPath: string): Promise { + let envContent = '# Vibe Coder MCP Configuration\n'; + envContent += '# Generated by setup wizard\n\n'; + + // Required configuration + envContent += '# Required: Your OpenRouter API key\n'; + envContent += `OPENROUTER_API_KEY="${config.OPENROUTER_API_KEY}"\n\n`; + + // Unified Configuration (if selected) + if (config.useUnifiedConfig && config.VIBE_PROJECT_ROOT) { + envContent += '# Unified Project Root Configuration (Recommended)\n'; + envContent += `VIBE_PROJECT_ROOT="${config.VIBE_PROJECT_ROOT}"\n`; + if (config.VIBE_USE_PROJECT_ROOT_AUTO_DETECTION) { + envContent += `VIBE_USE_PROJECT_ROOT_AUTO_DETECTION="${config.VIBE_USE_PROJECT_ROOT_AUTO_DETECTION}"\n`; + } + envContent += '\n'; + } + + // Directory Configuration + envContent += '# Directory Configuration\n'; + envContent += `VIBE_CODER_OUTPUT_DIR="${config.VIBE_CODER_OUTPUT_DIR}"\n`; + + // Legacy configuration (only if unified not used) + if (!config.useUnifiedConfig || config.configureDirs) { + envContent += '\n# Legacy Directory Configuration (Fallbacks)\n'; + envContent += `CODE_MAP_ALLOWED_DIR="${config.CODE_MAP_ALLOWED_DIR}"\n`; + envContent += `VIBE_TASK_MANAGER_READ_DIR="${config.VIBE_TASK_MANAGER_READ_DIR}"\n`; + envContent += `VIBE_TASK_MANAGER_SECURITY_MODE="${config.VIBE_TASK_MANAGER_SECURITY_MODE}"\n`; + } + envContent += '\n'; + + // Advanced configuration + envContent += '# Advanced Configuration\n'; + envContent += `OPENROUTER_BASE_URL="${config.OPENROUTER_BASE_URL}"\n`; + envContent += `GEMINI_MODEL="${config.GEMINI_MODEL}"\n`; + envContent += `PERPLEXITY_MODEL="${config.PERPLEXITY_MODEL}"\n`; + + await fs.writeFile(envPath, envContent, 'utf-8'); + } + + /** + * Save configuration JSON at specific path (helper for dual-location saving) + */ + private async saveConfigJsonAtPath(config: SetupConfig, configPath: string): Promise { + const configData = { + version: '1.0.0', + setupDate: new Date().toISOString(), + unified: { + enabled: config.useUnifiedConfig || false, + projectRoot: config.VIBE_PROJECT_ROOT, + autoDetection: config.VIBE_USE_PROJECT_ROOT_AUTO_DETECTION + }, + directories: { + output: config.VIBE_CODER_OUTPUT_DIR, + codeMap: config.CODE_MAP_ALLOWED_DIR, + taskManager: config.VIBE_TASK_MANAGER_READ_DIR + }, + security: { + mode: config.VIBE_TASK_MANAGER_SECURITY_MODE + }, + models: { + gemini: config.GEMINI_MODEL, + perplexity: config.PERPLEXITY_MODEL + }, + api: { + baseUrl: config.OPENROUTER_BASE_URL + } + }; + + await fs.writeJson(configPath, configData, { spaces: 2 }); + } + /** * Test API key with visual feedback */ @@ -546,38 +677,140 @@ Or run interactively with a TTY terminal. } /** - * Save configuration with UserConfigManager integration + * Save configuration with context-aware dual-location strategy + * For CLI: Save to user's working directory + package directory as fallback + * For Server: Save to package directory + user config directory */ private async saveEnhancedConfiguration(config: SetupConfig): Promise { - // Ensure user config directory exists - await this.userConfigManager.ensureUserConfigDir(); - - // Save to multiple locations for compatibility - const locations = [ - // 1. User config directory (primary) - { - dir: path.join(this.userConfigManager.getUserConfigDir(), 'configs'), - priority: 1 - }, - // 2. Project directory (backward compatibility) - { - dir: projectRoot, - priority: 2 + const savedLocations: string[] = []; + const errors: Array<{ location: string; error: unknown }> = []; + + try { + // Always ensure user config directory exists + await this.userConfigManager.ensureUserConfigDir(); + + if (this.transportContext?.transportType === 'cli' && this.transportContext.workingDirectory) { + // CLI: Save to user's working directory (primary) + package directory (fallback) + await this.saveCLIConfiguration(config, savedLocations, errors); + } else { + // Server: Save to package directory + user config directory + await this.saveServerConfiguration(config, savedLocations, errors); } - ]; + + // Report results + if (savedLocations.length > 0) { + logger.info({ + savedLocations, + transportType: this.transportContext?.transportType || 'server', + errorCount: errors.length + }, 'Configuration saved successfully'); + } + + if (errors.length > 0) { + logger.warn({ + errors: errors.map(e => ({ location: e.location, error: String(e.error) })), + successfulLocations: savedLocations + }, 'Some configuration saves failed, but at least one succeeded'); + } + + if (savedLocations.length === 0) { + throw new Error('Failed to save configuration to any location'); + } + + } catch (error) { + logger.error({ err: error, context: this.transportContext }, 'Configuration saving failed completely'); + throw error; + } + } + + /** + * Save configuration for CLI transport + */ + private async saveCLIConfiguration( + config: SetupConfig, + savedLocations: string[], + errors: Array<{ location: string; error: unknown }> + ): Promise { + const userProjectDir = this.transportContext!.workingDirectory!; - for (const location of locations) { - try { - // Create .env file - await this.createEnvFile(config); - - // Copy template files if they don't exist - await this.userConfigManager.copyDefaultConfigs(); - - } catch (error) { - logger.warn({ err: error, location }, 'Failed to save config to location'); + // Priority 1: User's working directory (primary location for CLI) + try { + await this.saveConfigToDirectory(config, userProjectDir, 'user-project'); + savedLocations.push(userProjectDir); + logger.info({ userProjectDir }, 'Configuration saved to user project directory'); + } catch (error) { + errors.push({ location: userProjectDir, error }); + logger.warn({ err: error, userProjectDir }, 'Failed to save config to user project directory'); + } + + // Priority 2: Package directory (fallback for CLI) + try { + await this.saveConfigToDirectory(config, projectRoot, 'package-fallback'); + savedLocations.push(projectRoot); + logger.info({ packageRoot: projectRoot }, 'Configuration saved to package directory as fallback'); + } catch (error) { + errors.push({ location: projectRoot, error }); + logger.warn({ err: error, packageRoot: projectRoot }, 'Failed to save config to package directory'); + } + } + + /** + * Save configuration for server transport + */ + private async saveServerConfiguration( + config: SetupConfig, + savedLocations: string[], + errors: Array<{ location: string; error: unknown }> + ): Promise { + // Priority 1: Package directory (primary for server) + try { + await this.saveConfigToDirectory(config, projectRoot, 'package-primary'); + savedLocations.push(projectRoot); + logger.info({ packageRoot: projectRoot }, 'Configuration saved to package directory'); + } catch (error) { + errors.push({ location: projectRoot, error }); + logger.warn({ err: error, packageRoot: projectRoot }, 'Failed to save config to package directory'); + } + + // Priority 2: User config directory (secondary for server) + try { + const userConfigDir = path.join(this.userConfigManager.getUserConfigDir(), 'configs'); + await this.saveConfigToDirectory(config, userConfigDir, 'user-config'); + savedLocations.push(userConfigDir); + logger.info({ userConfigDir }, 'Configuration saved to user config directory'); + } catch (error) { + const userConfigDir = path.join(this.userConfigManager.getUserConfigDir(), 'configs'); + errors.push({ location: userConfigDir, error }); + logger.warn({ err: error, userConfigDir }, 'Failed to save config to user config directory'); + } + } + + /** + * Save configuration files to a specific directory + */ + private async saveConfigToDirectory(config: SetupConfig, targetDir: string, context: string): Promise { + // Ensure target directory exists + await fs.ensureDir(targetDir); + + // Save .env file + const envPath = path.join(targetDir, '.env'); + await this.createEnvFileAtPath(config, envPath); + + // Save .vibe-config.json file + const configPath = path.join(targetDir, '.vibe-config.json'); + await this.saveConfigJsonAtPath(config, configPath); + + // Copy llm_config.json if it doesn't exist in target directory + const llmConfigPath = path.join(targetDir, 'llm_config.json'); + if (!await fs.pathExists(llmConfigPath)) { + const templateLLMConfigPath = path.join(projectRoot, 'llm_config.json'); + if (await fs.pathExists(templateLLMConfigPath)) { + await fs.copy(templateLLMConfigPath, llmConfigPath); + logger.debug({ source: templateLLMConfigPath, dest: llmConfigPath }, 'Copied llm_config.json to target directory'); } } + + logger.debug({ targetDir, context }, 'Configuration files saved to directory'); } /** @@ -661,7 +894,12 @@ Or run interactively with a TTY terminal. } } -// Export singleton instance +// Export factory function for context-aware setup wizard instances +export function createSetupWizard(transportContext?: TransportContext): SetupWizard { + return new SetupWizard(transportContext); +} + +// Export singleton instance (backward compatibility for server usage) export const setupWizard = new SetupWizard(); // If run directly, execute the wizard diff --git a/src/unified-cli.ts b/src/unified-cli.ts index e4c0731..f9ab1e7 100644 --- a/src/unified-cli.ts +++ b/src/unified-cli.ts @@ -15,7 +15,8 @@ import chalk from 'chalk'; import boxen from 'boxen'; import ora from 'ora'; import dotenv from 'dotenv'; -import { setupWizard } from './setup-wizard.js'; +import { createSetupWizard } from './setup-wizard.js'; +import { TransportContext } from './index-with-setup.js'; import logger from './logger.js'; // Get directory paths @@ -129,16 +130,28 @@ async function main() { try { const mode = detectMode(); + // Create CLI transport context for context-aware configuration + const cliTransportContext: TransportContext = { + sessionId: `cli-${Date.now()}`, + transportType: 'cli', + timestamp: Date.now(), + workingDirectory: process.cwd(), // User's current working directory + mcpClientConfig: undefined // Will be loaded by context-aware config manager + }; + // Load environment variables BEFORE checking first run // This ensures .env file is loaded so isFirstRun() can properly detect existing config dotenv.config({ path: envPath }); + // Create context-aware setup wizard instance + const contextAwareSetupWizard = createSetupWizard(cliTransportContext); + // Always check for first run (except in help mode) - if (mode !== 'help' && await setupWizard.isFirstRun()) { + if (mode !== 'help' && await contextAwareSetupWizard.isFirstRun()) { console.log(chalk.cyan.bold('\nšŸš€ Welcome to Vibe!\n')); console.log(chalk.yellow('First-time setup required...\n')); - const success = await setupWizard.run(); + const success = await contextAwareSetupWizard.run(); if (!success) { console.log(chalk.red('\nāŒ Setup cancelled.')); console.log(chalk.gray('Run ') + chalk.cyan('vibe --setup') + chalk.gray(' to configure later.')); @@ -204,7 +217,19 @@ async function main() { async function runSetup() { console.log(chalk.cyan.bold('\nšŸ”§ Vibe Configuration\n')); - const success = await setupWizard.run(); + // Create CLI transport context for setup reconfiguration + const cliTransportContext: TransportContext = { + sessionId: `cli-setup-${Date.now()}`, + transportType: 'cli', + timestamp: Date.now(), + workingDirectory: process.cwd(), // User's current working directory + mcpClientConfig: undefined // Will be loaded by context-aware config manager + }; + + // Create context-aware setup wizard for reconfiguration + const contextAwareSetupWizard = createSetupWizard(cliTransportContext); + + const success = await contextAwareSetupWizard.run(); if (success) { console.log(chalk.green('\nāœ… Configuration updated successfully!')); console.log(chalk.gray('Run ') + chalk.cyan('vibe') + chalk.gray(' to start the server.')); diff --git a/src/utils/openrouter-config-manager.ts b/src/utils/openrouter-config-manager.ts index 642c6ea..d89b585 100644 --- a/src/utils/openrouter-config-manager.ts +++ b/src/utils/openrouter-config-manager.ts @@ -17,6 +17,7 @@ import path from 'path'; import { readFile } from 'fs/promises'; import { OpenRouterConfig } from '../types/workflow.js'; import { getProjectRoot } from '../tools/code-map-generator/utils/pathUtils.enhanced.js'; +import { TransportContext } from '../index-with-setup.js'; import { ConfigurationError, ValidationError, @@ -843,6 +844,269 @@ export class OpenRouterConfigManager { static resetInstance(): void { OpenRouterConfigManager.instance = null; } + + // ======================================================================== + // CONTEXT-AWARE CONFIGURATION METHODS + // ======================================================================== + + /** + * Resolve configuration file paths based on transport context + * Implements precedence-based path resolution for CLI vs server usage + * + * For CLI: Check user project first, then package directory + * For Server: Use package directory (existing behavior) + * + * @param context Transport context containing working directory info + * @returns Object with ordered paths for different config files + */ + private resolveConfigPaths(context?: TransportContext): { + llmConfigPaths: string[]; + envPaths: string[]; + packageRoot: string; + userProjectRoot?: string; + } { + const packageRoot = getProjectRoot(); + const result = { + llmConfigPaths: [] as string[], + envPaths: [] as string[], + packageRoot, + userProjectRoot: undefined as string | undefined + }; + + // For CLI transport with working directory: Check user project first + if (context?.transportType === 'cli' && context.workingDirectory) { + result.userProjectRoot = context.workingDirectory; + + // Priority 1: User project directory + result.llmConfigPaths.push(path.join(context.workingDirectory, 'llm_config.json')); + result.envPaths.push(path.join(context.workingDirectory, '.env')); + } + + // Priority 2 (or only): Package directory (existing behavior) + result.llmConfigPaths.push(path.join(packageRoot, 'llm_config.json')); + result.envPaths.push(path.join(packageRoot, '.env')); + + logger.debug({ + transportType: context?.transportType, + userProjectRoot: result.userProjectRoot, + packageRoot: result.packageRoot, + llmConfigPaths: result.llmConfigPaths, + envPaths: result.envPaths + }, 'Resolved configuration paths with context'); + + return result; + } + + /** + * Load LLM configuration with precedence-based path resolution + * Tries paths in order until a valid config is found + * + * @param llmConfigPaths Array of paths to try in order + * @returns Loaded LLM configuration or null if not found + */ + private async loadLLMConfigWithPrecedence(llmConfigPaths: string[]): Promise { + for (const configPath of llmConfigPaths) { + try { + const configContent = await readFile(configPath, 'utf-8'); + const parsedConfig = JSON.parse(configContent) as LLMConfig; + + // Validate the config has the required structure + if (parsedConfig && typeof parsedConfig === 'object' && parsedConfig.llm_mapping) { + logger.info({ configPath, mappingCount: Object.keys(parsedConfig.llm_mapping).length }, + 'Successfully loaded LLM configuration from path'); + return parsedConfig; + } else { + logger.warn({ configPath }, 'LLM config file exists but has invalid structure'); + } + } catch (error) { + // Don't log error for first path attempts - this is expected for CLI + const isLastPath = configPath === llmConfigPaths[llmConfigPaths.length - 1]; + if (isLastPath) { + logger.warn({ err: error, configPath }, 'Failed to load LLM configuration from path'); + } else { + logger.debug({ configPath }, 'LLM config not found at path, trying next'); + } + } + } + + return null; + } + + /** + * Load environment variables with precedence-based path resolution + * Tries .env files in order until one is found + * + * @param envPaths Array of .env file paths to try in order + * @returns Object with loaded environment variables or null if none found + */ + private async loadEnvWithPrecedence(envPaths: string[]): Promise | null> { + for (const envPath of envPaths) { + try { + const dotenv = await import('dotenv'); + const result = dotenv.config({ path: envPath }); + + if (!result.error && result.parsed) { + logger.info({ envPath, varsLoaded: Object.keys(result.parsed).length }, + 'Successfully loaded environment variables from path'); + return result.parsed; + } else if (result.error) { + logger.debug({ envPath }, '.env file not found at path, trying next'); + } + } catch (error) { + const isLastPath = envPath === envPaths[envPaths.length - 1]; + if (isLastPath) { + logger.warn({ err: error, envPath }, 'Failed to load .env file from path'); + } else { + logger.debug({ envPath }, '.env file not accessible at path, trying next'); + } + } + } + + return null; + } + + /** + * Initialize configuration with context awareness + * Uses precedence-based loading for CLI transport + * Maintains existing behavior for server transports + * + * @param context Optional transport context for path resolution + */ + async initializeWithContext(context?: TransportContext): Promise { + if (this.initializationPromise && !context) { + // If no context provided and already initializing, use existing promise + return this.initializationPromise; + } + + // For context-aware initialization, create new promise + const initPromise = this.performContextAwareInitialization(context); + + if (!context) { + // Only cache the promise if no context (backward compatibility) + this.initializationPromise = initPromise; + } + + return initPromise; + } + + /** + * Perform the actual context-aware initialization + */ + private async performContextAwareInitialization(context?: TransportContext): Promise { + try { + logger.debug({ + hasContext: Boolean(context), + transportType: context?.transportType, + workingDirectory: context?.workingDirectory + }, 'Starting context-aware configuration initialization'); + + // Resolve configuration paths based on context + const paths = this.resolveConfigPaths(context); + + // Load LLM configuration with precedence + this.llmConfig = await this.loadLLMConfigWithPrecedence(paths.llmConfigPaths); + + if (!this.llmConfig) { + logger.warn({ + triedPaths: paths.llmConfigPaths + }, 'No valid LLM configuration found in any path. Using minimal config.'); + + // Create minimal config to prevent failures + this.llmConfig = { + llm_mapping: { + default_generation: process.env.DEFAULT_LLM_MODEL || 'google/gemini-2.5-flash-lite' + } + }; + } + + // Load environment variables with precedence (for validation) + const envVars = await this.loadEnvWithPrecedence(paths.envPaths); + + // Extract configuration from environment variables + const apiKey = process.env.OPENROUTER_API_KEY || envVars?.OPENROUTER_API_KEY || ''; + const baseUrl = process.env.OPENROUTER_BASE_URL || envVars?.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1'; + const geminiModel = process.env.GEMINI_MODEL || envVars?.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20'; + const perplexityModel = process.env.PERPLEXITY_MODEL || envVars?.PERPLEXITY_MODEL || 'perplexity/sonar'; + + // Create comprehensive config + this.config = { + apiKey, + baseUrl, + geminiModel, + perplexityModel, + llm_mapping: this.llmConfig.llm_mapping, + env: envVars || {} + }; + + logger.info({ + apiKeyPresent: Boolean(apiKey), + baseUrl, + llmMappingCount: Object.keys(this.llmConfig.llm_mapping).length, + contextUsed: Boolean(context), + userProjectRoot: paths.userProjectRoot, + packageRoot: paths.packageRoot + }, 'Context-aware configuration initialization completed'); + + } catch (error) { + logger.error({ err: error, context }, 'Context-aware configuration initialization failed'); + const errorContext = createErrorContext('OpenRouterConfigManager', 'performContextAwareInitialization') + .metadata({ + hasContext: Boolean(context), + transportType: context?.transportType, + workingDirectory: context?.workingDirectory + }) + .build(); + + throw new ConfigurationError('Failed to initialize configuration with context', errorContext); + } + } + + /** + * Check if configuration exists in user project (for CLI first-run detection) + * This is used by the setup wizard to determine if user has already configured their project + * + * @param context Transport context containing user's working directory + * @returns Promise True if valid configuration exists in user project + */ + async hasValidConfigInUserProject(context: TransportContext): Promise { + if (context.transportType !== 'cli' || !context.workingDirectory) { + return false; + } + + try { + const userEnvPath = path.join(context.workingDirectory, '.env'); + const userLLMConfigPath = path.join(context.workingDirectory, 'llm_config.json'); + + // Check if .env exists and has required keys + const dotenv = await import('dotenv'); + const envResult = dotenv.config({ path: userEnvPath }); + const hasApiKey = Boolean(envResult.parsed?.OPENROUTER_API_KEY); + + // Check if llm_config.json exists and is valid + let hasValidLLMConfig = false; + try { + const configContent = await readFile(userLLMConfigPath, 'utf-8'); + const parsedConfig = JSON.parse(configContent) as LLMConfig; + hasValidLLMConfig = Boolean(parsedConfig?.llm_mapping); + } catch { + hasValidLLMConfig = false; + } + + logger.debug({ + userProjectRoot: context.workingDirectory, + hasApiKey, + hasValidLLMConfig, + userEnvPath, + userLLMConfigPath + }, 'Checked configuration in user project'); + + return hasApiKey && hasValidLLMConfig; + + } catch (error) { + logger.debug({ err: error, context }, 'Error checking user project configuration'); + return false; + } + } } /**