genmix 1.0.5 → 1.2.2

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.
@@ -29,9 +29,13 @@ class BaseGenerator {
29
29
  * @param {Object} [options.formatOptions] - Format-specific options (quality, compressionLevel, palette, colours, etc.)
30
30
  * @returns {Promise<string[]>} Promise that resolves to array of saved file paths.
31
31
  */
32
- async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null} = {}) {
32
+ async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null, useSharp = true} = {}) {
33
33
  const targetImages = this.lastGeneration?.images;
34
34
  const targetPrompt = this.lastGeneration?.prompt;
35
+ const inheritedFormatOptions = this.lastGeneration?.formatOptions || null;
36
+ const effectiveFormatOptions = formatOptions
37
+ ? { ...(inheritedFormatOptions || {}), ...formatOptions }
38
+ : inheritedFormatOptions;
35
39
 
36
40
  if (!targetImages || !Array.isArray(targetImages) || targetImages.length === 0) {
37
41
  console.warn('No images to save.');
@@ -48,8 +52,8 @@ class BaseGenerator {
48
52
  }
49
53
 
50
54
  // Use format from formatOptions if provided, otherwise use extension
51
- if (formatOptions && formatOptions.format) {
52
- extension = formatOptions.format;
55
+ if (effectiveFormatOptions && effectiveFormatOptions.format) {
56
+ extension = effectiveFormatOptions.format;
53
57
  }
54
58
 
55
59
  // Normalize extension
@@ -69,32 +73,43 @@ class BaseGenerator {
69
73
 
70
74
  for (let index = 0; index < targetImages.length; index++) {
71
75
  const imgData = targetImages[index];
72
- const base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
76
+ const mimeMatch = imgData.match(/^data:image\/([a-zA-Z0-9.+-]+);base64,/);
77
+ const sourceFormat = mimeMatch ? mimeMatch[1].toLowerCase() : null;
78
+ const normalizedSourceFormat = sourceFormat === 'jpeg' ? 'jpg' : sourceFormat;
79
+ const base64Data = imgData.replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, "");
73
80
  const buffer = Buffer.from(base64Data, 'base64');
74
81
 
82
+ const effectiveExtension = useSharp ? fileExtension : (normalizedSourceFormat || fileExtension);
83
+
75
84
  let fileName;
76
85
  if (filename) {
77
86
  // Use custom filename, add index if multiple images
78
87
  if (targetImages.length > 1) {
79
- fileName = `${filename}_${index}.${fileExtension}`;
88
+ fileName = `${filename}_${index}.${effectiveExtension}`;
80
89
  } else {
81
- fileName = `${filename}.${fileExtension}`;
90
+ fileName = `${filename}.${effectiveExtension}`;
82
91
  }
83
92
  } else {
84
93
  // Use hash-based filename
85
94
  const hash = this.generateHash(targetPrompt + '_' + index);
86
- fileName = `${hash}.${fileExtension}`;
95
+ fileName = `${hash}.${effectiveExtension}`;
87
96
  }
88
97
 
89
98
  const outputPath = path.join(directory, fileName);
90
99
 
100
+ if (!useSharp) {
101
+ fs.writeFileSync(outputPath, buffer);
102
+ savedPaths.push(outputPath);
103
+ continue;
104
+ }
105
+
91
106
  // Convert image format using sharp
92
107
  try {
93
108
  let sharpInstance = sharp(buffer);
94
109
 
95
110
  // Resize if dimensions are specified in formatOptions
96
- if (formatOptions && formatOptions.width && formatOptions.height) {
97
- sharpInstance = sharpInstance.resize(formatOptions.width, formatOptions.height, {
111
+ if (effectiveFormatOptions && effectiveFormatOptions.width && effectiveFormatOptions.height) {
112
+ sharpInstance = sharpInstance.resize(effectiveFormatOptions.width, effectiveFormatOptions.height, {
98
113
  fit: 'fill'
99
114
  });
100
115
  }
@@ -102,29 +117,29 @@ class BaseGenerator {
102
117
  // Build format-specific options
103
118
  const sharpFormatOptions = {};
104
119
 
105
- if (formatOptions) {
106
- if (sharpFormat === 'jpg' && formatOptions.quality) {
107
- sharpFormatOptions.quality = formatOptions.quality;
120
+ if (effectiveFormatOptions) {
121
+ if (sharpFormat === 'jpg' && effectiveFormatOptions.quality) {
122
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
108
123
  } else if (sharpFormat === 'png') {
109
- if (formatOptions.compressionLevel !== undefined) {
110
- sharpFormatOptions.compressionLevel = formatOptions.compressionLevel;
124
+ if (effectiveFormatOptions.compressionLevel !== undefined) {
125
+ sharpFormatOptions.compressionLevel = effectiveFormatOptions.compressionLevel;
111
126
  }
112
- if (formatOptions.quality !== undefined) {
113
- sharpFormatOptions.quality = formatOptions.quality;
127
+ if (effectiveFormatOptions.quality !== undefined) {
128
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
114
129
  }
115
- if (formatOptions.effort !== undefined) {
116
- sharpFormatOptions.effort = formatOptions.effort;
130
+ if (effectiveFormatOptions.effort !== undefined) {
131
+ sharpFormatOptions.effort = effectiveFormatOptions.effort;
117
132
  }
118
- if (formatOptions.palette) {
133
+ if (effectiveFormatOptions.palette) {
119
134
  sharpFormatOptions.palette = true;
120
135
  // Add dithering for better quality with palette
121
136
  sharpFormatOptions.dither = 1.0;
122
137
  }
123
- if (formatOptions.colours) {
124
- sharpFormatOptions.colours = formatOptions.colours;
138
+ if (effectiveFormatOptions.colours) {
139
+ sharpFormatOptions.colours = effectiveFormatOptions.colours;
125
140
  }
126
- } else if (sharpFormat === 'webp' && formatOptions.quality) {
127
- sharpFormatOptions.quality = formatOptions.quality;
141
+ } else if (sharpFormat === 'webp' && effectiveFormatOptions.quality) {
142
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
128
143
  }
129
144
  }
130
145
 
@@ -0,0 +1,332 @@
1
+ const axios = require('axios');
2
+ const BaseGenerator = require('./BaseGenerator');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ class FalGenerator extends BaseGenerator {
7
+ static MODELS = {
8
+ BANANA_2: 'fal-ai/nano-banana-2/edit',
9
+ BANANA_PRO_EDIT: 'fal-ai/nano-banana-pro/edit'
10
+ };
11
+
12
+ static SUPPORTED_ASPECT_RATIOS_BY_MODEL = {
13
+ [FalGenerator.MODELS.BANANA_2]: new Set([
14
+ 'auto',
15
+ '21:9',
16
+ '16:9',
17
+ '3:2',
18
+ '4:3',
19
+ '5:4',
20
+ '1:1',
21
+ '4:5',
22
+ '3:4',
23
+ '2:3',
24
+ '9:16',
25
+ '4:1',
26
+ '1:4',
27
+ '8:1',
28
+ '1:8'
29
+ ]),
30
+ [FalGenerator.MODELS.BANANA_PRO_EDIT]: new Set([
31
+ 'auto',
32
+ '21:9',
33
+ '16:9',
34
+ '3:2',
35
+ '4:3',
36
+ '5:4',
37
+ '1:1',
38
+ '4:5',
39
+ '3:4',
40
+ '2:3',
41
+ '9:16'
42
+ ])
43
+ };
44
+
45
+ static SUPPORTED_RESOLUTIONS_BY_MODEL = {
46
+ [FalGenerator.MODELS.BANANA_2]: new Set(['0.5K', '1K', '2K', '4K']),
47
+ [FalGenerator.MODELS.BANANA_PRO_EDIT]: new Set(['1K', '2K', '4K'])
48
+ };
49
+
50
+ static COMMON_ASPECT_RATIOS = new Set([
51
+ 'auto',
52
+ '21:9',
53
+ '16:9',
54
+ '3:2',
55
+ '4:3',
56
+ '5:4',
57
+ '1:1',
58
+ '4:5',
59
+ '3:4',
60
+ '2:3',
61
+ '9:16'
62
+ ]);
63
+
64
+ constructor(config = {}) {
65
+ super(config);
66
+ this.apiKey = config.apiKey || process.env.FAL_API_KEY;
67
+ if (!this.apiKey) {
68
+ throw new Error('API Key is required. Provide it in the constructor or set FAL_API_KEY environment variable.');
69
+ }
70
+ this.modelId = config.modelId || FalGenerator.MODELS.BANANA_2;
71
+ this.apiUrl = `https://fal.run/${this.modelId}`;
72
+ this.references = [];
73
+ }
74
+
75
+ banana2() {
76
+ this.modelId = FalGenerator.MODELS.BANANA_2;
77
+ this.apiUrl = `https://fal.run/${this.modelId}`;
78
+ return this;
79
+ }
80
+
81
+ bananaPro() {
82
+ this.modelId = FalGenerator.MODELS.BANANA_PRO_EDIT;
83
+ this.apiUrl = `https://fal.run/${this.modelId}`;
84
+ return this;
85
+ }
86
+
87
+ pro() {
88
+ return this.bananaPro();
89
+ }
90
+
91
+ flash() {
92
+ return this.banana2();
93
+ }
94
+
95
+ addReference(image, description = '') {
96
+ this.references.push({ image, description });
97
+ return this;
98
+ }
99
+
100
+ clearReferences() {
101
+ this.references = [];
102
+ return this;
103
+ }
104
+
105
+ _gcd(a, b) {
106
+ let x = Math.abs(a);
107
+ let y = Math.abs(b);
108
+ while (y !== 0) {
109
+ const t = y;
110
+ y = x % y;
111
+ x = t;
112
+ }
113
+ return x || 1;
114
+ }
115
+
116
+ _deriveAspectRatio(width, height) {
117
+ const divisor = this._gcd(width, height);
118
+ return `${width / divisor}:${height / divisor}`;
119
+ }
120
+
121
+ _normalizeGenerateOptions(options = {}) {
122
+ const normalized = { ...options };
123
+ const hasWidth = normalized.width !== undefined && normalized.width !== null;
124
+ const hasHeight = normalized.height !== undefined && normalized.height !== null;
125
+
126
+ if (hasWidth !== hasHeight) {
127
+ throw new Error('Both options.width and options.height are required together.');
128
+ }
129
+
130
+ if (hasWidth && hasHeight) {
131
+ const width = Number(normalized.width);
132
+ const height = Number(normalized.height);
133
+
134
+ if (!Number.isInteger(width) || width <= 0) {
135
+ throw new Error('options.width must be a positive integer.');
136
+ }
137
+ if (!Number.isInteger(height) || height <= 0) {
138
+ throw new Error('options.height must be a positive integer.');
139
+ }
140
+
141
+ const derivedRatio = this._deriveAspectRatio(width, height);
142
+ if (normalized.aspectRatio && normalized.aspectRatio !== derivedRatio) {
143
+ throw new Error(`Aspect ratio mismatch: options.aspectRatio ${normalized.aspectRatio} does not match options.width/options.height (${derivedRatio}).`);
144
+ }
145
+
146
+ normalized.width = width;
147
+ normalized.height = height;
148
+ normalized.aspectRatio = derivedRatio;
149
+ }
150
+
151
+ const supportedRatios = FalGenerator.SUPPORTED_ASPECT_RATIOS_BY_MODEL[this.modelId] || FalGenerator.COMMON_ASPECT_RATIOS;
152
+ if (normalized.aspectRatio && !supportedRatios.has(normalized.aspectRatio)) {
153
+ if (hasWidth && hasHeight) {
154
+ normalized.aspectRatio = 'auto';
155
+ } else {
156
+ throw new Error(`Unsupported aspect ratio for Fal provider: ${normalized.aspectRatio}.`);
157
+ }
158
+ }
159
+
160
+ return normalized;
161
+ }
162
+
163
+ _mapQualityToResolution(quality) {
164
+ const mapped = String(quality || '1K').toUpperCase();
165
+ const supportedResolutions = FalGenerator.SUPPORTED_RESOLUTIONS_BY_MODEL[this.modelId] || new Set(['1K', '2K', '4K']);
166
+ if (!supportedResolutions.has(mapped)) {
167
+ throw new Error(`options.quality must be one of: ${Array.from(supportedResolutions).join(', ')}.`);
168
+ }
169
+ return mapped;
170
+ }
171
+
172
+ _isHttpUrl(value) {
173
+ return typeof value === 'string' && /^https?:\/\//i.test(value.trim());
174
+ }
175
+
176
+ _isDataUri(value) {
177
+ return typeof value === 'string' && /^data:image\/[a-zA-Z0-9.+-]+;base64,/.test(value.trim());
178
+ }
179
+
180
+ _detectMimeTypeFromPath(filePath) {
181
+ const ext = path.extname(filePath).toLowerCase();
182
+ const mimeTypes = {
183
+ '.png': 'image/png',
184
+ '.jpg': 'image/jpeg',
185
+ '.jpeg': 'image/jpeg',
186
+ '.webp': 'image/webp',
187
+ '.gif': 'image/gif',
188
+ '.bmp': 'image/bmp',
189
+ '.tif': 'image/tiff',
190
+ '.tiff': 'image/tiff',
191
+ '.avif': 'image/avif'
192
+ };
193
+ return mimeTypes[ext] || 'image/png';
194
+ }
195
+
196
+ _toDataUriFromBuffer(buffer, mimeType = 'image/png') {
197
+ return `data:${mimeType};base64,${buffer.toString('base64')}`;
198
+ }
199
+
200
+ async _normalizeImageReference(value) {
201
+ if (Buffer.isBuffer(value)) {
202
+ return this._toDataUriFromBuffer(value, 'image/png');
203
+ }
204
+
205
+ if (typeof value !== 'string') {
206
+ throw new Error('Fal references must be URL, data URI, local file path, or Buffer.');
207
+ }
208
+
209
+ const trimmed = value.trim();
210
+ if (!trimmed) {
211
+ throw new Error('Fal reference cannot be empty.');
212
+ }
213
+
214
+ if (this._isHttpUrl(trimmed) || this._isDataUri(trimmed)) {
215
+ return trimmed;
216
+ }
217
+
218
+ if (!fs.existsSync(trimmed)) {
219
+ throw new Error(`Fal reference file not found: ${trimmed}`);
220
+ }
221
+
222
+ const fileBuffer = fs.readFileSync(trimmed);
223
+ const mimeType = this._detectMimeTypeFromPath(trimmed);
224
+ return this._toDataUriFromBuffer(fileBuffer, mimeType);
225
+ }
226
+
227
+ async _collectImageUrls(options = {}) {
228
+ const rawReferences = [];
229
+
230
+ if (Array.isArray(options.imageUrls)) {
231
+ for (const value of options.imageUrls) {
232
+ if ((typeof value === 'string' && value.trim()) || Buffer.isBuffer(value)) {
233
+ rawReferences.push(value);
234
+ }
235
+ }
236
+ }
237
+
238
+ if ((typeof options.referenceImage === 'string' && options.referenceImage.trim()) || Buffer.isBuffer(options.referenceImage)) {
239
+ rawReferences.push(options.referenceImage);
240
+ }
241
+
242
+ for (const ref of this.references) {
243
+ if ((typeof ref.image === 'string' && ref.image.trim()) || Buffer.isBuffer(ref.image)) {
244
+ rawReferences.push(ref.image);
245
+ }
246
+ }
247
+
248
+ const normalizedUrls = [];
249
+ for (const reference of rawReferences) {
250
+ normalizedUrls.push(await this._normalizeImageReference(reference));
251
+ }
252
+
253
+ const uniqueUrls = Array.from(new Set(normalizedUrls));
254
+ return uniqueUrls;
255
+ }
256
+
257
+ async generate(prompt, options = {}) {
258
+ if (!prompt || !String(prompt).trim()) {
259
+ throw new Error('Prompt is required.');
260
+ }
261
+
262
+ const normalizedOptions = this._normalizeGenerateOptions(options);
263
+ const numberOfImages = normalizedOptions.numberOfImages || 1;
264
+
265
+ if (!Number.isInteger(numberOfImages) || numberOfImages < 1 || numberOfImages > 4) {
266
+ throw new Error('options.numberOfImages must be an integer between 1 and 4 for Fal Nano Banana models.');
267
+ }
268
+
269
+ const payload = {
270
+ prompt: String(prompt).trim(),
271
+ num_images: numberOfImages,
272
+ resolution: this._mapQualityToResolution(normalizedOptions.quality),
273
+ aspect_ratio: normalizedOptions.aspectRatio || 'auto'
274
+ };
275
+
276
+ const imageUrls = await this._collectImageUrls(normalizedOptions);
277
+ if (imageUrls.length === 0) {
278
+ throw new Error('Fal Nano Banana edit models require at least one reference image. Use addReference() or options.referenceImage.');
279
+ }
280
+ payload.image_urls = imageUrls;
281
+
282
+ try {
283
+ const response = await axios.post(this.apiUrl, payload, {
284
+ headers: {
285
+ Authorization: `Key ${this.apiKey}`,
286
+ 'Content-Type': 'application/json'
287
+ }
288
+ });
289
+
290
+ const result = await this.processResponse(response.data);
291
+ this.lastGeneration = {
292
+ prompt: String(prompt).trim(),
293
+ images: result.images,
294
+ text: result.text,
295
+ raw: result.raw,
296
+ formatOptions: normalizedOptions.width && normalizedOptions.height
297
+ ? { width: normalizedOptions.width, height: normalizedOptions.height }
298
+ : null
299
+ };
300
+ this.references = [];
301
+ return result;
302
+ } catch (error) {
303
+ const apiError = error.response?.data;
304
+ const message = apiError?.detail || apiError?.error || error.message;
305
+ throw new Error(`Fal API Error: ${message}`);
306
+ }
307
+ }
308
+
309
+ async processResponse(data) {
310
+ const imageEntries = Array.isArray(data?.images) ? data.images : [];
311
+ const images = [];
312
+
313
+ for (const entry of imageEntries) {
314
+ if (!entry || !entry.url) {
315
+ continue;
316
+ }
317
+
318
+ const mediaResponse = await axios.get(entry.url, { responseType: 'arraybuffer' });
319
+ const contentType = entry.content_type || mediaResponse.headers['content-type'] || 'image/png';
320
+ const base64 = Buffer.from(mediaResponse.data).toString('base64');
321
+ images.push(`data:${contentType};base64,${base64}`);
322
+ }
323
+
324
+ return {
325
+ images,
326
+ text: data?.description || '',
327
+ raw: data
328
+ };
329
+ }
330
+ }
331
+
332
+ module.exports = FalGenerator;
@@ -67,6 +67,55 @@ class GeminiGenerator extends BaseGenerator {
67
67
  return this;
68
68
  }
69
69
 
70
+ _gcd(a, b) {
71
+ let x = Math.abs(a);
72
+ let y = Math.abs(b);
73
+ while (y !== 0) {
74
+ const t = y;
75
+ y = x % y;
76
+ x = t;
77
+ }
78
+ return x || 1;
79
+ }
80
+
81
+ _deriveAspectRatio(width, height) {
82
+ const divisor = this._gcd(width, height);
83
+ return `${width / divisor}:${height / divisor}`;
84
+ }
85
+
86
+ _normalizeGenerateOptions(options = {}) {
87
+ const normalizedOptions = { ...options };
88
+ const hasWidth = normalizedOptions.width !== undefined && normalizedOptions.width !== null;
89
+ const hasHeight = normalizedOptions.height !== undefined && normalizedOptions.height !== null;
90
+
91
+ if (hasWidth !== hasHeight) {
92
+ throw new Error('Both options.width and options.height are required together.');
93
+ }
94
+
95
+ if (hasWidth && hasHeight) {
96
+ const width = Number(normalizedOptions.width);
97
+ const height = Number(normalizedOptions.height);
98
+
99
+ if (!Number.isInteger(width) || width <= 0) {
100
+ throw new Error('options.width must be a positive integer.');
101
+ }
102
+ if (!Number.isInteger(height) || height <= 0) {
103
+ throw new Error('options.height must be a positive integer.');
104
+ }
105
+
106
+ const derivedRatio = this._deriveAspectRatio(width, height);
107
+ if (normalizedOptions.aspectRatio && normalizedOptions.aspectRatio !== derivedRatio) {
108
+ throw new Error(`Aspect ratio mismatch: options.aspectRatio ${normalizedOptions.aspectRatio} does not match options.width/options.height (${derivedRatio}).`);
109
+ }
110
+
111
+ normalizedOptions.aspectRatio = derivedRatio;
112
+ normalizedOptions.width = width;
113
+ normalizedOptions.height = height;
114
+ }
115
+
116
+ return normalizedOptions;
117
+ }
118
+
70
119
  /**
71
120
  * @param {string} prompt
72
121
  * @param {Object} [options]
@@ -74,19 +123,22 @@ class GeminiGenerator extends BaseGenerator {
74
123
  * @param {string} [options.numberOfImages] - Number of images to generate
75
124
  * @param {string} [options.quality] - Image quality: '1K', '2K', '4K'
76
125
  * @param {string} [options.aspectRatio] - Aspect ratio like '1:1', '16:9', etc.
126
+ * @param {number} [options.width] - Final output width in pixels (requires options.height)
127
+ * @param {number} [options.height] - Final output height in pixels (requires options.width)
77
128
  * @returns {Promise<{images: string[], text: string, raw: any}>}
78
129
  */
79
130
  async generate(prompt, options = {}) {
131
+ const normalizedOptions = this._normalizeGenerateOptions(options);
80
132
  // If the user asks for multiple images, we might need to make parallel requests
81
133
  // if the API doesn't support candidateCount > 1 for images.
82
134
  // Based on search results, candidateCount > 1 can cause 400 errors.
83
- const numberOfImages = options.numberOfImages || 1;
135
+ const numberOfImages = normalizedOptions.numberOfImages || 1;
84
136
 
85
137
  let result;
86
138
  if (numberOfImages > 1) {
87
- result = await this.generateMultiple(prompt, numberOfImages, options);
139
+ result = await this.generateMultiple(prompt, numberOfImages, normalizedOptions);
88
140
  } else {
89
- result = await this._generateSingleRequest(prompt, options);
141
+ result = await this._generateSingleRequest(prompt, normalizedOptions);
90
142
  }
91
143
 
92
144
  // Store result in state for saveImages()
@@ -94,7 +146,10 @@ class GeminiGenerator extends BaseGenerator {
94
146
  prompt: prompt,
95
147
  images: result.images,
96
148
  text: result.text,
97
- raw: result.raw
149
+ raw: result.raw,
150
+ formatOptions: normalizedOptions.width && normalizedOptions.height
151
+ ? { width: normalizedOptions.width, height: normalizedOptions.height }
152
+ : null
98
153
  };
99
154
 
100
155
  this.references = [];
package/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const GeminiGenerator = require('./generators/GeminiGenerator');
2
+ const FalGenerator = require('./generators/FalGenerator');
2
3
 
3
4
  module.exports = {
4
5
  GeminiGenerator,
6
+ FalGenerator,
5
7
  MODELS: GeminiGenerator.MODELS,
8
+ FAL_MODELS: FalGenerator.MODELS,
6
9
  };
7
10
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "genmix",
3
- "version": "1.0.5",
4
- "description": "AI-powered image generator using Google Gemini API. Supports image generation from text prompts and image modification with reference images.",
3
+ "version": "1.2.2",
4
+ "description": "AI-powered image generator supporting Google Gemini and Fal Nano Banana 2.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Clasen",
7
7
  "keywords": [
@@ -11,16 +11,23 @@
11
11
  "flash",
12
12
  "image-generator",
13
13
  "google-gemini",
14
+ "fal-ai",
15
+ "nano-banana-2",
16
+ "nano-banana-pro",
14
17
  "gemini-api",
15
18
  "text-to-image",
16
19
  "image-modification",
17
20
  "style-transfer",
18
21
  "generative-ai",
19
22
  "genmix",
23
+ "cli",
20
24
  "clasen"
21
25
  ],
22
26
  "type": "commonjs",
23
27
  "main": "index.js",
28
+ "bin": {
29
+ "genmix": "./cli.js"
30
+ },
24
31
  "scripts": {
25
32
  "test": "echo \"Error: no test specified\" && exit 1"
26
33
  },