Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Configuración Segura Local - Nano Banana
NANOBANANA_GEMINI_API_KEY="your-api-key-here"
NANOBANANA_MODEL="gemini-3.1-flash-image-preview"

# Fallback keys for Vertex/Gemini
# GEMINI_API_KEY="your-api-key-here"
# GOOGLE_API_KEY="your-api-key-here"
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
dist/
node_modules/
nanobanana-output/
tmp/

# Env files
.env
.env.*
!.env.example
.aiignore
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.0.12

- **Feature:** Added robust hierarchical `.env` file resolution support (from project root up to `~/.gemini/.env`) to load API keys securely without relying on global environment variables.
- **Fix:** Removed hardcoded local debug paths, unnecessary telemetry, and diagnostic logs from image generation flow for a cleaner production output (Fixes #18).

## 1.0.11

- Set Nano Banana 2 (`gemini-3.1-flash-image-preview`) as the default model.
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ A professional Gemini CLI extension for generating and manipulating images using

For authentication setup, see the [official Gemini CLI documentation](https://geminicli.com/docs/get-started/authentication/).

### Environment Variables (.env Support)

For better security and convenience, Nano Banana supports loading your API keys and configurations directly from a `.env` file, bypassing the Gemini CLI's environment variable redaction policies.

You can place a `.env` file in any of the following locations (the extension will automatically search for it in this exact order):

1. **Your Current Project Directory** (e.g., `./.env` in the folder where you are running the `gemini` command)
2. **A local `.gemini` folder** (e.g., `./.gemini/.env`)
3. **Your Global Gemini configuration folder** (e.g., `~/.gemini/.env` or `%USERPROFILE%\.gemini\.env`)
4. **Your User Home Directory** (e.g., `~/.env` or `%USERPROFILE%\.env`)

**💡 Recommended Setup:**

Create a `.env` file in your global Gemini directory (`~/.gemini/.env` on macOS/Linux or `C:\Users\YourUser\.gemini\.env` on Windows).

```env
NANOBANANA_GEMINI_API_KEY=AIzaSyYourSecretKeyHere...
# You can also set a specific model for the extension
NANOBANANA_MODEL=gemini-3.1-flash-image-preview
```

This ensures your keys are kept secure and loaded globally for Nano Banana without requiring you to manually `export` them in every terminal session.
### Key Components

- **`index.ts`**: MCP server using `@modelcontextprotocol/sdk` for professional protocol handling
Expand Down Expand Up @@ -454,10 +476,6 @@ The extension uses the official Model Context Protocol (MCP) SDK for robust clie

4. **"Image not found"**: Check that input files are in one of the searched directories (see File Search Locations above)

### Debug Mode

The MCP server includes detailed debug logging that appears in the Gemini CLI console to help diagnose issues.

## 📄 Legal

- **License**: [Apache License 2.0](LICENSE)
Expand Down
3 changes: 1 addition & 2 deletions gemini-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"mcpServers": {
"nanobanana": {
"command": "node",
"args": ["${extensionPath}/mcp-server/dist/index.js"],
"env": {}
"args": ["${extensionPath}/mcp-server/dist/index.js"]
}
},
"contextFileName": "GEMINI.md",
Expand Down
15 changes: 14 additions & 1 deletion mcp-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"dependencies": {
"@google/genai": "^1.17.0",
"@modelcontextprotocol/sdk": "^1.0.0"
"@modelcontextprotocol/sdk": "^1.0.0",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
84 changes: 4 additions & 80 deletions mcp-server/src/imageGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export class ImageGenerator {
});
this.modelName =
process.env.NANOBANANA_MODEL || ImageGenerator.DEFAULT_MODEL;
console.error(`DEBUG - Using image model: ${this.modelName}`);
}

private async openImagePreview(filePath: string): Promise<void> {
Expand All @@ -49,12 +48,7 @@ export class ImageGenerator {
}

await execAsync(command);
console.error(`DEBUG - Opened preview for: ${filePath}`);
} catch (error: unknown) {
console.error(
`DEBUG - Failed to open preview for ${filePath}:`,
error instanceof Error ? error.message : String(error),
);
// Don't throw - preview failure shouldn't break image generation
}
}
Expand Down Expand Up @@ -82,17 +76,10 @@ export class ImageGenerator {

if (!shouldPreview || !files.length) {
if (files.length > 1 && request.noPreview) {
console.error(
`DEBUG - Auto-preview disabled for ${files.length} images (--no-preview specified)`,
);
}
return;
}

console.error(
`DEBUG - ${request.preview ? 'Explicit' : 'Auto'}-opening ${files.length} image(s) for preview`,
);

// Open all generated images
const previewPromises = files.map((file) => this.openImagePreview(file));
await Promise.all(previewPromises);
Expand All @@ -108,29 +95,29 @@ export class ImageGenerator {
const nanoGeminiKey = process.env.NANOBANANA_GEMINI_API_KEY;
if (nanoGeminiKey) {
console.error('✓ Found NANOBANANA_GEMINI_API_KEY environment variable (fallback)');
return { apiKey: nanoGeminiKey };
return { apiKey: nanoGeminiKey, keyType: 'GEMINI_API_KEY' };
}

const nanoGoogleKey = process.env.NANOBANANA_GOOGLE_API_KEY;
if (nanoGoogleKey) {
console.error('✓ Found NANOBANANA_GOOGLE_API_KEY environment variable (fallback)');
return { apiKey: nanoGoogleKey };
return { apiKey: nanoGoogleKey, keyType: 'GOOGLE_API_KEY' };
}

const geminiKey = process.env.GEMINI_API_KEY;
if (geminiKey) {
console.error(
'✓ Found GEMINI_API_KEY environment variable (fallback)',
);
return { apiKey: geminiKey };
return { apiKey: geminiKey, keyType: 'GEMINI_API_KEY' };
}

const googleKey = process.env.GOOGLE_API_KEY;
if (googleKey) {
console.error(
'✓ Found GOOGLE_API_KEY environment variable (fallback)',
);
return { apiKey: googleKey };
return { apiKey: googleKey, keyType: 'GOOGLE_API_KEY' };
}

throw new Error(
Expand All @@ -154,11 +141,6 @@ export class ImageGenerator {

// Additional check: base64 image data is typically quite long
if (data.length < 1000) {
console.error(
'DEBUG - Skipping short data that may not be image:',
data.length,
'characters',
);
return false;
}

Expand Down Expand Up @@ -252,16 +234,8 @@ export class ImageGenerator {
const generatedFiles: string[] = [];
const prompts = this.buildBatchPrompts(request);
let firstError: string | null = null;

console.error(`DEBUG - Generating ${prompts.length} image variation(s)`);

for (let i = 0; i < prompts.length; i++) {
const currentPrompt = prompts[i];
console.error(
`DEBUG - Generating variation ${i + 1}/${prompts.length}:`,
currentPrompt,
);

try {
// Make API call for each variation
const response = await this.ai.models.generateContent({
Expand All @@ -274,24 +248,15 @@ export class ImageGenerator {
],
});

console.error('DEBUG - API Response structure for variation', i + 1);

if (response.candidates && response.candidates[0]?.content?.parts) {
// Process image parts in the response
for (const part of response.candidates[0].content.parts) {
let imageBase64: string | undefined;

if (part.inlineData?.data) {
imageBase64 = part.inlineData.data;
console.error('DEBUG - Found image data in inlineData:', {
length: imageBase64.length,
mimeType: part.inlineData.mimeType,
});
} else if (part.text && this.isValidBase64ImageData(part.text)) {
imageBase64 = part.text;
console.error(
'DEBUG - Found image data in text field (fallback)',
);
}

if (imageBase64) {
Expand All @@ -308,7 +273,6 @@ export class ImageGenerator {
filename,
);
generatedFiles.push(fullPath);
console.error('DEBUG - Image saved to:', fullPath);
break; // Only process first valid image per variation
}
}
Expand All @@ -318,10 +282,6 @@ export class ImageGenerator {
if (!firstError) {
firstError = errorMessage;
}
console.error(
`DEBUG - Error generating variation ${i + 1}:`,
errorMessage,
);

// If auth-related, stop immediately
if (errorMessage.toLowerCase().includes('authentication failed')) {
Expand Down Expand Up @@ -351,7 +311,6 @@ export class ImageGenerator {
generatedFiles,
};
} catch (error: unknown) {
console.error('DEBUG - Error in generateTextToImage:', error);
return {
success: false,
message: 'Failed to generate image',
Expand Down Expand Up @@ -419,8 +378,6 @@ export class ImageGenerator {
const transition = args?.transition || 'smooth';
let firstError: string | null = null;

console.error(`DEBUG - Generating ${steps}-step ${type} sequence`);

// Generate each step of the story/process
for (let i = 0; i < steps; i++) {
const stepNumber = i + 1;
Expand All @@ -447,8 +404,6 @@ export class ImageGenerator {
stepPrompt += `, ${transition} transition from previous step`;
}

console.error(`DEBUG - Generating step ${stepNumber}: ${stepPrompt}`);

try {
const response = await this.ai.models.generateContent({
model: this.modelName,
Expand Down Expand Up @@ -482,7 +437,6 @@ export class ImageGenerator {
filename,
);
generatedFiles.push(fullPath);
console.error(`DEBUG - Step ${stepNumber} saved to:`, fullPath);
break;
}
}
Expand All @@ -492,10 +446,6 @@ export class ImageGenerator {
if (!firstError) {
firstError = errorMessage;
}
console.error(
`DEBUG - Error generating step ${stepNumber}:`,
errorMessage,
);
if (errorMessage.toLowerCase().includes('authentication failed')) {
return {
success: false,
Expand All @@ -507,16 +457,9 @@ export class ImageGenerator {

// Check if this step was actually generated
if (generatedFiles.length < stepNumber) {
console.error(
`DEBUG - WARNING: Step ${stepNumber} failed to generate - no valid image data received`,
);
}
}

console.error(
`DEBUG - Story generation completed. Generated ${generatedFiles.length} out of ${steps} requested images`,
);

if (generatedFiles.length === 0) {
return {
success: false,
Expand All @@ -539,7 +482,6 @@ export class ImageGenerator {
generatedFiles,
};
} catch (error: unknown) {
console.error('DEBUG - Error in generateStorySequence:', error);
return {
success: false,
message: `Failed to generate ${request.mode} sequence`,
Expand Down Expand Up @@ -590,12 +532,6 @@ export class ImageGenerator {
},
],
});

console.error(
'DEBUG - Edit API Response structure:',
JSON.stringify(response, null, 2),
);

if (response.candidates && response.candidates[0]?.content?.parts) {
const generatedFiles: string[] = [];
let imageFound = false;
Expand All @@ -605,15 +541,8 @@ export class ImageGenerator {

if (part.inlineData?.data) {
resultImageBase64 = part.inlineData.data;
console.error('DEBUG - Found edited image in inlineData:', {
length: resultImageBase64.length,
mimeType: part.inlineData.mimeType,
});
} else if (part.text && this.isValidBase64ImageData(part.text)) {
resultImageBase64 = part.text;
console.error(
'DEBUG - Found edited image in text field (fallback)',
);
}

if (resultImageBase64) {
Expand All @@ -628,16 +557,12 @@ export class ImageGenerator {
filename,
);
generatedFiles.push(fullPath);
console.error('DEBUG - Edited image saved to:', fullPath);
imageFound = true;
break; // Only process the first valid image
}
}

if (!imageFound) {
console.error(
'DEBUG - No valid image data found in edit response parts',
);
}

// Handle preview if requested
Expand All @@ -656,7 +581,6 @@ generatedFiles.push(fullPath);
error: 'No image data in response',
};
} catch (error: unknown) {
console.error(`DEBUG - Error in ${request.mode}Image:`, error);
return {
success: false,
message: `Failed to ${request.mode} image`,
Expand Down
Loading