egaki 0.0.1 → 0.1.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/CHANGELOG.md +54 -0
- package/README.md +239 -0
- package/dist/antigravity-auth.d.ts +15 -0
- package/dist/antigravity-auth.d.ts.map +1 -0
- package/dist/antigravity-auth.js +544 -0
- package/dist/antigravity-image.d.ts +11 -0
- package/dist/antigravity-image.d.ts.map +1 -0
- package/dist/antigravity-image.js +285 -0
- package/dist/chatgpt-auth.js +294 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +666 -0
- package/dist/credentials.d.ts +18 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +107 -0
- package/dist/gateway-model-candidates.generated.d.ts +11 -0
- package/dist/gateway-model-candidates.generated.d.ts.map +1 -0
- package/dist/gateway-model-candidates.generated.js +64 -0
- package/dist/login.d.ts +9 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +112 -0
- package/dist/model-catalog.d.ts +42 -0
- package/dist/model-catalog.d.ts.map +1 -0
- package/dist/model-catalog.js +898 -0
- package/dist/models.d.ts +15 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +196 -0
- package/dist/open-browser.d.ts +2 -0
- package/dist/open-browser.d.ts.map +1 -0
- package/dist/open-browser.js +37 -0
- package/dist/subscription.d.ts +5 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +216 -0
- package/dist/video-model-catalog.d.ts +52 -0
- package/dist/video-model-catalog.d.ts.map +1 -0
- package/dist/video-model-catalog.js +245 -0
- package/package.json +68 -5
- package/src/cli.ts +998 -0
- package/src/credentials.ts +126 -0
- package/src/login.ts +163 -0
- package/src/model-catalog.ts +968 -0
- package/src/models.ts +258 -0
- package/src/open-browser.ts +48 -0
- package/src/subscription.ts +298 -0
- package/src/video-model-catalog.ts +302 -0
- package/index.js +0 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Main CLI entrypoint for egaki - AI image and video generation.
|
|
3
|
+
// Uses the Vercel AI SDK for image generation across multiple providers.
|
|
4
|
+
// Designed to be called by agents and humans alike.
|
|
5
|
+
//
|
|
6
|
+
// Two generation paths depending on model:
|
|
7
|
+
// - imagen-* models → generateImage() with google.image()
|
|
8
|
+
// - all other models → generateText() with responseModalities: ['IMAGE']
|
|
9
|
+
// The CLI auto-detects which path to use based on model ID prefix.
|
|
10
|
+
import { goke } from 'goke';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import dedent from 'string-dedent';
|
|
13
|
+
import { generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, } from 'ai';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import pc from 'picocolors';
|
|
17
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
18
|
+
import { injectCredentialsToEnv, PROVIDERS } from './credentials.js';
|
|
19
|
+
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, } from './models.js';
|
|
20
|
+
import { VIDEO_CATALOG } from './video-model-catalog.js';
|
|
21
|
+
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
22
|
+
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
23
|
+
const cli = goke('egaki');
|
|
24
|
+
process.title = 'egaki';
|
|
25
|
+
// Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
|
|
26
|
+
// The AI SDK errors include request bodies, response headers, and other noisy
|
|
27
|
+
// properties that get dumped by Node's default handler. We strip those and just
|
|
28
|
+
// print the message + stack trace.
|
|
29
|
+
process.on('uncaughtException', (err) => {
|
|
30
|
+
console.error(pc.red(err.message));
|
|
31
|
+
if (err.stack) {
|
|
32
|
+
const lines = err.stack.split('\n');
|
|
33
|
+
const stackOnly = lines.filter((l) => l.trimStart().startsWith('at '));
|
|
34
|
+
if (stackOnly.length > 0) {
|
|
35
|
+
console.error(pc.dim(stackOnly.join('\n')));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
// ─── login command ───────────────────────────────────────────────────────────
|
|
41
|
+
cli
|
|
42
|
+
.command('login', dedent `
|
|
43
|
+
Configure API keys for image generation providers.
|
|
44
|
+
Interactive mode: shows a provider picker and secure key input.
|
|
45
|
+
Non-interactive mode: pass --provider and --key flags, or pipe key via stdin.
|
|
46
|
+
Keys are saved to ~/.config/egaki/credentials.json (mode 0600).
|
|
47
|
+
`)
|
|
48
|
+
.option('-p, --provider [name]', z
|
|
49
|
+
.string()
|
|
50
|
+
.describe(`Provider name for non-interactive login (${Object.keys(PROVIDERS).join(', ')})`))
|
|
51
|
+
.option('-k, --key [key]', z.string().describe('API key value for non-interactive login'))
|
|
52
|
+
.option('--show', 'Show which providers are configured and their status')
|
|
53
|
+
.option('--remove [provider]', z.string().describe('Remove the stored key for a provider'))
|
|
54
|
+
.example('# Interactive login (pick provider, paste key)')
|
|
55
|
+
.example('egaki login')
|
|
56
|
+
.example('# Non-interactive login with flags')
|
|
57
|
+
.example('egaki login --provider google --key AIza...')
|
|
58
|
+
.example('# Pipe key from stdin (useful in CI/scripts)')
|
|
59
|
+
.example('echo "AIza..." | egaki login --provider google')
|
|
60
|
+
.example('# Show configured providers')
|
|
61
|
+
.example('egaki login --show')
|
|
62
|
+
.example('# Remove a stored key')
|
|
63
|
+
.example('egaki login --remove google')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
if (options.show) {
|
|
66
|
+
showLoginStatus();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (options.remove) {
|
|
70
|
+
removeLogin(options.remove);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Non-interactive: --provider + --key or stdin
|
|
74
|
+
if (options.provider) {
|
|
75
|
+
const key = options.key || (await readKeyFromStdin());
|
|
76
|
+
loginNonInteractive({ provider: options.provider, key });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Interactive mode
|
|
80
|
+
await loginInteractive();
|
|
81
|
+
});
|
|
82
|
+
// ─── subscribe command ───────────────────────────────────────────────────────
|
|
83
|
+
cli
|
|
84
|
+
.command('subscribe', dedent `
|
|
85
|
+
Subscribe to Egaki for access to all image models with a single API key.
|
|
86
|
+
You can also use your own provider keys (Google/OpenAI/Replicate/Fal)
|
|
87
|
+
via 'egaki login --provider <name> --key <key>' if you prefer BYOK.
|
|
88
|
+
Egaki subscription avoids managing one key per provider.
|
|
89
|
+
Three plans: Starter ($9/mo, 100 credits), Pro ($29/mo, 500 credits),
|
|
90
|
+
Unlimited ($99/mo, 2000 credits). One credit ≈ one standard image.
|
|
91
|
+
Interactive mode: pick a plan and get a checkout URL (email prefill optional).
|
|
92
|
+
Non-interactive: --email is optional and only pre-fills checkout.
|
|
93
|
+
`)
|
|
94
|
+
.option('-e, --email [email]', z.string().describe('Optional email prefill for checkout (skips interactive prompt)'))
|
|
95
|
+
.option('--plan [plan]', z.string().describe('Plan ID: starter, pro, or unlimited (default: pro)'))
|
|
96
|
+
.example('# Interactive subscribe')
|
|
97
|
+
.example('egaki subscribe')
|
|
98
|
+
.example('# Non-interactive (for agents)')
|
|
99
|
+
.example('egaki subscribe --email user@example.com --plan pro')
|
|
100
|
+
.example('# Non-interactive without email prefill')
|
|
101
|
+
.example('egaki subscribe --plan pro')
|
|
102
|
+
.action(async (options) => {
|
|
103
|
+
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
104
|
+
if (!isTTY || options.email || options.plan) {
|
|
105
|
+
subscribeNonInteractive(options.email, options.plan);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await subscribeInteractive();
|
|
109
|
+
});
|
|
110
|
+
// ─── unsubscribe command ─────────────────────────────────────────────────────
|
|
111
|
+
cli
|
|
112
|
+
.command('unsubscribe', dedent `
|
|
113
|
+
Cancel your Egaki subscription. Uses the stored API key to identify
|
|
114
|
+
the subscription. You can resubscribe anytime with 'egaki subscribe'.
|
|
115
|
+
`)
|
|
116
|
+
.example('egaki unsubscribe')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
await unsubscribe();
|
|
119
|
+
});
|
|
120
|
+
// ─── usage command ───────────────────────────────────────────────────────────
|
|
121
|
+
cli
|
|
122
|
+
.command('usage', dedent `
|
|
123
|
+
Show your current Egaki credit usage for this billing period.
|
|
124
|
+
Displays plan, credits used, credits remaining, and period info.
|
|
125
|
+
`)
|
|
126
|
+
.example('egaki usage')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
await showUsage();
|
|
129
|
+
});
|
|
130
|
+
// ─── image command ───────────────────────────────────────────────────────────
|
|
131
|
+
cli
|
|
132
|
+
.command('image <prompt>', dedent `
|
|
133
|
+
Generate images from a text prompt using AI models.
|
|
134
|
+
Supports Imagen models (dedicated image generation) and Gemini
|
|
135
|
+
multimodal models (text+image output). The model type is auto-detected
|
|
136
|
+
from the model ID: imagen-* uses the image API, everything else uses
|
|
137
|
+
the text API with image output enabled.
|
|
138
|
+
`)
|
|
139
|
+
.option('-m, --model [model]', z.enum(IMAGE_MODELS).default(DEFAULT_MODEL).describe('Model ID for generation'))
|
|
140
|
+
.option('-o, --output [path]', z
|
|
141
|
+
.string()
|
|
142
|
+
.default('egaki-output.png')
|
|
143
|
+
.describe('Output file path (index suffix added when generating multiple)'))
|
|
144
|
+
.option('-n, --count [n]', z.number().default(1).describe('Number of images to generate'))
|
|
145
|
+
.option('--aspect-ratio [ratio]', z
|
|
146
|
+
.string()
|
|
147
|
+
.describe('Aspect ratio for the generated image. Imagen supports: 1:1, 3:4, 4:3, 9:16, 16:9. Gemini supports additional ratios: 2:3, 3:2, 4:5, 5:4, 21:9'))
|
|
148
|
+
.option('--seed [seed]', z.number().describe('Seed for reproducible generation. Same seed + same prompt = same image'))
|
|
149
|
+
.option('--image-size [size]', z
|
|
150
|
+
.enum(['1K', '2K', '4K'])
|
|
151
|
+
.describe('Output resolution for Gemini text-model image generation. Only applies to gemini-*-image* models'))
|
|
152
|
+
.option('-i, --input [file]', z
|
|
153
|
+
.array(z.string())
|
|
154
|
+
.describe('Reference image for editing or variations (repeatable). Accepts local file paths or URLs (http/https). Pass one or more images along with a text prompt to edit them'))
|
|
155
|
+
.option('--mask [file]', z
|
|
156
|
+
.string()
|
|
157
|
+
.describe('Mask image for inpainting. Accepts a local file path or URL (http/https). White areas in the mask are replaced with generated content. Used together with --input'))
|
|
158
|
+
.option('--allow-people', 'Allow generating images of people (Imagen blocks people by default)')
|
|
159
|
+
.option('--json', 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)')
|
|
160
|
+
.option('--stdout', 'Write raw image bytes to stdout instead of saving to a file. Useful for piping to other tools')
|
|
161
|
+
.example('# Generate a simple image')
|
|
162
|
+
.example('egaki image "a sunset over mars"')
|
|
163
|
+
.example('# Use a specific model with aspect ratio')
|
|
164
|
+
.example('egaki image "cyberpunk city at night" -m imagen-4.0-ultra-generate-001 --aspect-ratio 16:9')
|
|
165
|
+
.example('# Edit an existing image')
|
|
166
|
+
.example('egaki image "add a wizard hat to the cat" --input cat.jpg -o cat-wizard.png')
|
|
167
|
+
.example('# Edit an image from a URL')
|
|
168
|
+
.example('egaki image "make it pop art" --input https://example.com/photo.jpg')
|
|
169
|
+
.example('# Inpainting with a mask')
|
|
170
|
+
.example('egaki image "fill with flowers" --input photo.jpg --mask mask.png')
|
|
171
|
+
.example('# Generate with Gemini multimodal at 4K')
|
|
172
|
+
.example('egaki image "dreamy landscape" -m gemini-2.5-flash-image --image-size 4K')
|
|
173
|
+
.example('# Generate multiple images')
|
|
174
|
+
.example('egaki image "abstract art" -n 4 -o art.png')
|
|
175
|
+
.example('# Pipe to another tool')
|
|
176
|
+
.example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
|
|
177
|
+
.action(async (prompt, options) => {
|
|
178
|
+
// Inject stored API keys as env vars before calling the AI SDK
|
|
179
|
+
injectCredentialsToEnv();
|
|
180
|
+
const model = options.model;
|
|
181
|
+
const outputPath = options.output;
|
|
182
|
+
const config = getModelConfig(model);
|
|
183
|
+
if (!options.stdout) {
|
|
184
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
185
|
+
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
186
|
+
console.error(pc.dim(config.strategy === 'image'
|
|
187
|
+
? 'Mode: Image API (generateImage)'
|
|
188
|
+
: 'Mode: Text+Image (generateText)'));
|
|
189
|
+
}
|
|
190
|
+
const inputImages = await readInputImages(options.input);
|
|
191
|
+
const maskImage = options.mask
|
|
192
|
+
? await readInputSource(options.mask)
|
|
193
|
+
: undefined;
|
|
194
|
+
if (config.strategy === 'image') {
|
|
195
|
+
await generateWithImageModel({
|
|
196
|
+
prompt,
|
|
197
|
+
model,
|
|
198
|
+
outputPath,
|
|
199
|
+
count: options.count,
|
|
200
|
+
aspectRatio: options.aspectRatio,
|
|
201
|
+
seed: options.seed,
|
|
202
|
+
inputImages,
|
|
203
|
+
maskImage,
|
|
204
|
+
allowPeople: options.allowPeople || false,
|
|
205
|
+
json: options.json || false,
|
|
206
|
+
stdout: options.stdout || false,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await generateWithTextModel({
|
|
211
|
+
prompt,
|
|
212
|
+
model,
|
|
213
|
+
outputPath,
|
|
214
|
+
inputImages,
|
|
215
|
+
imageSize: options.imageSize,
|
|
216
|
+
aspectRatio: options.aspectRatio,
|
|
217
|
+
json: options.json || false,
|
|
218
|
+
stdout: options.stdout || false,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// ─── video command ───────────────────────────────────────────────────────────
|
|
223
|
+
cli
|
|
224
|
+
.command('video <prompt>', dedent `
|
|
225
|
+
Generate videos from a text prompt (or image+text prompt for models that
|
|
226
|
+
support image-to-video). Uses AI SDK experimental_generateVideo under the hood.
|
|
227
|
+
|
|
228
|
+
Agent note: video generation can be slow. When invoking this command from
|
|
229
|
+
automation, use a command timeout of at least 5 minutes.
|
|
230
|
+
`)
|
|
231
|
+
.option('-m, --model [model]', z.enum(VIDEO_MODELS).default(DEFAULT_VIDEO_MODEL).describe('Video model ID for generation'))
|
|
232
|
+
.option('-o, --output [path]', z
|
|
233
|
+
.string()
|
|
234
|
+
.default('egaki-output.mp4')
|
|
235
|
+
.describe('Output file path (index suffix added when generating multiple)'))
|
|
236
|
+
.option('-n, --count [n]', z.number().default(1).describe('Number of videos to generate'))
|
|
237
|
+
.option('--aspect-ratio [ratio]', z.string().describe('Video aspect ratio in WIDTH:HEIGHT format (e.g. 16:9, 9:16)'))
|
|
238
|
+
.option('--resolution [resolution]', z
|
|
239
|
+
.string()
|
|
240
|
+
.describe('Video resolution in WIDTHxHEIGHT or provider format (e.g. 1280x720, 720p)'))
|
|
241
|
+
.option('--duration [seconds]', z.number().describe('Video duration in seconds (provider/model-specific limits apply)'))
|
|
242
|
+
.option('--fps [fps]', z.number().describe('Frames per second for video models that support fps override'))
|
|
243
|
+
.option('--seed [seed]', z.number().describe('Seed for reproducible video generation (model support varies)'))
|
|
244
|
+
.option('-i, --input [file]', z
|
|
245
|
+
.string()
|
|
246
|
+
.describe('Optional reference image for image-to-video. Accepts local file path or URL (http/https)'))
|
|
247
|
+
.option('--json', 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)')
|
|
248
|
+
.option('--stdout', 'Write raw video bytes to stdout instead of saving to a file. Useful for piping to other tools')
|
|
249
|
+
.example('# Generate a video')
|
|
250
|
+
.example('egaki video "A paper airplane gliding through clouds" -o airplane.mp4')
|
|
251
|
+
.example('# Generate with Veo model + duration')
|
|
252
|
+
.example('egaki video "cinematic rainy street at night" -m veo-3.1-fast-generate-001 --duration 6')
|
|
253
|
+
.example('# Image-to-video (model support required)')
|
|
254
|
+
.example('egaki video "animate subtle camera pan" --model luma-ray-2 --input frame.png -o animated.mp4')
|
|
255
|
+
.example('# Generate multiple videos')
|
|
256
|
+
.example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
|
|
257
|
+
.action(async (prompt, options) => {
|
|
258
|
+
injectCredentialsToEnv();
|
|
259
|
+
const model = options.model;
|
|
260
|
+
const outputPath = options.output;
|
|
261
|
+
const config = getModelConfig(model);
|
|
262
|
+
if (config.strategy !== 'video') {
|
|
263
|
+
console.error(pc.red(`Model ${model} is not a video model`));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (!options.stdout) {
|
|
267
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
268
|
+
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
269
|
+
console.error(pc.dim('Mode: Video API (experimental_generateVideo)'));
|
|
270
|
+
}
|
|
271
|
+
const inputImage = options.input
|
|
272
|
+
? await readInputSource(options.input)
|
|
273
|
+
: undefined;
|
|
274
|
+
await generateWithVideoModel({
|
|
275
|
+
prompt,
|
|
276
|
+
model,
|
|
277
|
+
outputPath,
|
|
278
|
+
count: options.count,
|
|
279
|
+
aspectRatio: options.aspectRatio,
|
|
280
|
+
resolution: options.resolution,
|
|
281
|
+
duration: options.duration,
|
|
282
|
+
fps: options.fps,
|
|
283
|
+
seed: options.seed,
|
|
284
|
+
inputImage,
|
|
285
|
+
json: options.json || false,
|
|
286
|
+
stdout: options.stdout || false,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// ─── models command ──────────────────────────────────────────────────────────
|
|
290
|
+
cli
|
|
291
|
+
.command('models', dedent `
|
|
292
|
+
List all supported image generation models with pricing, features,
|
|
293
|
+
and provider info. Output is YAML for easy reading and piping.
|
|
294
|
+
`)
|
|
295
|
+
.option('-p, --provider [provider]', z.string().describe('Filter models by provider name (e.g. google, openai, replicate, fal)'))
|
|
296
|
+
.option('--type [type]', z
|
|
297
|
+
.enum(['all', 'image', 'video'])
|
|
298
|
+
.default('all')
|
|
299
|
+
.describe('Filter by model type: image (image+text-image), video, or all'))
|
|
300
|
+
.option('--json', 'Output as JSON instead of YAML')
|
|
301
|
+
.action(async (options) => {
|
|
302
|
+
const { CATALOG } = await import('./model-catalog.js');
|
|
303
|
+
const yaml = await import('js-yaml');
|
|
304
|
+
let models = options.type === 'video'
|
|
305
|
+
? VIDEO_CATALOG
|
|
306
|
+
: options.type === 'image'
|
|
307
|
+
? CATALOG
|
|
308
|
+
: [...CATALOG, ...VIDEO_CATALOG];
|
|
309
|
+
if (options.provider) {
|
|
310
|
+
models = models.filter((m) => m.provider === options.provider);
|
|
311
|
+
if (models.length === 0) {
|
|
312
|
+
console.error(pc.red(`No models found for provider: ${options.provider}`));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const output = models.map((m) => ({
|
|
317
|
+
id: m.id,
|
|
318
|
+
name: m.name,
|
|
319
|
+
...(m.description ? { description: m.description } : {}),
|
|
320
|
+
provider: m.provider,
|
|
321
|
+
strategy: m.strategy,
|
|
322
|
+
released: m.released,
|
|
323
|
+
cost: formatCatalogCost(m.cost),
|
|
324
|
+
features: m.strategy === 'video'
|
|
325
|
+
? {
|
|
326
|
+
textToVideo: m.features.textToVideo,
|
|
327
|
+
imageToVideo: m.features.imageToVideo,
|
|
328
|
+
capabilities: m.features.capabilities.join(', ') || 'none',
|
|
329
|
+
seed: m.features.seed,
|
|
330
|
+
multipleVideos: m.features.multipleVideos,
|
|
331
|
+
aspectRatios: m.features.aspectRatios?.join(', ') || 'none',
|
|
332
|
+
resolutions: m.features.resolutions?.join(', ') || 'unknown',
|
|
333
|
+
durationRangeSec: m.features.durationRangeSec
|
|
334
|
+
? `${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}`
|
|
335
|
+
: 'unknown',
|
|
336
|
+
}
|
|
337
|
+
: {
|
|
338
|
+
editing: m.features.editing,
|
|
339
|
+
inpainting: m.features.inpainting,
|
|
340
|
+
seed: m.features.seed,
|
|
341
|
+
multipleImages: m.features.multipleImages,
|
|
342
|
+
aspectRatios: m.features.aspectRatios.join(', ') || 'none',
|
|
343
|
+
...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
|
|
344
|
+
},
|
|
345
|
+
}));
|
|
346
|
+
if (options.json) {
|
|
347
|
+
console.log(JSON.stringify(output, null, 2));
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log(yaml.dump(output, { lineWidth: 120, noRefs: true }));
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
cli.help();
|
|
354
|
+
cli.version(pkg.version);
|
|
355
|
+
cli.parse();
|
|
356
|
+
function isUrl(input) {
|
|
357
|
+
return /^https?:\/\//i.test(input);
|
|
358
|
+
}
|
|
359
|
+
async function readInputSource(input) {
|
|
360
|
+
if (isUrl(input)) {
|
|
361
|
+
console.error(pc.dim(`Fetching ${input}...`));
|
|
362
|
+
const res = await fetch(input);
|
|
363
|
+
if (!res.ok) {
|
|
364
|
+
console.error(pc.red(`Failed to fetch ${input}: ${res.status} ${res.statusText}`));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
368
|
+
}
|
|
369
|
+
const resolved = path.resolve(input);
|
|
370
|
+
if (!fs.existsSync(resolved)) {
|
|
371
|
+
console.error(pc.red(`File not found: ${resolved}`));
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
return new Uint8Array(fs.readFileSync(resolved));
|
|
375
|
+
}
|
|
376
|
+
async function readInputImages(inputs) {
|
|
377
|
+
if (!inputs || inputs.length === 0) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
return Promise.all(inputs.map((f) => readInputSource(f)));
|
|
381
|
+
}
|
|
382
|
+
// Generate using the dedicated generateImage API (Imagen models)
|
|
383
|
+
async function generateWithImageModel({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, allowPeople, json, stdout, }) {
|
|
384
|
+
// Build the prompt: plain string or multimodal with reference images
|
|
385
|
+
const imagePrompt = inputImages.length > 0
|
|
386
|
+
? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
|
|
387
|
+
: prompt;
|
|
388
|
+
const imageModel = await createImageModel(model);
|
|
389
|
+
if (!stdout) {
|
|
390
|
+
console.error(pc.cyan('Generating...'));
|
|
391
|
+
}
|
|
392
|
+
const result = await aiGenerateImage({
|
|
393
|
+
model: imageModel,
|
|
394
|
+
prompt: imagePrompt,
|
|
395
|
+
n: count,
|
|
396
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
397
|
+
...(seed !== undefined ? { seed } : {}),
|
|
398
|
+
providerOptions: {
|
|
399
|
+
google: {
|
|
400
|
+
...(allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
401
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
if (stdout) {
|
|
406
|
+
// Write first image to stdout for piping
|
|
407
|
+
const image = result.images[0];
|
|
408
|
+
if (image) {
|
|
409
|
+
process.stdout.write(Buffer.from(image.uint8Array));
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const savedFiles = [];
|
|
414
|
+
for (let i = 0; i < result.images.length; i++) {
|
|
415
|
+
const image = result.images[i];
|
|
416
|
+
const ext = extensionFromMediaType(image.mediaType);
|
|
417
|
+
const filePath = result.images.length === 1
|
|
418
|
+
? ensureExtension(outputPath, ext)
|
|
419
|
+
: insertIndex(outputPath, i, ext);
|
|
420
|
+
fs.writeFileSync(filePath, image.uint8Array);
|
|
421
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
422
|
+
savedFiles.push(filePath);
|
|
423
|
+
}
|
|
424
|
+
const config = getModelConfig(model);
|
|
425
|
+
const cost = calculateCost(config.cost, result.usage, result.images.length);
|
|
426
|
+
if (cost != null) {
|
|
427
|
+
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
428
|
+
}
|
|
429
|
+
if (json) {
|
|
430
|
+
const output = {
|
|
431
|
+
model,
|
|
432
|
+
files: savedFiles,
|
|
433
|
+
count: result.images.length,
|
|
434
|
+
cost,
|
|
435
|
+
usage: result.usage,
|
|
436
|
+
warnings: result.warnings,
|
|
437
|
+
};
|
|
438
|
+
console.log(JSON.stringify(output, null, 2));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Generate using generateText with responseModalities (Gemini multimodal models)
|
|
442
|
+
async function generateWithTextModel({ prompt, model, outputPath, inputImages, imageSize, aspectRatio, json, stdout, }) {
|
|
443
|
+
const textModel = await createTextModel(model);
|
|
444
|
+
if (!stdout) {
|
|
445
|
+
console.error(pc.cyan('Generating...'));
|
|
446
|
+
}
|
|
447
|
+
// Build messages: if we have input images, create a multimodal prompt
|
|
448
|
+
const messages = inputImages.length > 0
|
|
449
|
+
? [
|
|
450
|
+
{
|
|
451
|
+
role: 'user',
|
|
452
|
+
content: [
|
|
453
|
+
{ type: 'text', text: prompt },
|
|
454
|
+
...inputImages.map((img) => ({
|
|
455
|
+
type: 'image',
|
|
456
|
+
image: img,
|
|
457
|
+
})),
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
]
|
|
461
|
+
: undefined;
|
|
462
|
+
const result = await generateText({
|
|
463
|
+
model: textModel,
|
|
464
|
+
...(messages ? { messages } : { prompt }),
|
|
465
|
+
providerOptions: {
|
|
466
|
+
google: {
|
|
467
|
+
responseModalities: ['TEXT', 'IMAGE'],
|
|
468
|
+
...(imageSize || aspectRatio
|
|
469
|
+
? {
|
|
470
|
+
imageConfig: {
|
|
471
|
+
...(imageSize ? { imageSize } : {}),
|
|
472
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
: {}),
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
const imageFiles = result.files.filter((f) => {
|
|
480
|
+
return f.mediaType.startsWith('image/');
|
|
481
|
+
});
|
|
482
|
+
if (imageFiles.length === 0) {
|
|
483
|
+
console.error(pc.red('No images generated.'));
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
if (stdout) {
|
|
487
|
+
const file = imageFiles[0];
|
|
488
|
+
if (file) {
|
|
489
|
+
process.stdout.write(Buffer.from(file.uint8Array));
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const savedFiles = [];
|
|
494
|
+
for (let i = 0; i < imageFiles.length; i++) {
|
|
495
|
+
const file = imageFiles[i];
|
|
496
|
+
const ext = extensionFromMediaType(file.mediaType);
|
|
497
|
+
const filePath = imageFiles.length === 1
|
|
498
|
+
? ensureExtension(outputPath, ext)
|
|
499
|
+
: insertIndex(outputPath, i, ext);
|
|
500
|
+
fs.writeFileSync(filePath, file.uint8Array);
|
|
501
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
502
|
+
savedFiles.push(filePath);
|
|
503
|
+
}
|
|
504
|
+
const config = getModelConfig(model);
|
|
505
|
+
const cost = calculateCost(config.cost, result.usage, imageFiles.length);
|
|
506
|
+
if (cost != null) {
|
|
507
|
+
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
508
|
+
}
|
|
509
|
+
if (result.text && !json) {
|
|
510
|
+
console.error(pc.dim(result.text));
|
|
511
|
+
}
|
|
512
|
+
if (json) {
|
|
513
|
+
const output = {
|
|
514
|
+
model,
|
|
515
|
+
files: savedFiles,
|
|
516
|
+
count: imageFiles.length,
|
|
517
|
+
text: result.text || null,
|
|
518
|
+
cost,
|
|
519
|
+
usage: result.usage,
|
|
520
|
+
};
|
|
521
|
+
console.log(JSON.stringify(output, null, 2));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Generate using experimental_generateVideo (video models)
|
|
525
|
+
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
|
|
526
|
+
const videoModel = await createVideoModel(model);
|
|
527
|
+
if (!stdout) {
|
|
528
|
+
console.error(pc.cyan('Generating...'));
|
|
529
|
+
}
|
|
530
|
+
const result = await aiGenerateVideo({
|
|
531
|
+
model: videoModel,
|
|
532
|
+
prompt: inputImage
|
|
533
|
+
? { image: inputImage, text: prompt }
|
|
534
|
+
: prompt,
|
|
535
|
+
n: count,
|
|
536
|
+
...(aspectRatio ? { aspectRatio: aspectRatio } : {}),
|
|
537
|
+
...(resolution ? { resolution: resolution } : {}),
|
|
538
|
+
...(duration != null ? { duration } : {}),
|
|
539
|
+
...(fps != null ? { fps } : {}),
|
|
540
|
+
...(seed != null ? { seed } : {}),
|
|
541
|
+
});
|
|
542
|
+
if (stdout) {
|
|
543
|
+
const video = result.videos[0];
|
|
544
|
+
if (video) {
|
|
545
|
+
process.stdout.write(Buffer.from(video.uint8Array));
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const savedFiles = [];
|
|
550
|
+
for (let i = 0; i < result.videos.length; i++) {
|
|
551
|
+
const video = result.videos[i];
|
|
552
|
+
const ext = extensionFromMediaType(video.mediaType);
|
|
553
|
+
const filePath = result.videos.length === 1
|
|
554
|
+
? ensureExtension(outputPath, ext)
|
|
555
|
+
: insertIndex(outputPath, i, ext);
|
|
556
|
+
fs.writeFileSync(filePath, video.uint8Array);
|
|
557
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
558
|
+
savedFiles.push(filePath);
|
|
559
|
+
}
|
|
560
|
+
const config = getModelConfig(model);
|
|
561
|
+
const cost = calculateCost(config.cost, {
|
|
562
|
+
videosGenerated: result.videos.length,
|
|
563
|
+
durationSeconds: duration,
|
|
564
|
+
resolution,
|
|
565
|
+
}, result.videos.length);
|
|
566
|
+
if (cost != null) {
|
|
567
|
+
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
568
|
+
}
|
|
569
|
+
if (json) {
|
|
570
|
+
const output = {
|
|
571
|
+
model,
|
|
572
|
+
files: savedFiles,
|
|
573
|
+
count: result.videos.length,
|
|
574
|
+
cost,
|
|
575
|
+
warnings: result.warnings,
|
|
576
|
+
responses: result.responses,
|
|
577
|
+
};
|
|
578
|
+
console.log(JSON.stringify(output, null, 2));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// ─── cost helpers ────────────────────────────────────────────────────────────
|
|
582
|
+
function calculateCost(cost, usage, count = 1) {
|
|
583
|
+
if (cost.type === 'per-image') {
|
|
584
|
+
return cost.perImage * count;
|
|
585
|
+
}
|
|
586
|
+
if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
|
|
587
|
+
return ((usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000);
|
|
588
|
+
}
|
|
589
|
+
if (cost.type === 'per-video-second') {
|
|
590
|
+
const durationSec = usage.durationSeconds ?? cost.defaultDurationSec;
|
|
591
|
+
const resolution = normalizeResolutionKey(usage.resolution);
|
|
592
|
+
const tier = cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
|
|
593
|
+
cost.tiers[0];
|
|
594
|
+
if (!tier) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return tier.costPerSecond * durationSec * count;
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
function formatCatalogCost(cost) {
|
|
602
|
+
if (cost.type === 'per-image') {
|
|
603
|
+
return `$${cost.perImage}/image`;
|
|
604
|
+
}
|
|
605
|
+
if (cost.type === 'per-token') {
|
|
606
|
+
return `$${cost.inputPerM}/M input, $${cost.outputPerM}/M output`;
|
|
607
|
+
}
|
|
608
|
+
if (cost.type === 'per-video-second') {
|
|
609
|
+
const tierText = cost.tiers
|
|
610
|
+
.map((t) => {
|
|
611
|
+
const parts = [
|
|
612
|
+
t.resolution,
|
|
613
|
+
t.mode ? `mode=${t.mode}` : undefined,
|
|
614
|
+
t.audio != null ? `audio=${t.audio}` : undefined,
|
|
615
|
+
].filter(Boolean);
|
|
616
|
+
return parts.length > 0
|
|
617
|
+
? `$${t.costPerSecond}/s (${parts.join(', ')})`
|
|
618
|
+
: `$${t.costPerSecond}/s`;
|
|
619
|
+
})
|
|
620
|
+
.join('; ');
|
|
621
|
+
return `${tierText} (default duration ${cost.defaultDurationSec}s)`;
|
|
622
|
+
}
|
|
623
|
+
return 'unknown';
|
|
624
|
+
}
|
|
625
|
+
function formatCost(dollars) {
|
|
626
|
+
if (dollars < 0.01) {
|
|
627
|
+
return `$${dollars.toFixed(4)}`;
|
|
628
|
+
}
|
|
629
|
+
return `$${dollars.toFixed(2)}`;
|
|
630
|
+
}
|
|
631
|
+
function extensionFromMediaType(mediaType) {
|
|
632
|
+
const map = {
|
|
633
|
+
'image/png': '.png',
|
|
634
|
+
'image/jpeg': '.jpg',
|
|
635
|
+
'image/webp': '.webp',
|
|
636
|
+
'image/gif': '.gif',
|
|
637
|
+
'video/mp4': '.mp4',
|
|
638
|
+
'video/webm': '.webm',
|
|
639
|
+
'video/quicktime': '.mov',
|
|
640
|
+
};
|
|
641
|
+
return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
|
|
642
|
+
}
|
|
643
|
+
function normalizeResolutionKey(input) {
|
|
644
|
+
if (!input)
|
|
645
|
+
return undefined;
|
|
646
|
+
const normalized = input.trim().toLowerCase();
|
|
647
|
+
if (normalized === '1920x1080')
|
|
648
|
+
return '1080p';
|
|
649
|
+
if (normalized === '1280x720')
|
|
650
|
+
return '720p';
|
|
651
|
+
if (normalized === '854x480' || normalized === '848x480')
|
|
652
|
+
return '480p';
|
|
653
|
+
return normalized;
|
|
654
|
+
}
|
|
655
|
+
function ensureExtension(filePath, ext) {
|
|
656
|
+
const parsed = path.parse(filePath);
|
|
657
|
+
if (parsed.ext) {
|
|
658
|
+
return filePath;
|
|
659
|
+
}
|
|
660
|
+
return filePath + ext;
|
|
661
|
+
}
|
|
662
|
+
function insertIndex(filePath, index, ext) {
|
|
663
|
+
const parsed = path.parse(filePath);
|
|
664
|
+
const finalExt = parsed.ext || ext;
|
|
665
|
+
return path.join(parsed.dir, `${parsed.name}-${index}${finalExt}`);
|
|
666
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const PROVIDERS: Record<string, {
|
|
2
|
+
envVar: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
|
|
7
|
+
export type Credentials = Record<string, string>;
|
|
8
|
+
export declare function readCredentials(): Credentials;
|
|
9
|
+
export declare function writeCredentials(creds: Credentials): void;
|
|
10
|
+
export declare function saveProviderKey(provider: string, key: string): void;
|
|
11
|
+
export declare function removeProviderKey(provider: string): void;
|
|
12
|
+
export declare function getProviderKey(provider: string): string | undefined;
|
|
13
|
+
export declare function injectCredentialsToEnv(): void;
|
|
14
|
+
export declare function getKeyStatus(provider: string): {
|
|
15
|
+
available: boolean;
|
|
16
|
+
source: 'env' | 'stored' | 'none';
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,EAAE,MAAM,CAC5B,MAAM,EACN;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA2BhD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEhD,wBAAgB,eAAe,IAAI,WAAW,CAO7C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAW7C;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;CAClC,CAaA"}
|