@cli4ai/nanobanana 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c4ai.json +21 -0
- package/package.json +43 -0
- package/run.ts +173 -0
package/c4ai.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nanobanana",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Image generation with Gemini (Nano Banana Pro)",
|
|
5
|
+
"author": "cliforai",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"entry": "run.ts",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"keywords": ["gemini", "image", "generation", "ai", "browser"],
|
|
10
|
+
"commands": {
|
|
11
|
+
"image": { "description": "Generate image with Gemini", "args": [{ "name": "prompt", "required": true }] }
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"puppeteer": "^24.0.0",
|
|
15
|
+
"commander": "^14.0.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"chrome": "c4ai chrome tool must be connected with Google account logged in"
|
|
19
|
+
},
|
|
20
|
+
"mcp": { "enabled": true, "transport": "stdio" }
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cli4ai/nanobanana",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Image generation with Gemini (Nano Banana Pro)",
|
|
5
|
+
"author": "cliforai",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "run.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nanobanana": "./run.ts"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"c4ai",
|
|
14
|
+
"cli",
|
|
15
|
+
"ai-tools",
|
|
16
|
+
"gemini",
|
|
17
|
+
"image",
|
|
18
|
+
"generation",
|
|
19
|
+
"ai",
|
|
20
|
+
"browser"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/cli4ai/packages",
|
|
25
|
+
"directory": "packages/nanobanana"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/cli4ai/packages/tree/main/packages/nanobanana",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/cli4ai/packages/issues"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"chrome": "c4ai chrome tool must be connected with Google account logged in"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"run.ts",
|
|
36
|
+
"c4ai.json",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/run.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import puppeteer from '../chrome/node_modules/puppeteer';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { cli, output, outputError, withErrorHandling, sleep } from '../lib/cli.ts';
|
|
8
|
+
|
|
9
|
+
// List Gemini files in Downloads using shell (bypasses sandbox)
|
|
10
|
+
function getGeminiFiles(): string[] {
|
|
11
|
+
try {
|
|
12
|
+
const result = execSync('ls -t ~/Downloads/Gemini_Generated_Image_* 2>/dev/null', { encoding: 'utf-8' });
|
|
13
|
+
return result.trim().split('\n').filter(Boolean);
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const WS_FILE = join(import.meta.dir, '../chrome/.ws-endpoint');
|
|
20
|
+
const DOWNLOADS_DIR = join(homedir(), 'Downloads');
|
|
21
|
+
|
|
22
|
+
const program = cli('nanobanana', '1.0.0', 'Image generation with Gemini (Nano Banana Pro)');
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('image <prompt>')
|
|
26
|
+
.description('Generate image with Gemini')
|
|
27
|
+
.action(withErrorHandling(async (prompt: string) => {
|
|
28
|
+
// Prepend "Create an image of" if not already an image prompt
|
|
29
|
+
const imagePrompt = prompt.toLowerCase().startsWith('create') ? prompt : `Create an image of ${prompt}`;
|
|
30
|
+
|
|
31
|
+
let ws: string;
|
|
32
|
+
try {
|
|
33
|
+
ws = readFileSync(WS_FILE, 'utf-8');
|
|
34
|
+
} catch {
|
|
35
|
+
outputError('NOT_FOUND', 'Not connected to Chrome', { hint: 'Run: chrome connect' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const browser = await puppeteer.connect({ browserWSEndpoint: ws! });
|
|
39
|
+
const page = await browser.newPage();
|
|
40
|
+
await page.setViewport({ width: 2560, height: 1440 });
|
|
41
|
+
|
|
42
|
+
// Reset download behavior to default (~/Downloads)
|
|
43
|
+
const client = await page.createCDPSession();
|
|
44
|
+
await client.send('Browser.setDownloadBehavior', {
|
|
45
|
+
behavior: 'default'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
console.error('Navigating to Gemini...');
|
|
50
|
+
await page.goto('https://gemini.google.com/app', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
51
|
+
await sleep(2000);
|
|
52
|
+
|
|
53
|
+
// Wait for the input area
|
|
54
|
+
console.error('Waiting for input area...');
|
|
55
|
+
const inputSelector = 'div[contenteditable="true"], rich-textarea div[contenteditable="true"], .ql-editor';
|
|
56
|
+
await page.waitForSelector(inputSelector, { timeout: 30000 });
|
|
57
|
+
await sleep(1000);
|
|
58
|
+
|
|
59
|
+
// Type the prompt
|
|
60
|
+
console.error(`Typing prompt: "${imagePrompt}"`);
|
|
61
|
+
const inputElement = await page.$(inputSelector);
|
|
62
|
+
await inputElement!.click();
|
|
63
|
+
await sleep(500);
|
|
64
|
+
await page.keyboard.type(imagePrompt, { delay: 30 });
|
|
65
|
+
await sleep(500);
|
|
66
|
+
|
|
67
|
+
// Click the Send message button
|
|
68
|
+
console.error('Submitting prompt...');
|
|
69
|
+
const sendBtn = await page.$('button[aria-label="Send message"]');
|
|
70
|
+
if (sendBtn) {
|
|
71
|
+
await sendBtn.click();
|
|
72
|
+
} else {
|
|
73
|
+
await page.keyboard.press('Enter');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wait for image generation (up to 120 seconds)
|
|
77
|
+
console.error('Waiting for image generation (up to 120 seconds)...');
|
|
78
|
+
|
|
79
|
+
let imageElement: puppeteer.ElementHandle | null = null;
|
|
80
|
+
const maxWait = 120000;
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
|
|
83
|
+
while (Date.now() - startTime < maxWait) {
|
|
84
|
+
await sleep(3000);
|
|
85
|
+
|
|
86
|
+
// Look for generated images (large ones from googleusercontent)
|
|
87
|
+
const imgs = await page.$$('img[src*="googleusercontent.com"]');
|
|
88
|
+
for (const img of imgs) {
|
|
89
|
+
const box = await img.boundingBox();
|
|
90
|
+
if (box && box.width > 200 && box.height > 200) {
|
|
91
|
+
imageElement = img;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (imageElement) {
|
|
97
|
+
console.error('Image generated!');
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.stderr.write('.');
|
|
102
|
+
}
|
|
103
|
+
console.error('');
|
|
104
|
+
|
|
105
|
+
if (!imageElement) {
|
|
106
|
+
outputError('TIMEOUT', 'No image generated within timeout');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Click on the image to open the preview modal
|
|
110
|
+
console.error('Opening image preview...');
|
|
111
|
+
await imageElement!.click();
|
|
112
|
+
await sleep(2000);
|
|
113
|
+
|
|
114
|
+
// Click the download button (has mat-icon with download fonticon)
|
|
115
|
+
console.error('Clicking download button...');
|
|
116
|
+
await sleep(1000);
|
|
117
|
+
|
|
118
|
+
const downloadClicked = await page.evaluate(() => {
|
|
119
|
+
const btns = document.querySelectorAll('button');
|
|
120
|
+
for (const btn of btns) {
|
|
121
|
+
const icon = btn.querySelector('mat-icon[fonticon="download"]');
|
|
122
|
+
if (icon) {
|
|
123
|
+
btn.click();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!downloadClicked) {
|
|
131
|
+
outputError('NOT_FOUND', 'Could not find download button');
|
|
132
|
+
}
|
|
133
|
+
console.error('Download initiated');
|
|
134
|
+
|
|
135
|
+
// Poll for new file in Downloads
|
|
136
|
+
const beforeFiles = new Set(getGeminiFiles());
|
|
137
|
+
let newFile: string | null = null;
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < 30; i++) {
|
|
140
|
+
await sleep(1000);
|
|
141
|
+
const afterFiles = getGeminiFiles();
|
|
142
|
+
const newFiles = afterFiles.filter(f => !beforeFiles.has(f));
|
|
143
|
+
if (newFiles.length > 0) {
|
|
144
|
+
newFile = newFiles[0];
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
process.stderr.write('.');
|
|
148
|
+
}
|
|
149
|
+
console.error('');
|
|
150
|
+
|
|
151
|
+
// Close the preview modal
|
|
152
|
+
await page.keyboard.press('Escape');
|
|
153
|
+
|
|
154
|
+
if (newFile) {
|
|
155
|
+
output({
|
|
156
|
+
prompt: imagePrompt,
|
|
157
|
+
image: newFile
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
output({
|
|
161
|
+
prompt: imagePrompt,
|
|
162
|
+
downloadDir: DOWNLOADS_DIR,
|
|
163
|
+
hint: 'Check Downloads folder for Gemini_Generated_Image_*.jpeg'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} finally {
|
|
168
|
+
// Keep page open, just disconnect
|
|
169
|
+
browser.disconnect();
|
|
170
|
+
}
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
program.parse();
|