egaki 0.2.0 → 0.4.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 +70 -0
- package/README.md +32 -5
- package/dist/chatgpt-auth.d.ts +25 -0
- package/dist/chatgpt-auth.d.ts.map +1 -0
- package/dist/chatgpt-auth.js +18 -11
- package/dist/cli.js +246 -19
- package/dist/credentials.d.ts +17 -4
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +55 -10
- package/dist/login.d.ts +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +44 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +87 -3
- package/dist/models.d.ts +5 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +58 -8
- package/dist/video-model-catalog.d.ts.map +1 -1
- package/dist/video-model-catalog.js +61 -0
- package/package.json +5 -2
- package/skills/egaki/SKILL.md +100 -0
- package/src/chatgpt-auth.ts +387 -0
- package/src/cli.ts +317 -22
- package/src/credentials.ts +75 -17
- package/src/login.ts +49 -4
- package/src/model-catalog.ts +92 -3
- package/src/models.ts +59 -8
- package/src/video-model-catalog.ts +61 -0
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,14 @@ import { generateImage as aiGenerateImage, generateText, experimental_generateVi
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import pc from 'picocolors';
|
|
17
|
+
import { createParser } from 'eventsource-parser';
|
|
17
18
|
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';
|
|
19
|
+
import { injectCredentialsToEnv, PROVIDERS, getChatGptAuth, saveChatGptAuth, getKeyStatus, } from './credentials.js';
|
|
20
|
+
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
|
|
20
21
|
import { VIDEO_CATALOG } from './video-model-catalog.js';
|
|
21
22
|
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
22
23
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
24
|
+
import { getValidChatGptAuth } from './chatgpt-auth.js';
|
|
23
25
|
const cli = goke('egaki');
|
|
24
26
|
process.title = 'egaki';
|
|
25
27
|
// Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
|
|
@@ -55,6 +57,7 @@ cli
|
|
|
55
57
|
.example('egaki login')
|
|
56
58
|
.example('# Non-interactive login with flags')
|
|
57
59
|
.example('egaki login --provider google --key AIza...')
|
|
60
|
+
.example('egaki login --provider vertex --key AIza...')
|
|
58
61
|
.example('# Pipe key from stdin (useful in CI/scripts)')
|
|
59
62
|
.example('echo "AIza..." | egaki login --provider google')
|
|
60
63
|
.example('# Show configured providers')
|
|
@@ -72,8 +75,13 @@ cli
|
|
|
72
75
|
}
|
|
73
76
|
// Non-interactive: --provider + --key or stdin
|
|
74
77
|
if (options.provider) {
|
|
78
|
+
// ChatGPT uses browser OAuth — skip key reading
|
|
79
|
+
if (options.provider === 'chatgpt') {
|
|
80
|
+
await loginNonInteractive({ provider: options.provider, key: '' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
75
83
|
const key = options.key || (await readKeyFromStdin());
|
|
76
|
-
loginNonInteractive({ provider: options.provider, key });
|
|
84
|
+
await loginNonInteractive({ provider: options.provider, key });
|
|
77
85
|
return;
|
|
78
86
|
}
|
|
79
87
|
// Interactive mode
|
|
@@ -170,34 +178,59 @@ cli
|
|
|
170
178
|
.example('egaki image "fill with flowers" --input photo.jpg --mask mask.png')
|
|
171
179
|
.example('# Generate with Gemini multimodal at 4K')
|
|
172
180
|
.example('egaki image "dreamy landscape" -m gemini-2.5-flash-image --image-size 4K')
|
|
181
|
+
.example('# Route through Vertex AI (Google Cloud billing)')
|
|
182
|
+
.example('egaki image "product photo on marble" -m vertex/imagen-4.0-generate-001')
|
|
173
183
|
.example('# Generate multiple images')
|
|
174
184
|
.example('egaki image "abstract art" -n 4 -o art.png')
|
|
175
185
|
.example('# Pipe to another tool')
|
|
176
186
|
.example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
|
|
177
187
|
.action(async (prompt, options) => {
|
|
178
|
-
// Inject stored API keys as env vars before calling the AI SDK
|
|
188
|
+
// Inject stored API keys as env vars before calling the AI SDK.
|
|
179
189
|
injectCredentialsToEnv();
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
// goke infers schema .default() values via Zod's input type, which leaves
|
|
191
|
+
// options.model / options.output / options.count as `T | undefined` even
|
|
192
|
+
// though the runtime always resolves them. Apply the same defaults here.
|
|
193
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
194
|
+
const outputPath = options.output ?? 'egaki-output.png';
|
|
195
|
+
const count = options.count ?? 1;
|
|
182
196
|
const config = getModelConfig(model);
|
|
197
|
+
const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio);
|
|
183
198
|
if (!options.stdout) {
|
|
184
199
|
console.error(pc.dim(`Model: ${model}`));
|
|
185
200
|
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
201
|
}
|
|
190
202
|
const inputImages = await readInputImages(options.input);
|
|
191
203
|
const maskImage = options.mask
|
|
192
204
|
? await readInputSource(options.mask)
|
|
193
205
|
: undefined;
|
|
194
|
-
|
|
206
|
+
// When using ChatGPT OAuth with OpenAI image models, route through the
|
|
207
|
+
// Responses API + imageGeneration tool instead of the Image API.
|
|
208
|
+
// The Codex OAuth client lacks the `api.model.images.request` scope.
|
|
209
|
+
const useResponsesApi = config.strategy === 'image' && shouldUseResponsesApi(model);
|
|
210
|
+
if (useResponsesApi) {
|
|
211
|
+
if (!options.stdout) {
|
|
212
|
+
console.error(pc.dim('Mode: Responses API (ChatGPT OAuth)'));
|
|
213
|
+
}
|
|
214
|
+
await generateWithResponsesApi({
|
|
215
|
+
prompt,
|
|
216
|
+
model,
|
|
217
|
+
outputPath,
|
|
218
|
+
count,
|
|
219
|
+
aspectRatio: parsedAspectRatio,
|
|
220
|
+
seed: options.seed,
|
|
221
|
+
inputImages,
|
|
222
|
+
maskImage,
|
|
223
|
+
json: options.json || false,
|
|
224
|
+
stdout: options.stdout || false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (config.strategy === 'image') {
|
|
195
228
|
await generateWithImageModel({
|
|
196
229
|
prompt,
|
|
197
230
|
model,
|
|
198
231
|
outputPath,
|
|
199
|
-
count
|
|
200
|
-
aspectRatio:
|
|
232
|
+
count,
|
|
233
|
+
aspectRatio: parsedAspectRatio,
|
|
201
234
|
seed: options.seed,
|
|
202
235
|
inputImages,
|
|
203
236
|
maskImage,
|
|
@@ -250,14 +283,18 @@ cli
|
|
|
250
283
|
.example('egaki video "A paper airplane gliding through clouds" -o airplane.mp4')
|
|
251
284
|
.example('# Generate with Veo model + duration')
|
|
252
285
|
.example('egaki video "cinematic rainy street at night" -m veo-3.1-fast-generate-001 --duration 6')
|
|
286
|
+
.example('# Route through Vertex AI (Google Cloud billing)')
|
|
287
|
+
.example('egaki video "storm over mountains" -m vertex/veo-3.1-fast-generate-001 --duration 6')
|
|
253
288
|
.example('# Image-to-video (model support required)')
|
|
254
289
|
.example('egaki video "animate subtle camera pan" --model luma-ray-2 --input frame.png -o animated.mp4')
|
|
255
290
|
.example('# Generate multiple videos')
|
|
256
291
|
.example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
|
|
257
292
|
.action(async (prompt, options) => {
|
|
258
293
|
injectCredentialsToEnv();
|
|
259
|
-
|
|
260
|
-
const
|
|
294
|
+
// Apply the same .default(...) values goke's input-type inference drops.
|
|
295
|
+
const model = options.model ?? DEFAULT_VIDEO_MODEL;
|
|
296
|
+
const outputPath = options.output ?? 'egaki-output.mp4';
|
|
297
|
+
const count = options.count ?? 1;
|
|
261
298
|
const config = getModelConfig(model);
|
|
262
299
|
if (config.strategy !== 'video') {
|
|
263
300
|
console.error(pc.red(`Model ${model} is not a video model`));
|
|
@@ -275,7 +312,7 @@ cli
|
|
|
275
312
|
prompt,
|
|
276
313
|
model,
|
|
277
314
|
outputPath,
|
|
278
|
-
count
|
|
315
|
+
count,
|
|
279
316
|
aspectRatio: options.aspectRatio,
|
|
280
317
|
resolution: options.resolution,
|
|
281
318
|
duration: options.duration,
|
|
@@ -301,6 +338,7 @@ cli
|
|
|
301
338
|
.action(async (options) => {
|
|
302
339
|
const { CATALOG } = await import('./model-catalog.js');
|
|
303
340
|
const yaml = await import('js-yaml');
|
|
341
|
+
const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
|
|
304
342
|
let models = options.type === 'video'
|
|
305
343
|
? VIDEO_CATALOG
|
|
306
344
|
: options.type === 'image'
|
|
@@ -318,6 +356,7 @@ cli
|
|
|
318
356
|
name: m.name,
|
|
319
357
|
...(m.description ? { description: m.description } : {}),
|
|
320
358
|
provider: m.provider,
|
|
359
|
+
auth: providerStatuses[m.provider] ?? { available: false, source: 'none' },
|
|
321
360
|
strategy: m.strategy,
|
|
322
361
|
released: m.released,
|
|
323
362
|
cost: formatCatalogCost(m.cost),
|
|
@@ -385,7 +424,10 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
|
|
|
385
424
|
const imagePrompt = inputImages.length > 0
|
|
386
425
|
? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
|
|
387
426
|
: prompt;
|
|
427
|
+
const config = getModelConfig(model);
|
|
388
428
|
const imageModel = await createImageModel(model);
|
|
429
|
+
// Use the correct providerOptions key based on provider (google vs vertex)
|
|
430
|
+
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
|
|
389
431
|
if (!stdout) {
|
|
390
432
|
console.error(pc.cyan('Generating...'));
|
|
391
433
|
}
|
|
@@ -396,7 +438,7 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
|
|
|
396
438
|
...(aspectRatio ? { aspectRatio } : {}),
|
|
397
439
|
...(seed !== undefined ? { seed } : {}),
|
|
398
440
|
providerOptions: {
|
|
399
|
-
|
|
441
|
+
[providerOptionsKey]: {
|
|
400
442
|
...(allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
401
443
|
...(aspectRatio ? { aspectRatio } : {}),
|
|
402
444
|
},
|
|
@@ -421,7 +463,6 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
|
|
|
421
463
|
console.error(pc.green(`Saved: ${filePath}`));
|
|
422
464
|
savedFiles.push(filePath);
|
|
423
465
|
}
|
|
424
|
-
const config = getModelConfig(model);
|
|
425
466
|
const cost = calculateCost(config.cost, result.usage, result.images.length);
|
|
426
467
|
if (cost != null) {
|
|
427
468
|
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
@@ -441,6 +482,8 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
|
|
|
441
482
|
// Generate using generateText with responseModalities (Gemini multimodal models)
|
|
442
483
|
async function generateWithTextModel({ prompt, model, outputPath, inputImages, imageSize, aspectRatio, json, stdout, }) {
|
|
443
484
|
const textModel = await createTextModel(model);
|
|
485
|
+
const config = getModelConfig(model);
|
|
486
|
+
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
|
|
444
487
|
if (!stdout) {
|
|
445
488
|
console.error(pc.cyan('Generating...'));
|
|
446
489
|
}
|
|
@@ -463,7 +506,7 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
463
506
|
model: textModel,
|
|
464
507
|
...(messages ? { messages } : { prompt }),
|
|
465
508
|
providerOptions: {
|
|
466
|
-
|
|
509
|
+
[providerOptionsKey]: {
|
|
467
510
|
responseModalities: ['TEXT', 'IMAGE'],
|
|
468
511
|
...(imageSize || aspectRatio
|
|
469
512
|
? {
|
|
@@ -501,7 +544,6 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
501
544
|
console.error(pc.green(`Saved: ${filePath}`));
|
|
502
545
|
savedFiles.push(filePath);
|
|
503
546
|
}
|
|
504
|
-
const config = getModelConfig(model);
|
|
505
547
|
const cost = calculateCost(config.cost, result.usage, imageFiles.length);
|
|
506
548
|
if (cost != null) {
|
|
507
549
|
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
@@ -521,6 +563,148 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
521
563
|
console.log(JSON.stringify(output, null, 2));
|
|
522
564
|
}
|
|
523
565
|
}
|
|
566
|
+
// Generate using the OpenAI Responses API with imageGeneration tool.
|
|
567
|
+
// Used when auth comes from ChatGPT OAuth (no Image API scope).
|
|
568
|
+
async function generateWithResponsesApi({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, json, stdout, }) {
|
|
569
|
+
if (count !== 1) {
|
|
570
|
+
console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
if (seed !== undefined) {
|
|
574
|
+
console.error(pc.red('ChatGPT image generation does not support --seed.'));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
if (maskImage) {
|
|
578
|
+
console.error(pc.red('ChatGPT image generation does not support --mask yet.'));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined;
|
|
582
|
+
if (aspectRatio && !size) {
|
|
583
|
+
console.error(pc.red('ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.'));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const storedAuth = getChatGptAuth();
|
|
587
|
+
if (!storedAuth?.accountId) {
|
|
588
|
+
console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth);
|
|
592
|
+
if (auth instanceof Error) {
|
|
593
|
+
console.error(pc.red(auth.message));
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
if (!stdout) {
|
|
597
|
+
console.error(pc.cyan('Generating...'));
|
|
598
|
+
}
|
|
599
|
+
const content = [
|
|
600
|
+
{ type: 'input_text', text: prompt },
|
|
601
|
+
...inputImages.map((image) => ({
|
|
602
|
+
type: 'input_image',
|
|
603
|
+
image_url: imageBytesToDataUrl(image),
|
|
604
|
+
})),
|
|
605
|
+
];
|
|
606
|
+
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${auth.access}`,
|
|
610
|
+
'ChatGPT-Account-ID': auth.accountId,
|
|
611
|
+
'Content-Type': 'application/json',
|
|
612
|
+
},
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
model: 'gpt-5.4',
|
|
615
|
+
instructions: 'You are Codex.',
|
|
616
|
+
input: [
|
|
617
|
+
{
|
|
618
|
+
type: 'message',
|
|
619
|
+
role: 'user',
|
|
620
|
+
content,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
tools: [
|
|
624
|
+
{
|
|
625
|
+
type: 'image_generation',
|
|
626
|
+
model,
|
|
627
|
+
size: 'auto',
|
|
628
|
+
quality: 'auto',
|
|
629
|
+
output_format: 'png',
|
|
630
|
+
output_compression: 100,
|
|
631
|
+
moderation: 'auto',
|
|
632
|
+
...(size ? { size } : {}),
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
tool_choice: 'auto',
|
|
636
|
+
parallel_tool_calls: true,
|
|
637
|
+
stream: true,
|
|
638
|
+
store: false,
|
|
639
|
+
include: [],
|
|
640
|
+
}),
|
|
641
|
+
});
|
|
642
|
+
if (!response.ok || !response.body) {
|
|
643
|
+
const body = await response.text().catch(() => '');
|
|
644
|
+
console.error(pc.red(`ChatGPT image generation failed: ${response.status}`));
|
|
645
|
+
if (body)
|
|
646
|
+
console.error(pc.dim(body));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
let imageBase64;
|
|
650
|
+
let revisedPrompt = null;
|
|
651
|
+
const processSseMessage = (message) => {
|
|
652
|
+
if (!message.data)
|
|
653
|
+
return;
|
|
654
|
+
try {
|
|
655
|
+
const event = JSON.parse(message.data);
|
|
656
|
+
if (event.type === 'response.image_generation_call.partial_image' &&
|
|
657
|
+
event.partial_image_b64 &&
|
|
658
|
+
!imageBase64) {
|
|
659
|
+
imageBase64 = event.partial_image_b64;
|
|
660
|
+
}
|
|
661
|
+
if (event.type === 'response.output_item.done' &&
|
|
662
|
+
event.item?.type === 'image_generation_call') {
|
|
663
|
+
if (event.item.result)
|
|
664
|
+
imageBase64 = event.item.result;
|
|
665
|
+
revisedPrompt = event.item.revised_prompt ?? revisedPrompt;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// Ignore keepalive and partial parse noise.
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const parser = createParser({
|
|
673
|
+
onEvent: processSseMessage,
|
|
674
|
+
});
|
|
675
|
+
const decoder = new TextDecoder();
|
|
676
|
+
for await (const chunk of response.body) {
|
|
677
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
678
|
+
}
|
|
679
|
+
parser.feed(decoder.decode());
|
|
680
|
+
if (!imageBase64) {
|
|
681
|
+
console.error(pc.red('No images generated.'));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
const imageBytes = Buffer.from(imageBase64, 'base64');
|
|
685
|
+
const mediaType = 'image/png';
|
|
686
|
+
if (stdout) {
|
|
687
|
+
process.stdout.write(imageBytes);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const savedFiles = [];
|
|
691
|
+
const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType));
|
|
692
|
+
fs.writeFileSync(filePath, imageBytes);
|
|
693
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
694
|
+
savedFiles.push(filePath);
|
|
695
|
+
if (revisedPrompt && !json) {
|
|
696
|
+
console.error(pc.dim(`Revised prompt: ${revisedPrompt}`));
|
|
697
|
+
}
|
|
698
|
+
if (json) {
|
|
699
|
+
const output = {
|
|
700
|
+
model,
|
|
701
|
+
files: savedFiles,
|
|
702
|
+
count: 1,
|
|
703
|
+
revisedPrompt,
|
|
704
|
+
};
|
|
705
|
+
console.log(JSON.stringify(output, null, 2));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
524
708
|
// Generate using experimental_generateVideo (video models)
|
|
525
709
|
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
|
|
526
710
|
const videoModel = await createVideoModel(model);
|
|
@@ -640,6 +824,40 @@ function extensionFromMediaType(mediaType) {
|
|
|
640
824
|
};
|
|
641
825
|
return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
|
|
642
826
|
}
|
|
827
|
+
function inferImageMediaType(image) {
|
|
828
|
+
if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
|
|
829
|
+
return 'image/png';
|
|
830
|
+
}
|
|
831
|
+
if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
|
|
832
|
+
return 'image/jpeg';
|
|
833
|
+
}
|
|
834
|
+
if (image.length >= 12 &&
|
|
835
|
+
image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
|
|
836
|
+
image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50) {
|
|
837
|
+
return 'image/webp';
|
|
838
|
+
}
|
|
839
|
+
if (image.length >= 6 &&
|
|
840
|
+
image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38) {
|
|
841
|
+
return 'image/gif';
|
|
842
|
+
}
|
|
843
|
+
return 'image/png';
|
|
844
|
+
}
|
|
845
|
+
function imageBytesToDataUrl(image) {
|
|
846
|
+
const mediaType = inferImageMediaType(image);
|
|
847
|
+
return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
|
|
848
|
+
}
|
|
849
|
+
function chatGptImageSizeFromAspectRatio(aspectRatio) {
|
|
850
|
+
switch (aspectRatio) {
|
|
851
|
+
case '1:1':
|
|
852
|
+
return '1024x1024';
|
|
853
|
+
case '3:2':
|
|
854
|
+
return '1536x1024';
|
|
855
|
+
case '2:3':
|
|
856
|
+
return '1024x1536';
|
|
857
|
+
default:
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
643
861
|
function normalizeResolutionKey(input) {
|
|
644
862
|
if (!input)
|
|
645
863
|
return undefined;
|
|
@@ -652,6 +870,15 @@ function normalizeResolutionKey(input) {
|
|
|
652
870
|
return '480p';
|
|
653
871
|
return normalized;
|
|
654
872
|
}
|
|
873
|
+
function isAspectRatio(input) {
|
|
874
|
+
return /^\d+:\d+$/.test(input);
|
|
875
|
+
}
|
|
876
|
+
function parseAspectRatioOption(input) {
|
|
877
|
+
if (!input || !isAspectRatio(input)) {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
return input;
|
|
881
|
+
}
|
|
655
882
|
function ensureExtension(filePath, ext) {
|
|
656
883
|
const parsed = path.parse(filePath);
|
|
657
884
|
if (parsed.ext) {
|
package/dist/credentials.d.ts
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ChatGptAuth } from './chatgpt-auth.js';
|
|
2
|
+
export type ProviderInfo = {
|
|
2
3
|
envVar: string;
|
|
3
4
|
label: string;
|
|
4
5
|
hint: string;
|
|
5
|
-
|
|
6
|
+
/** If true, this provider uses OAuth instead of a plain API key */
|
|
7
|
+
oauth?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const PROVIDERS: Record<string, ProviderInfo>;
|
|
6
10
|
export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
|
|
7
|
-
|
|
11
|
+
/** A credential value is either a plain API key string or a ChatGPT OAuth object. */
|
|
12
|
+
export type CredentialValue = string | ChatGptAuth;
|
|
13
|
+
export type Credentials = Record<string, CredentialValue>;
|
|
8
14
|
export declare function readCredentials(): Credentials;
|
|
9
15
|
export declare function writeCredentials(creds: Credentials): void;
|
|
10
16
|
export declare function saveProviderKey(provider: string, key: string): void;
|
|
17
|
+
export declare function saveChatGptAuth(auth: ChatGptAuth): void;
|
|
18
|
+
export declare function getChatGptAuth(): ChatGptAuth | undefined;
|
|
11
19
|
export declare function removeProviderKey(provider: string): void;
|
|
12
20
|
export declare function getProviderKey(provider: string): string | undefined;
|
|
13
21
|
export declare function injectCredentialsToEnv(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the current run should use the ChatGPT/Codex backend flow
|
|
24
|
+
* for OpenAI image models instead of a direct OpenAI API key.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldUseChatGptBackend(): boolean;
|
|
14
27
|
export declare function getKeyStatus(provider: string): {
|
|
15
28
|
available: boolean;
|
|
16
|
-
source: 'env' | 'stored' | 'none';
|
|
29
|
+
source: 'env' | 'stored' | 'oauth' | 'none';
|
|
17
30
|
};
|
|
18
31
|
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAKD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqClD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,qFAAqF;AACrF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAA;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAEzD,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,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIvD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,SAAS,CAOxD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAInE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAa7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;CAC5C,CAuBA"}
|
package/dist/credentials.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Credential storage for egaki.
|
|
2
2
|
// Reads and writes API keys from ~/.config/egaki/credentials.json.
|
|
3
3
|
// Keys are stored per-provider and injected as env vars at runtime.
|
|
4
|
+
//
|
|
5
|
+
// Most providers store a plain API key string. The 'chatgpt' provider stores
|
|
6
|
+
// a structured ChatGptAuth object with OAuth tokens for the Codex backend flow.
|
|
4
7
|
import fs from 'node:fs';
|
|
5
8
|
import path from 'node:path';
|
|
6
9
|
import os from 'node:os';
|
|
@@ -13,14 +16,25 @@ export const PROVIDERS = {
|
|
|
13
16
|
label: 'Egaki (all models, one subscription)',
|
|
14
17
|
hint: 'Subscribe at https://egaki.org/buy or run: egaki subscribe',
|
|
15
18
|
},
|
|
19
|
+
chatgpt: {
|
|
20
|
+
envVar: 'OPENAI_API_KEY',
|
|
21
|
+
label: 'ChatGPT (use your subscription)',
|
|
22
|
+
hint: 'Sign in with your ChatGPT account via browser',
|
|
23
|
+
oauth: true,
|
|
24
|
+
},
|
|
16
25
|
google: {
|
|
17
26
|
envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
|
|
18
27
|
label: 'Google AI (Gemini, Imagen)',
|
|
19
28
|
hint: 'Get your key at https://aistudio.google.com/apikey',
|
|
20
29
|
},
|
|
30
|
+
vertex: {
|
|
31
|
+
envVar: 'GOOGLE_VERTEX_API_KEY',
|
|
32
|
+
label: 'Google Vertex AI (Gemini, Imagen, Veo via Google Cloud)',
|
|
33
|
+
hint: 'Get an API key at https://console.cloud.google.com/apis/credentials',
|
|
34
|
+
},
|
|
21
35
|
openai: {
|
|
22
36
|
envVar: 'OPENAI_API_KEY',
|
|
23
|
-
label: 'OpenAI (
|
|
37
|
+
label: 'OpenAI (API key)',
|
|
24
38
|
hint: 'Get your key at https://platform.openai.com/api-keys',
|
|
25
39
|
},
|
|
26
40
|
replicate: {
|
|
@@ -65,6 +79,19 @@ export function saveProviderKey(provider, key) {
|
|
|
65
79
|
creds[provider] = key;
|
|
66
80
|
writeCredentials(creds);
|
|
67
81
|
}
|
|
82
|
+
export function saveChatGptAuth(auth) {
|
|
83
|
+
const creds = readCredentials();
|
|
84
|
+
creds['chatgpt'] = auth;
|
|
85
|
+
writeCredentials(creds);
|
|
86
|
+
}
|
|
87
|
+
export function getChatGptAuth() {
|
|
88
|
+
const creds = readCredentials();
|
|
89
|
+
const val = creds['chatgpt'];
|
|
90
|
+
if (val && typeof val === 'object') {
|
|
91
|
+
return val;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
68
95
|
export function removeProviderKey(provider) {
|
|
69
96
|
const creds = readCredentials();
|
|
70
97
|
delete creds[provider];
|
|
@@ -72,23 +99,33 @@ export function removeProviderKey(provider) {
|
|
|
72
99
|
}
|
|
73
100
|
export function getProviderKey(provider) {
|
|
74
101
|
const creds = readCredentials();
|
|
75
|
-
|
|
102
|
+
const val = creds[provider];
|
|
103
|
+
return typeof val === 'string' ? val : undefined;
|
|
76
104
|
}
|
|
77
|
-
// Inject all stored credentials as env vars so the AI SDK
|
|
78
|
-
// providers pick them up automatically. Env vars already set
|
|
79
|
-
//
|
|
105
|
+
// Inject all stored non-ChatGPT credentials as env vars so the AI SDK
|
|
106
|
+
// providers pick them up automatically. Env vars already set by the user take
|
|
107
|
+
// precedence (we don't overwrite them).
|
|
80
108
|
export function injectCredentialsToEnv() {
|
|
81
109
|
const creds = readCredentials();
|
|
82
|
-
for (const [provider,
|
|
110
|
+
for (const [provider, value] of Object.entries(creds)) {
|
|
83
111
|
const info = PROVIDERS[provider];
|
|
84
|
-
if (!info)
|
|
112
|
+
if (!info)
|
|
85
113
|
continue;
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
114
|
+
// Skip ChatGPT OAuth entries — handled separately
|
|
115
|
+
if (provider === 'chatgpt')
|
|
116
|
+
continue;
|
|
117
|
+
if (typeof value === 'string' && !process.env[info.envVar]) {
|
|
118
|
+
process.env[info.envVar] = value;
|
|
89
119
|
}
|
|
90
120
|
}
|
|
91
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when the current run should use the ChatGPT/Codex backend flow
|
|
124
|
+
* for OpenAI image models instead of a direct OpenAI API key.
|
|
125
|
+
*/
|
|
126
|
+
export function shouldUseChatGptBackend() {
|
|
127
|
+
return !process.env['OPENAI_API_KEY'] && Boolean(getChatGptAuth());
|
|
128
|
+
}
|
|
92
129
|
// Check if a provider has a key available (from env or stored credentials).
|
|
93
130
|
// Returns the source of the key for display purposes.
|
|
94
131
|
export function getKeyStatus(provider) {
|
|
@@ -96,6 +133,14 @@ export function getKeyStatus(provider) {
|
|
|
96
133
|
if (!info) {
|
|
97
134
|
return { available: false, source: 'none' };
|
|
98
135
|
}
|
|
136
|
+
// ChatGPT OAuth is a special case
|
|
137
|
+
if (provider === 'chatgpt') {
|
|
138
|
+
const auth = getChatGptAuth();
|
|
139
|
+
if (auth) {
|
|
140
|
+
return { available: true, source: 'oauth' };
|
|
141
|
+
}
|
|
142
|
+
return { available: false, source: 'none' };
|
|
143
|
+
}
|
|
99
144
|
if (process.env[info.envVar]) {
|
|
100
145
|
return { available: true, source: 'env' };
|
|
101
146
|
}
|
package/dist/login.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare function loginInteractive(): Promise<void>;
|
|
|
2
2
|
export declare function loginNonInteractive({ provider, key, }: {
|
|
3
3
|
provider: string;
|
|
4
4
|
key: string;
|
|
5
|
-
}): void
|
|
5
|
+
}): Promise<void>;
|
|
6
6
|
export declare function showLoginStatus(): void;
|
|
7
7
|
export declare function removeLogin(provider: string): void;
|
|
8
8
|
export declare function readKeyFromStdin(): Promise<string>;
|
package/dist/login.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAyBA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkFtD;AAED,wBAAsB,mBAAmB,CAAC,EACxC,QAAQ,EACR,GAAG,GACJ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB;AAED,wBAAgB,eAAe,IAAI,IAAI,CAyCtC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAalD;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMxD"}
|