@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
JavaScript
"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');
}
//