UNPKG

@sirmrmarty/n8n-nodes-gemini-cli

Version:

n8n node for Google Gemini CLI integration with AI-powered coding assistance

1,069 lines (1,054 loc) 71.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GeminiCli = void 0; const n8n_workflow_1 = require("n8n-workflow"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path = __importStar(require("path")); class GeminiCli { constructor() { this.description = { displayName: 'Gemini CLI', name: 'geminiCli', icon: 'file:logo.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["prompt"]}}', description: 'Use Google Gemini CLI to execute AI-powered coding tasks with multimodal capabilities', defaults: { name: 'Gemini CLI', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Approve Plan', value: 'approve_plan', description: 'Mark a plan as approved and ready for execution', action: 'Approve a plan for execution', }, { name: 'Continue', value: 'continue', description: 'Continue a previous conversation (requires prior query)', action: 'Continue a previous conversation requires prior query', }, { name: 'Edit Plan', value: 'edit_plan', description: 'Modify an existing execution plan', action: 'Edit an existing execution plan', }, { name: 'Execute Plan', value: 'execute_plan', description: 'Execute an approved plan', action: 'Execute an approved execution plan', }, { name: 'Generate Plan', value: 'generate_plan', description: 'Generate an execution plan without executing it', action: 'Generate an execution plan for the given task', }, { name: 'List Plans', value: 'list_plans', description: 'List all available execution plans', action: 'List all stored execution plans', }, { name: 'Query', value: 'query', description: 'Start a new conversation with Gemini CLI', action: 'Start a new conversation with gemini cli', }, ], default: 'query', }, { displayName: 'Prompt', name: 'prompt', type: 'string', typeOptions: { rows: 4, }, default: '', description: 'The prompt or instruction to send to Gemini CLI', required: true, placeholder: 'e.g., "Create a Python function to parse CSV files"', hint: 'Use expressions like {{$json.prompt}} to use data from previous nodes', displayOptions: { show: { operation: ['query', 'continue', 'generate_plan'], }, }, }, { displayName: 'Plan ID', name: 'planId', type: 'string', default: '', description: 'The ID of the plan to work with', required: true, placeholder: 'e.g., "plan_20240108_123456"', displayOptions: { show: { operation: ['edit_plan', 'approve_plan', 'execute_plan'], }, }, }, { displayName: 'Edit Instructions', name: 'editInstructions', type: 'string', typeOptions: { rows: 3, }, default: '', description: 'Instructions for how to modify the plan', placeholder: 'e.g., "Add error handling to step 2 and increase timeout for step 4"', displayOptions: { show: { operation: ['edit_plan'], }, }, }, { displayName: 'Model', name: 'model', type: 'options', options: [ { name: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro', description: 'Most capable model with 1M token context window', }, { name: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash', description: 'Fast and efficient model for quick responses', }, ], default: 'gemini-2.5-pro', description: 'Gemini model to use', }, { displayName: 'Max Turns', name: 'maxTurns', type: 'number', default: 10, description: 'Maximum number of conversation turns (back-and-forth exchanges) allowed', }, { displayName: 'Timeout', name: 'timeout', type: 'number', default: 300, description: 'Maximum time to wait for completion (in seconds) before aborting', }, { displayName: 'Project Path', name: 'projectPath', type: 'string', default: '', description: 'The directory path where Gemini CLI should run (e.g., /path/to/project). If empty, uses the current working directory.', placeholder: '/home/user/projects/my-app', hint: 'This sets the working directory for Gemini CLI, allowing it to access files and run commands in the specified project location', }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', noDataExpression: true, options: [ { name: 'Messages', value: 'messages', description: 'Returns the raw array of all messages exchanged', }, { name: 'Plan', value: 'plan', description: 'Returns the execution plan structure (for plan operations)', }, { name: 'Plan Status', value: 'plan_status', description: 'Returns plan execution progress and status', }, { name: 'Structured', value: 'structured', description: 'Returns a structured object with messages, summary, result, and metrics', }, { name: 'Text', value: 'text', description: 'Returns only the final result text', }, ], default: 'structured', description: 'Choose how to format the output data', }, { displayName: 'Additional Options', name: 'additionalOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'API Key', name: 'apiKey', type: 'string', typeOptions: { password: true, }, default: '', description: 'Gemini API key (if not set via GEMINI_API_KEY environment variable)', placeholder: 'Your Gemini API key', }, { displayName: 'Use Vertex AI', name: 'useVertexAI', type: 'boolean', default: false, description: 'Whether to use Vertex AI instead of Gemini API', }, { displayName: 'System Prompt', name: 'systemPrompt', type: 'string', typeOptions: { rows: 4, }, default: '', description: 'Additional context or instructions for Gemini CLI', placeholder: 'You are helping with a Python project. Focus on clean, readable code with proper error handling.', }, { displayName: 'Debug Mode', name: 'debug', type: 'boolean', default: false, description: 'Whether to enable debug logging', }, ], }, { displayName: 'Tools Configuration', name: 'toolsConfig', type: 'collection', placeholder: 'Configure Tools', default: {}, description: 'Configure built-in tools and external integrations', options: [ { displayName: 'Enable Built-in Tools', name: 'enabledTools', type: 'multiOptions', options: [ { name: 'File System (Read/Write)', value: 'filesystem', description: 'Enable reading and writing files in the project directory', }, { name: 'Shell Commands', value: 'shell', description: 'Allow execution of shell commands (use with caution)', }, { name: 'Web Fetch', value: 'web_fetch', description: 'Enable fetching content from URLs', }, { name: 'Web Search', value: 'web_search', description: 'Enable web search capabilities', }, ], default: ['web_fetch', 'web_search'], description: 'Select which built-in tools to enable for Gemini CLI', }, { displayName: 'Security Mode', name: 'securityMode', type: 'options', options: [ { name: 'Safe Mode (Confirmations Required)', value: 'safe', description: 'Require confirmation for destructive operations (recommended)', }, { name: 'Auto-Approve (YOLO Mode)', value: 'yolo', description: 'Automatically approve all tool operations (use with extreme caution)', }, { name: 'Sandbox Mode', value: 'sandbox', description: 'Run in sandbox environment with restricted access', }, ], default: 'safe', description: 'Choose security level for tool operations', }, { displayName: 'Enable Checkpointing', name: 'checkpointing', type: 'boolean', default: false, description: 'Whether to enable session checkpointing to save conversation state', }, ], }, { displayName: 'MCP Servers', name: 'mcpServers', type: 'fixedCollection', placeholder: 'Add MCP Server', default: { servers: [] }, description: 'Configure external MCP (Model Context Protocol) servers for extended functionality', typeOptions: { multipleValues: true, }, options: [ { displayName: 'Servers', name: 'servers', values: [ { displayName: 'Command', name: 'command', type: 'string', default: '', description: 'Command to execute the MCP server', placeholder: 'npx @modelcontextprotocol/server-github', displayOptions: { show: { connectionType: ['command'], }, }, }, { displayName: 'Command Arguments', name: 'args', type: 'string', default: '', description: 'Command arguments (space-separated)', placeholder: '--port 3000 --verbose', displayOptions: { show: { connectionType: ['command'], }, }, }, { displayName: 'Connection Type', name: 'connectionType', type: 'options', options: [ { name: 'Command (Stdio)', value: 'command', description: 'Connect via command execution with stdio transport', }, { name: 'HTTP URL', value: 'http', description: 'Connect via HTTP with Server-Sent Events', }, ], default: 'command', description: 'How to connect to the MCP server', }, { displayName: 'Environment Variables', name: 'env', type: 'string', default: '', description: 'Environment variables in KEY=VALUE format (one per line)', typeOptions: { rows: 3, }, placeholder: 'GITHUB_PERSONAL_ACCESS_TOKEN=your_token\nAPI_BASE_URL=https://api.example.com', }, { displayName: 'Exclude Tools', name: 'excludeTools', type: 'string', default: '', description: 'Comma-separated list of tools to exclude (blacklist, takes precedence)', placeholder: 'delete_file,format_disk', }, { displayName: 'HTTP URL', name: 'httpUrl', type: 'string', default: '', description: 'HTTP URL for the MCP server', placeholder: 'http://localhost:3000/sse', displayOptions: { show: { connectionType: ['http'], }, }, }, { displayName: 'Include Tools', name: 'includeTools', type: 'string', default: '', description: 'Comma-separated list of tools to include (whitelist)', placeholder: 'read_file,write_file,list_files', }, { displayName: 'Server Name', name: 'name', type: 'string', default: '', required: true, description: 'Unique name for this MCP server', placeholder: 'github-server', }, { displayName: 'Timeout (Ms)', name: 'timeout', type: 'number', default: 30000, description: 'Request timeout in milliseconds', }, { displayName: 'Trust Level', name: 'trust', type: 'options', options: [ { name: 'Require Confirmations', value: false, description: 'Require user confirmation for tool calls (recommended)', }, { name: 'Auto-Trust', value: true, description: 'Automatically trust and execute tool calls (use with caution)', }, ], default: false, description: 'Whether to automatically trust and execute this server\'s tool calls', }, { displayName: 'Working Directory', name: 'cwd', type: 'string', default: '', description: 'Working directory for the server command', placeholder: '/path/to/server', displayOptions: { show: { connectionType: ['command'], }, }, }, ], }, ], }, ], }; } static async checkGeminiCLI(debug = false) { try { // Check if gemini command is available const versionOutput = (0, child_process_1.execSync)('gemini --version', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], }).trim(); if (debug) { console.log(`[GeminiCli] Gemini CLI version: ${versionOutput}`); } // Try to check authentication by running a simple command try { (0, child_process_1.execSync)('gemini --help', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], }); if (debug) { console.log(`[GeminiCli] Gemini CLI available and accessible`); } return { isAvailable: true, isAuthenticated: true, version: versionOutput, }; } catch (authError) { if (debug) { console.warn(`[GeminiCli] Gemini CLI authentication check failed: ${authError instanceof Error ? authError.message : 'Unknown error'}`); } return { isAvailable: true, isAuthenticated: false, version: versionOutput, error: 'Gemini CLI is installed but may not be properly configured. Ensure API keys are set.', }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (debug) { console.error(`[GeminiCli] Gemini CLI not available: ${errorMessage}`); } return { isAvailable: false, isAuthenticated: false, error: 'Gemini CLI is not installed or not accessible. Install with: npm install -g @google/gemini-cli', }; } } static async createGeminiConfig(context, mcpServers, workingDirectory, debug = false) { const configDir = path.join(workingDirectory, '.gemini'); const settingsPath = path.join(configDir, 'settings.json'); if (debug) { console.log(`[GeminiCli] Creating config directory: ${configDir}`); } // Create .gemini directory try { await fs_1.promises.mkdir(configDir, { recursive: true }); } catch (error) { throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Failed to create config directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Generate settings.json content const settings = {}; if ((mcpServers === null || mcpServers === void 0 ? void 0 : mcpServers.servers) && mcpServers.servers.length > 0) { settings.mcpServers = {}; for (const server of mcpServers.servers) { if (!server.name || server.name.trim() === '') { continue; // Skip servers without names } const serverConfig = { timeout: server.timeout || 30000, trust: server.trust || false, }; // Handle connection type if (server.connectionType === 'command' && server.command) { serverConfig.command = server.command; // Parse arguments if (server.args && server.args.trim()) { serverConfig.args = server.args.trim().split(/\s+/); } if (server.cwd && server.cwd.trim()) { serverConfig.cwd = server.cwd.trim(); } } else if (server.connectionType === 'http' && server.httpUrl) { serverConfig.url = server.httpUrl; } // Parse environment variables if (server.env && server.env.trim()) { serverConfig.env = {}; const envLines = server.env.split('\n'); for (const line of envLines) { const trimmed = line.trim(); if (trimmed && trimmed.includes('=')) { const [key, ...valueParts] = trimmed.split('='); const value = valueParts.join('='); serverConfig.env[key.trim()] = value.trim(); } } } // Handle tool filtering if (server.includeTools && server.includeTools.trim()) { serverConfig.includeTools = server.includeTools .split(',') .map(t => t.trim()) .filter(t => t.length > 0); } if (server.excludeTools && server.excludeTools.trim()) { serverConfig.excludeTools = server.excludeTools .split(',') .map(t => t.trim()) .filter(t => t.length > 0); } settings.mcpServers[server.name.trim()] = serverConfig; } } // Write settings file try { await fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); if (debug) { console.log(`[GeminiCli] Created settings file: ${settingsPath}`); console.log(`[GeminiCli] Settings content:`, JSON.stringify(settings, null, 2)); } } catch (error) { throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Failed to write settings file: ${error instanceof Error ? error.message : 'Unknown error'}`); } return { configDir, settingsPath }; } static async ensurePlansDirectory(workingDirectory) { const plansDir = path.join(workingDirectory, '.gemini', 'plans'); try { await fs_1.promises.mkdir(plansDir, { recursive: true }); return plansDir; } catch (error) { throw new n8n_workflow_1.ApplicationError(`Failed to create plans directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static generatePlanId() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, -5); const randomSuffix = Math.random().toString(36).substring(2, 8); return `plan_${timestamp}_${randomSuffix}`; } static async savePlan(plan, workingDirectory) { const plansDir = await GeminiCli.ensurePlansDirectory(workingDirectory); const planPath = path.join(plansDir, `${plan.id}.json`); try { await fs_1.promises.writeFile(planPath, JSON.stringify(plan, null, 2), 'utf8'); } catch (error) { throw new n8n_workflow_1.ApplicationError(`Failed to save plan: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async loadPlan(planId, workingDirectory) { const plansDir = path.join(workingDirectory, '.gemini', 'plans'); const planPath = path.join(plansDir, `${planId}.json`); try { const planContent = await fs_1.promises.readFile(planPath, 'utf8'); return JSON.parse(planContent); } catch (error) { if (error.code === 'ENOENT') { return null; // Plan not found } throw new n8n_workflow_1.ApplicationError(`Failed to load plan: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async listPlans(workingDirectory) { const plansDir = path.join(workingDirectory, '.gemini', 'plans'); const plans = []; try { const files = await fs_1.promises.readdir(plansDir); const planFiles = files.filter(file => file.endsWith('.json')); for (const file of planFiles) { try { const planContent = await fs_1.promises.readFile(path.join(plansDir, file), 'utf8'); const plan = JSON.parse(planContent); plans.push(plan); } catch { // Skip invalid plan files continue; } } // Sort by creation date, newest first return plans.sort((a, b) => b.created_at - a.created_at); } catch (error) { if (error.code === 'ENOENT') { return []; // Plans directory doesn't exist yet } throw new n8n_workflow_1.ApplicationError(`Failed to list plans: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async cleanupGeminiConfig(configDir, debug = false) { try { if (debug) { console.log(`[GeminiCli] Cleaning up config directory: ${configDir}`); } // Remove the entire .gemini directory await fs_1.promises.rm(configDir, { recursive: true, force: true }); } catch (error) { if (debug) { console.warn(`[GeminiCli] Failed to cleanup config directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Don't throw error for cleanup failures } } static async validateProjectPath(projectPath, debug = false) { const result = { isValid: false, warnings: [], }; try { // Resolve and normalize the path const resolvedPath = path.resolve(projectPath); result.resolvedPath = resolvedPath; if (debug) { console.log(`[GeminiCli] Validating project path: ${projectPath} -> ${resolvedPath}`); } // Check if path exists try { const stats = await fs_1.promises.stat(resolvedPath); if (!stats.isDirectory()) { result.error = `Path exists but is not a directory: ${resolvedPath}`; return result; } } catch { result.error = `Directory does not exist: ${resolvedPath}`; return result; } // Check read/write permissions try { await fs_1.promises.access(resolvedPath, fs_1.promises.constants.R_OK | fs_1.promises.constants.W_OK); } catch { result.error = `Insufficient permissions for directory: ${resolvedPath}`; return result; } result.isValid = true; return result; } catch (err) { result.error = `Unexpected error validating project path: ${err instanceof Error ? err.message : 'Unknown error'}`; return result; } } static async generatePlanWithGemini(context, prompt, options) { const planId = GeminiCli.generatePlanId(); const timestamp = Date.now(); // Create a planning-specific system prompt const planningSystemPrompt = `You are an AI assistant that creates detailed execution plans. When given a task, you must respond with a structured execution plan in the following JSON format: { "title": "Brief title for the task", "description": "Detailed description of what needs to be accomplished", "steps": [ { "id": "step_1", "description": "Clear description of what this step does", "command": "optional: specific command to run", "files": ["optional: array of files this step will work with"], "estimated_duration": 300 } ] } Break down complex tasks into specific, actionable steps. Each step should be clear and executable. Do not execute anything - only create the plan. Include estimated duration in seconds for each step. Be specific about files that will be created, modified, or analyzed. ${options.systemPrompt ? `\n\nAdditional context: ${options.systemPrompt}` : ''}`; const planningPrompt = `Create an execution plan for the following task: ${prompt} Respond ONLY with the JSON structure described in the system prompt. Do not include any other text, explanations, or markdown formatting.`; try { // Call Gemini CLI to generate the plan const response = await GeminiCli.runGeminiCLI(context, planningPrompt, { ...options, systemPrompt: planningSystemPrompt, }); if (!response.success) { throw new n8n_workflow_1.ApplicationError(`Failed to generate plan: ${response.error || 'Unknown error'}`); } // Parse the response to extract plan structure let planData; try { // Try to extract JSON from the response const jsonMatch = response.result.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new n8n_workflow_1.ApplicationError('No JSON structure found in Gemini response'); } planData = JSON.parse(jsonMatch[0]); } catch (parseError) { throw new n8n_workflow_1.ApplicationError(`Failed to parse plan structure: ${parseError instanceof Error ? parseError.message : 'Invalid JSON'}`); } // Validate and create the execution plan if (!planData.title || !planData.description || !Array.isArray(planData.steps)) { throw new n8n_workflow_1.ApplicationError('Invalid plan structure: missing required fields'); } // Generate step IDs if not provided and validate steps const validatedSteps = planData.steps.map((step, index) => ({ id: step.id || `step_${index + 1}`, description: step.description || `Step ${index + 1}`, command: step.command, files: Array.isArray(step.files) ? step.files : undefined, estimated_duration: typeof step.estimated_duration === 'number' ? step.estimated_duration : 300, status: 'pending', })); const executionPlan = { id: planId, title: planData.title, description: planData.description, steps: validatedSteps, created_at: timestamp, modified_at: timestamp, status: 'draft', original_prompt: prompt, configuration: { model: options.model, projectPath: options.cwd, toolsConfig: options.toolsConfig, }, }; // Save the plan await GeminiCli.savePlan(executionPlan, options.cwd || process.cwd()); return executionPlan; } catch (error) { throw new n8n_workflow_1.ApplicationError(`Plan generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async editPlanWithGemini(context, planId, editInstructions, options) { const workingDirectory = options.cwd || process.cwd(); // Load the existing plan const existingPlan = await GeminiCli.loadPlan(planId, workingDirectory); if (!existingPlan) { throw new n8n_workflow_1.ApplicationError(`Plan with ID ${planId} not found`); } // Create editing-specific system prompt const editingSystemPrompt = `You are an AI assistant that modifies execution plans based on user instructions. You will receive: 1. An existing execution plan in JSON format 2. Instructions for how to modify the plan You must respond with the modified plan in the same JSON format: { "title": "Brief title for the task", "description": "Detailed description of what needs to be accomplished", "steps": [ { "id": "step_1", "description": "Clear description of what this step does", "command": "optional: specific command to run", "files": ["optional: array of files this step will work with"], "estimated_duration": 300 } ] } Modify only what is requested in the instructions. Keep the existing structure and IDs where possible. Respond ONLY with the JSON structure. Do not include any other text or explanations.`; const editingPrompt = `Here is the existing execution plan: ${JSON.stringify({ title: existingPlan.title, description: existingPlan.description, steps: existingPlan.steps.map(step => ({ id: step.id, description: step.description, command: step.command, files: step.files, estimated_duration: step.estimated_duration, })), }, null, 2)} Please modify this plan according to these instructions: ${editInstructions} Respond ONLY with the modified JSON structure.`; try { // Call Gemini CLI to edit the plan const response = await GeminiCli.runGeminiCLI(context, editingPrompt, { ...options, systemPrompt: editingSystemPrompt, }); if (!response.success) { throw new n8n_workflow_1.ApplicationError(`Failed to edit plan: ${response.error || 'Unknown error'}`); } // Parse the response to extract modified plan structure let planData; try { const jsonMatch = response.result.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new n8n_workflow_1.ApplicationError('No JSON structure found in Gemini response'); } planData = JSON.parse(jsonMatch[0]); } catch (parseError) { throw new n8n_workflow_1.ApplicationError(`Failed to parse edited plan structure: ${parseError instanceof Error ? parseError.message : 'Invalid JSON'}`); } // Validate and update the execution plan if (!planData.title || !planData.description || !Array.isArray(planData.steps)) { throw new n8n_workflow_1.ApplicationError('Invalid edited plan structure: missing required fields'); } const validatedSteps = planData.steps.map((step, index) => ({ id: step.id || `step_${index + 1}`, description: step.description || `Step ${index + 1}`, command: step.command, files: Array.isArray(step.files) ? step.files : undefined, estimated_duration: typeof step.estimated_duration === 'number' ? step.estimated_duration : 300, status: 'pending', })); const updatedPlan = { ...existingPlan, title: planData.title, description: planData.description, steps: validatedSteps, modified_at: Date.now(), status: 'draft', // Reset to draft after editing }; // Save the updated plan await GeminiCli.savePlan(updatedPlan, workingDirectory); return updatedPlan; } catch (error) { throw new n8n_workflow_1.ApplicationError(`Plan editing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async executePlan(context, planId, options) { const workingDirectory = options.cwd || process.cwd(); // Load the plan const plan = await GeminiCli.loadPlan(planId, workingDirectory); if (!plan) { throw new n8n_workflow_1.ApplicationError(`Plan with ID ${planId} not found`); } if (plan.status !== 'approved') { throw new n8n_workflow_1.ApplicationError(`Plan must be approved before execution. Current status: ${plan.status}`); } // Update plan status to executing plan.status = 'executing'; plan.modified_at = Date.now(); await GeminiCli.savePlan(plan, workingDirectory); const results = []; let allStepsSuccessful = true; try { // Execute each step sequentially for (const step of plan.steps) { if (options.debug) { console.log(`[GeminiCli] Executing step: ${step.id} - ${step.description}`); } // Update step status step.status = 'in_progress'; await GeminiCli.savePlan(plan, workingDirectory); // Create execution prompt for this step const executionPrompt = `Execute the following step from an approved plan: Step Description: ${step.description} ${step.command ? `Suggested Command: ${step.command}` : ''} ${step.files ? `Files to work with: ${step.files.join(', ')}` : ''} Complete this step thoroughly. If this involves code changes, make the actual changes. If it involves analysis, provide detailed analysis.`; try { // Execute the step const stepResult = await GeminiCli.runGeminiCLI(context, executionPrompt, options); results.push(stepResult); if (stepResult.success) { step.status = 'completed'; if (options.debug) { console.log(`[GeminiCli] Step ${step.id} completed successfully`); } } else { step.status = 'failed'; allStepsSuccessful = false; if (options.debug) { console.log(`[GeminiCli] Step ${step.id} failed: ${stepResult.error}`); } break; // Stop execution on first failure } } catch (stepError) { step.status = 'failed'; allStepsSuccessful = false; if (options.debug) { console.error(`[GeminiCli] Step ${step.id} error: ${stepError}`); } break; } // Save progress after each step await GeminiCli.savePlan(plan, workingDirectory); } // Update final plan status plan.status = allStepsSuccessful ? 'completed' : 'failed'; plan.modified_at = Date.now(); await GeminiCli.savePlan(plan, workingDirectory); return { plan, results }; } catch (error) { // Mark plan as failed on any error plan.status = 'failed'; plan.modified_at = Date.now(); await GeminiCli.savePlan(plan, workingDirectory); throw error; } } static async runGeminiCLI(context, prompt, options) { return new Promise(async (resolve, reject) => { var _a, _b, _c; const messages = []; const startTime = Date.now(); let fullResponse = ''; let configDir; try { // Set up environment const env = { ...process.env }; if (options.apiKey) { env.GEMINI_API_KEY = options.apiKey; } if (options.useVertexAI) { env.GOOGLE_GENAI_USE_VERTEXAI = 'true'; if (options.apiKey) { env.GOOGLE_API_KEY = options.apiKey; } } // Set up configuration directory for MCP servers const workingDirectory = options.cwd || process.cwd(); if (((_a = options.mcpServers) === null || _a === void 0 ? void 0 : _a.servers) && options.mcpServers.servers.length > 0) { const configResult = await GeminiCli.createGeminiConfig(context, options.mcpServers, workingDirectory, options.debug); configDir = configResult.configDir; } // Build command arguments const args = []; // Add model parameter if (options.model) { args.push('--model', options.model); } // Add security mode arguments if ((_b = options.toolsConfig) === null || _b === void 0 ? void 0 : _b.securityMode) { switch (options.toolsConfig.securityMode) { case 'yolo': args.push('--yolo'); break; case 'sandbox': args.push('--sandbox'); break; // 'safe' is the default, no additional args needed } } // Add checkpointing if enabled if ((_c = options.toolsConfig) === null || _c === void 0 ? void 0 : _c.checkpointing) { args.push('-c'); } //