@cli4ai/nanobanana 1.0.12 → 1.0.15

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