egaki 0.0.2 → 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.
@@ -0,0 +1,285 @@
1
+ // Antigravity image generation bridge for egaki.
2
+ // Handles direct Antigravity OAuth requests without provider fallback.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+ import { execFileSync } from 'node:child_process';
7
+ import pc from 'picocolors';
8
+ import { getAntigravityAuth, saveAntigravityAuth } from './credentials.js';
9
+ import { getValidAntigravityAccessToken } from './antigravity-auth.js';
10
+ export async function generateWithAntigravityModel({ prompt, model, outputPath, inputImages, aspectRatio, count, json, stdout, }) {
11
+ const auth = getAntigravityAuth();
12
+ if (!auth) {
13
+ console.error(pc.red('Missing Antigravity OAuth login. Run: egaki login --provider antigravity'));
14
+ process.exit(1);
15
+ }
16
+ const token = await getValidAntigravityAccessToken(auth, (updated) => {
17
+ saveAntigravityAuth(updated);
18
+ });
19
+ if (token instanceof Error) {
20
+ console.error(pc.red(token.message));
21
+ process.exit(1);
22
+ }
23
+ if (!stdout) {
24
+ console.error(pc.cyan('Generating...'));
25
+ }
26
+ const actualModel = resolveAntigravityModel(model);
27
+ const projectCandidates = [
28
+ process.env['ANTIGRAVITY_PROJECT_ID'],
29
+ token.projectId,
30
+ ...getGcloudProjectIds(),
31
+ ].filter((value, index, array) => {
32
+ return Boolean(value) && array.indexOf(value) === index;
33
+ });
34
+ const userParts = [{ text: prompt }];
35
+ for (const image of inputImages) {
36
+ userParts.push({
37
+ inlineData: {
38
+ mimeType: detectImageMimeType(image),
39
+ data: Buffer.from(image).toString('base64'),
40
+ },
41
+ });
42
+ }
43
+ const baseRequest = {
44
+ sessionId: `egaki-${crypto.randomUUID()}`,
45
+ systemInstruction: {
46
+ role: 'user',
47
+ parts: [{ text: 'You are an AI image generator. Generate images based on user descriptions.' }],
48
+ },
49
+ contents: [
50
+ {
51
+ role: 'user',
52
+ parts: userParts,
53
+ },
54
+ ],
55
+ generationConfig: {
56
+ candidateCount: count,
57
+ imageConfig: {
58
+ aspectRatio: aspectRatio || '1:1',
59
+ },
60
+ },
61
+ safetySettings: [
62
+ { category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH' },
63
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH' },
64
+ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH' },
65
+ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
66
+ ],
67
+ };
68
+ const antigravityAttempts = projectCandidates.map((projectId) => {
69
+ return {
70
+ mode: `antigravity:${projectId}`,
71
+ endpoints: [
72
+ 'https://daily-cloudcode-pa.sandbox.googleapis.com',
73
+ 'https://autopush-cloudcode-pa.sandbox.googleapis.com',
74
+ 'https://cloudcode-pa.googleapis.com',
75
+ ],
76
+ payload: {
77
+ project: projectId,
78
+ model: actualModel,
79
+ requestType: 'agent',
80
+ userAgent: 'antigravity',
81
+ requestId: `agent-${crypto.randomUUID()}`,
82
+ request: baseRequest,
83
+ },
84
+ };
85
+ });
86
+ const attemptConfigs = antigravityAttempts;
87
+ let lastError = 'Unknown Antigravity API error';
88
+ let responseBody;
89
+ let successfulProjectId;
90
+ for (const attempt of attemptConfigs) {
91
+ for (const endpoint of attempt.endpoints) {
92
+ const response = await fetch(`${endpoint}/v1internal:generateContent`, {
93
+ method: 'POST',
94
+ headers: {
95
+ Authorization: `Bearer ${token.accessToken}`,
96
+ 'Content-Type': 'application/json',
97
+ 'User-Agent': 'google-api-nodejs-client/9.15.1',
98
+ 'X-Goog-Api-Client': 'gl-node/22.17.0',
99
+ 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI',
100
+ },
101
+ body: JSON.stringify(attempt.payload),
102
+ });
103
+ const text = await response.text();
104
+ if (!response.ok) {
105
+ lastError = formatAntigravityError({
106
+ mode: attempt.mode,
107
+ status: response.status,
108
+ bodyText: text,
109
+ });
110
+ continue;
111
+ }
112
+ responseBody = JSON.parse(text);
113
+ if (typeof attempt.payload.project === 'string' && attempt.payload.project.length > 0) {
114
+ successfulProjectId = attempt.payload.project;
115
+ }
116
+ break;
117
+ }
118
+ if (responseBody) {
119
+ break;
120
+ }
121
+ }
122
+ if (!responseBody) {
123
+ console.error(pc.red(lastError));
124
+ process.exit(1);
125
+ }
126
+ if (successfulProjectId && successfulProjectId !== auth.projectId) {
127
+ saveAntigravityAuth({ ...auth, projectId: successfulProjectId });
128
+ }
129
+ const generatedImages = extractAntigravityImages(responseBody);
130
+ if (generatedImages.length === 0) {
131
+ console.error(pc.red('No images generated by Antigravity response'));
132
+ process.exit(1);
133
+ }
134
+ if (stdout) {
135
+ process.stdout.write(generatedImages[0].bytes);
136
+ return;
137
+ }
138
+ const savedFiles = [];
139
+ for (let i = 0; i < generatedImages.length; i++) {
140
+ const generated = generatedImages[i];
141
+ const filePath = generatedImages.length === 1
142
+ ? ensureExtension(outputPath, extensionFromMediaType(generated.mediaType))
143
+ : insertIndex(outputPath, i, extensionFromMediaType(generated.mediaType));
144
+ fs.writeFileSync(filePath, generated.bytes);
145
+ console.error(pc.green(`Saved: ${filePath}`));
146
+ savedFiles.push(filePath);
147
+ }
148
+ if (json) {
149
+ console.log(JSON.stringify({
150
+ model,
151
+ files: savedFiles,
152
+ count: savedFiles.length,
153
+ provider: 'antigravity',
154
+ projectId: token.projectId,
155
+ }, null, 2));
156
+ }
157
+ }
158
+ function resolveAntigravityModel(modelId) {
159
+ if (modelId.startsWith('antigravity-')) {
160
+ return modelId.slice('antigravity-'.length);
161
+ }
162
+ return modelId;
163
+ }
164
+ function getGcloudProjectIds() {
165
+ try {
166
+ const output = execFileSync('gcloud', ['projects', 'list', '--format=value(projectId)', '--limit=50'], {
167
+ encoding: 'utf8',
168
+ stdio: ['ignore', 'pipe', 'ignore'],
169
+ });
170
+ return output
171
+ .split('\n')
172
+ .map((line) => line.trim())
173
+ .filter((line) => line.length > 0);
174
+ }
175
+ catch {
176
+ return [];
177
+ }
178
+ }
179
+ function detectImageMimeType(bytes) {
180
+ if (bytes.length > 3 &&
181
+ bytes[0] === 0x89 &&
182
+ bytes[1] === 0x50 &&
183
+ bytes[2] === 0x4e &&
184
+ bytes[3] === 0x47) {
185
+ return 'image/png';
186
+ }
187
+ if (bytes.length > 2 && bytes[0] === 0xff && bytes[1] === 0xd8) {
188
+ return 'image/jpeg';
189
+ }
190
+ if (bytes.length > 12 &&
191
+ bytes[0] === 0x52 &&
192
+ bytes[1] === 0x49 &&
193
+ bytes[2] === 0x46 &&
194
+ bytes[3] === 0x46 &&
195
+ bytes[8] === 0x57 &&
196
+ bytes[9] === 0x45 &&
197
+ bytes[10] === 0x42 &&
198
+ bytes[11] === 0x50) {
199
+ return 'image/webp';
200
+ }
201
+ return 'image/png';
202
+ }
203
+ function extractAntigravityImages(payload) {
204
+ const root = payload && typeof payload === 'object' && 'response' in payload
205
+ ? payload.response
206
+ : payload;
207
+ if (!root || typeof root !== 'object') {
208
+ return [];
209
+ }
210
+ const candidates = root.candidates;
211
+ if (!candidates || candidates.length === 0) {
212
+ return [];
213
+ }
214
+ const images = [];
215
+ for (const candidate of candidates) {
216
+ for (const part of candidate.content?.parts || []) {
217
+ const data = part.inlineData?.data;
218
+ if (!data)
219
+ continue;
220
+ const mediaType = part.inlineData?.mimeType || 'image/png';
221
+ images.push({
222
+ bytes: new Uint8Array(Buffer.from(data, 'base64')),
223
+ mediaType,
224
+ });
225
+ }
226
+ }
227
+ return images;
228
+ }
229
+ function formatAntigravityError({ mode, status, bodyText, }) {
230
+ const payload = parseAntigravityErrorPayload(bodyText);
231
+ const message = payload?.error?.message || bodyText;
232
+ const metadata = payload?.error?.details?.find((detail) => detail.metadata)?.metadata;
233
+ const links = payload?.error?.details
234
+ ?.flatMap((detail) => detail.links || [])
235
+ .filter((link) => Boolean(link?.url));
236
+ const verificationUrl = metadata?.['validation_url']
237
+ || links?.find((link) => /verify/i.test(link.description || ''))?.url;
238
+ if (verificationUrl) {
239
+ return [
240
+ `${mode} API error ${status}: account verification required`,
241
+ `Verify your Google account here: ${verificationUrl}`,
242
+ `Backend message: ${message}`,
243
+ ].join('\n');
244
+ }
245
+ const activationUrl = metadata?.['activationUrl']
246
+ || metadata?.['activation_url']
247
+ || links?.find((link) => /activation|console|api/i.test(link.description || ''))?.url;
248
+ if (activationUrl) {
249
+ return [
250
+ `${mode} API error ${status}: API access not configured`,
251
+ `Enable required API here: ${activationUrl}`,
252
+ `Backend message: ${message}`,
253
+ ].join('\n');
254
+ }
255
+ return `${mode} API error ${status}: ${message}`;
256
+ }
257
+ function parseAntigravityErrorPayload(bodyText) {
258
+ try {
259
+ return JSON.parse(bodyText);
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ }
265
+ function extensionFromMediaType(mediaType) {
266
+ const map = {
267
+ 'image/png': '.png',
268
+ 'image/jpeg': '.jpg',
269
+ 'image/webp': '.webp',
270
+ 'image/gif': '.gif',
271
+ };
272
+ return map[mediaType] || '.png';
273
+ }
274
+ function ensureExtension(filePath, ext) {
275
+ const parsed = path.parse(filePath);
276
+ if (parsed.ext) {
277
+ return filePath;
278
+ }
279
+ return filePath + ext;
280
+ }
281
+ function insertIndex(filePath, index, ext) {
282
+ const parsed = path.parse(filePath);
283
+ const finalExt = parsed.ext || ext;
284
+ return path.join(parsed.dir, `${parsed.name}-${index}${finalExt}`);
285
+ }
package/dist/cli.js CHANGED
@@ -10,13 +10,14 @@
10
10
  import { goke } from 'goke';
11
11
  import { z } from 'zod';
12
12
  import dedent from 'string-dedent';
13
- import { generateImage as aiGenerateImage, generateText } from 'ai';
13
+ import { generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, } from 'ai';
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import pc from 'picocolors';
17
17
  import pkg from '../package.json' with { type: 'json' };
18
18
  import { injectCredentialsToEnv, PROVIDERS } from './credentials.js';
19
- import { IMAGE_MODELS, DEFAULT_MODEL, getModelConfig, createImageModel, createTextModel, } from './models.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';
20
21
  import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
21
22
  import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
22
23
  const cli = goke('egaki');
@@ -87,15 +88,17 @@ cli
87
88
  Egaki subscription avoids managing one key per provider.
88
89
  Three plans: Starter ($9/mo, 100 credits), Pro ($29/mo, 500 credits),
89
90
  Unlimited ($99/mo, 2000 credits). One credit ≈ one standard image.
90
- Interactive mode: pick a plan, enter email, get checkout URL.
91
- Non-interactive: pass --email to get the checkout URL directly.
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.
92
93
  `)
93
- .option('-e, --email [email]', z.string().describe('Email for the subscription (skips interactive prompt)'))
94
+ .option('-e, --email [email]', z.string().describe('Optional email prefill for checkout (skips interactive prompt)'))
94
95
  .option('--plan [plan]', z.string().describe('Plan ID: starter, pro, or unlimited (default: pro)'))
95
96
  .example('# Interactive subscribe')
96
97
  .example('egaki subscribe')
97
98
  .example('# Non-interactive (for agents)')
98
99
  .example('egaki subscribe --email user@example.com --plan pro')
100
+ .example('# Non-interactive without email prefill')
101
+ .example('egaki subscribe --plan pro')
99
102
  .action(async (options) => {
100
103
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
101
104
  if (!isTTY || options.email || options.plan) {
@@ -216,6 +219,73 @@ cli
216
219
  });
217
220
  }
218
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
+ });
219
289
  // ─── models command ──────────────────────────────────────────────────────────
220
290
  cli
221
291
  .command('models', dedent `
@@ -223,11 +293,19 @@ cli
223
293
  and provider info. Output is YAML for easy reading and piping.
224
294
  `)
225
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'))
226
300
  .option('--json', 'Output as JSON instead of YAML')
227
301
  .action(async (options) => {
228
302
  const { CATALOG } = await import('./model-catalog.js');
229
303
  const yaml = await import('js-yaml');
230
- let models = CATALOG;
304
+ let models = options.type === 'video'
305
+ ? VIDEO_CATALOG
306
+ : options.type === 'image'
307
+ ? CATALOG
308
+ : [...CATALOG, ...VIDEO_CATALOG];
231
309
  if (options.provider) {
232
310
  models = models.filter((m) => m.provider === options.provider);
233
311
  if (models.length === 0) {
@@ -242,17 +320,28 @@ cli
242
320
  provider: m.provider,
243
321
  strategy: m.strategy,
244
322
  released: m.released,
245
- cost: m.cost.type === 'per-image'
246
- ? `$${m.cost.perImage}/image`
247
- : `$${m.cost.inputPerM}/M input, $${m.cost.outputPerM}/M output`,
248
- features: {
249
- editing: m.features.editing,
250
- inpainting: m.features.inpainting,
251
- seed: m.features.seed,
252
- multipleImages: m.features.multipleImages,
253
- aspectRatios: m.features.aspectRatios.join(', ') || 'none',
254
- ...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
255
- },
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
+ },
256
345
  }));
257
346
  if (options.json) {
258
347
  console.log(JSON.stringify(output, null, 2));
@@ -432,16 +521,107 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
432
521
  console.log(JSON.stringify(output, null, 2));
433
522
  }
434
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
+ }
435
581
  // ─── cost helpers ────────────────────────────────────────────────────────────
436
- function calculateCost(cost, usage, imageCount) {
582
+ function calculateCost(cost, usage, count = 1) {
437
583
  if (cost.type === 'per-image') {
438
- return cost.perImage * imageCount;
584
+ return cost.perImage * count;
439
585
  }
440
586
  if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
441
587
  return ((usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000);
442
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
+ }
443
599
  return null;
444
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
+ }
445
625
  function formatCost(dollars) {
446
626
  if (dollars < 0.01) {
447
627
  return `$${dollars.toFixed(4)}`;
@@ -454,8 +634,23 @@ function extensionFromMediaType(mediaType) {
454
634
  'image/jpeg': '.jpg',
455
635
  'image/webp': '.webp',
456
636
  'image/gif': '.gif',
637
+ 'video/mp4': '.mp4',
638
+ 'video/webm': '.webm',
639
+ 'video/quicktime': '.mov',
457
640
  };
458
- return map[mediaType] || '.png';
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;
459
654
  }
460
655
  function ensureExtension(filePath, ext) {
461
656
  const parsed = path.parse(filePath);
@@ -3,7 +3,7 @@ export declare const PROVIDERS: Record<string, {
3
3
  label: string;
4
4
  hint: string;
5
5
  }>;
6
- export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v1/ai";
6
+ export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
7
7
  export type Credentials = Record<string, string>;
8
8
  export declare function readCredentials(): Credentials;
9
9
  export declare function writeCredentials(creds: Credentials): void;
@@ -34,7 +34,7 @@ export const PROVIDERS = {
34
34
  hint: 'Get your key at https://fal.ai/dashboard/keys',
35
35
  },
36
36
  };
37
- export const EGAKI_GATEWAY_URL = 'https://egaki.org/v1/ai';
37
+ export const EGAKI_GATEWAY_URL = 'https://egaki.org/v3/ai';
38
38
  function getConfigDir() {
39
39
  // XDG_CONFIG_HOME or ~/.config/egaki
40
40
  const xdg = process.env['XDG_CONFIG_HOME'];
@@ -0,0 +1,11 @@
1
+ export type GatewayModelCandidate = {
2
+ fullId: string;
3
+ provider: string;
4
+ modelId: string;
5
+ type: string;
6
+ tags: string[];
7
+ reason: 'type-image' | 'tag-image-generation';
8
+ };
9
+ export declare const GATEWAY_MODEL_CANDIDATES: GatewayModelCandidate[];
10
+ export declare const GATEWAY_MODEL_CANDIDATE_PROVIDERS: string[];
11
+ //# sourceMappingURL=gateway-model-candidates.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-model-candidates.generated.d.ts","sourceRoot":"","sources":["../src/gateway-model-candidates.generated.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,MAAM,EAAE,YAAY,GAAG,sBAAsB,CAAA;CAC9C,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,qBAAqB,EAwD3D,CAAA;AAED,eAAO,MAAM,iCAAiC,UAG7C,CAAA"}