@ai-agent-tools/picgen 0.1.0-alpha.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/dist/cli.js ADDED
@@ -0,0 +1,1602 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import "dotenv/config";
5
+ import { Command } from "commander";
6
+
7
+ // src/commands/create.ts
8
+ import YAML2 from "yaml";
9
+
10
+ // src/assets/output.ts
11
+ import { mkdir, writeFile } from "fs/promises";
12
+ import { basename, extname, join } from "path";
13
+ async function createGenerationRun(plan, now = /* @__PURE__ */ new Date()) {
14
+ const id = createRunId(plan, now);
15
+ const dateFolder = formatDate(now);
16
+ const outputDirectory = join(plan.outputDirectory, dateFolder, id);
17
+ const metadataPath = join(outputDirectory, "metadata.json");
18
+ const promptPath = join(outputDirectory, "prompt.txt");
19
+ await mkdir(outputDirectory, { recursive: true });
20
+ await writeFile(promptPath, plan.prompt, "utf8");
21
+ return {
22
+ id,
23
+ outputDirectory,
24
+ metadataPath,
25
+ promptPath
26
+ };
27
+ }
28
+ async function writeGenerationMetadata(run, metadata) {
29
+ await writeFile(run.metadataPath, JSON.stringify(redactGenerationMetadata(metadata), null, 2), "utf8");
30
+ }
31
+ function redactGenerationMetadata(metadata) {
32
+ return {
33
+ ...metadata,
34
+ provider_response: metadata.provider_response === void 0 ? void 0 : redactProviderImageData(metadata.provider_response)
35
+ };
36
+ }
37
+ async function writeProviderImage(run, image, index) {
38
+ const normalized = await normalizeProviderImage(image);
39
+ const id = `image-${index + 1}`;
40
+ const extension = extensionForMimeType(normalized.mime_type);
41
+ const path = join(run.outputDirectory, `${id}.${extension}`);
42
+ await writeFile(path, normalized.data);
43
+ const dimensions = readImageDimensions(normalized.data, normalized.mime_type);
44
+ return {
45
+ id,
46
+ path,
47
+ mime_type: normalized.mime_type,
48
+ metadata_path: run.metadataPath,
49
+ width: dimensions?.width,
50
+ height: dimensions?.height
51
+ };
52
+ }
53
+ async function writeProviderImages(run, images) {
54
+ const results = [];
55
+ for (const [index, image] of images.entries()) {
56
+ results.push(await writeProviderImage(run, image, index));
57
+ }
58
+ return results;
59
+ }
60
+ async function normalizeProviderImage(image) {
61
+ if (image.kind === "bytes") {
62
+ return {
63
+ data: image.data,
64
+ mime_type: image.mime_type
65
+ };
66
+ }
67
+ if (image.kind === "base64") {
68
+ const parsed = parseBase64Image(image.data);
69
+ return {
70
+ data: parsed.data,
71
+ mime_type: image.mime_type ?? parsed.mime_type ?? "image/png"
72
+ };
73
+ }
74
+ const response = await fetch(image.url);
75
+ if (!response.ok) {
76
+ throw new Error(`Failed to download generated image: ${response.status} ${response.statusText}`);
77
+ }
78
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
79
+ return {
80
+ data: new Uint8Array(await response.arrayBuffer()),
81
+ mime_type: image.mime_type ?? contentType ?? mimeTypeFromUrl(image.url) ?? "image/png"
82
+ };
83
+ }
84
+ function parseBase64Image(data) {
85
+ const dataUrlMatch = /^data:([^;]+);base64,(.+)$/s.exec(data);
86
+ const mimeType = dataUrlMatch?.[1];
87
+ const encoded = dataUrlMatch?.[2] ?? data;
88
+ return {
89
+ data: Buffer.from(encoded, "base64"),
90
+ mime_type: mimeType
91
+ };
92
+ }
93
+ function extensionForMimeType(mimeType) {
94
+ switch (mimeType) {
95
+ case "image/jpeg":
96
+ case "image/jpg":
97
+ return "jpg";
98
+ case "image/webp":
99
+ return "webp";
100
+ case "image/png":
101
+ return "png";
102
+ default:
103
+ return "bin";
104
+ }
105
+ }
106
+ function mimeTypeFromUrl(url) {
107
+ const extension = extname(basename(new URL(url).pathname)).toLowerCase();
108
+ switch (extension) {
109
+ case ".jpg":
110
+ case ".jpeg":
111
+ return "image/jpeg";
112
+ case ".webp":
113
+ return "image/webp";
114
+ case ".png":
115
+ return "image/png";
116
+ default:
117
+ return void 0;
118
+ }
119
+ }
120
+ function readImageDimensions(data, mimeType) {
121
+ const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
122
+ if (mimeType === "image/png") {
123
+ return readPngDimensions(buffer);
124
+ }
125
+ if (mimeType === "image/jpeg" || mimeType === "image/jpg") {
126
+ return readJpegDimensions(buffer);
127
+ }
128
+ if (mimeType === "image/webp") {
129
+ return readWebpDimensions(buffer);
130
+ }
131
+ return void 0;
132
+ }
133
+ function readPngDimensions(buffer) {
134
+ if (buffer.length < 24 || buffer[0] !== 137 || buffer.toString("ascii", 1, 4) !== "PNG" || buffer.toString("ascii", 12, 16) !== "IHDR") {
135
+ return void 0;
136
+ }
137
+ return {
138
+ width: buffer.readUInt32BE(16),
139
+ height: buffer.readUInt32BE(20)
140
+ };
141
+ }
142
+ function readJpegDimensions(buffer) {
143
+ if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216) {
144
+ return void 0;
145
+ }
146
+ let offset = 2;
147
+ while (offset + 9 < buffer.length) {
148
+ if (buffer[offset] !== 255) return void 0;
149
+ const marker = buffer[offset + 1];
150
+ const segmentLength = buffer.readUInt16BE(offset + 2);
151
+ if (segmentLength < 2 || offset + 2 + segmentLength > buffer.length) return void 0;
152
+ if (isJpegStartOfFrame(marker)) {
153
+ return {
154
+ height: buffer.readUInt16BE(offset + 5),
155
+ width: buffer.readUInt16BE(offset + 7)
156
+ };
157
+ }
158
+ offset += 2 + segmentLength;
159
+ }
160
+ return void 0;
161
+ }
162
+ function isJpegStartOfFrame(marker) {
163
+ return marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
164
+ }
165
+ function readWebpDimensions(buffer) {
166
+ if (buffer.length < 30 || buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP") {
167
+ return void 0;
168
+ }
169
+ let offset = 12;
170
+ while (offset + 8 <= buffer.length) {
171
+ const chunkType = buffer.toString("ascii", offset, offset + 4);
172
+ const chunkSize = buffer.readUInt32LE(offset + 4);
173
+ const chunkDataOffset = offset + 8;
174
+ if (chunkDataOffset + chunkSize > buffer.length) return void 0;
175
+ if (chunkType === "VP8X" && chunkSize >= 10) {
176
+ return {
177
+ width: 1 + buffer.readUIntLE(chunkDataOffset + 4, 3),
178
+ height: 1 + buffer.readUIntLE(chunkDataOffset + 7, 3)
179
+ };
180
+ }
181
+ if (chunkType === "VP8L" && chunkSize >= 5 && buffer[chunkDataOffset] === 47) {
182
+ const b1 = buffer[chunkDataOffset + 1];
183
+ const b2 = buffer[chunkDataOffset + 2];
184
+ const b3 = buffer[chunkDataOffset + 3];
185
+ const b4 = buffer[chunkDataOffset + 4];
186
+ return {
187
+ width: 1 + ((b2 & 63) << 8 | b1),
188
+ height: 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6)
189
+ };
190
+ }
191
+ if (chunkType === "VP8 " && chunkSize >= 10 && buffer[chunkDataOffset + 3] === 157 && buffer[chunkDataOffset + 4] === 1 && buffer[chunkDataOffset + 5] === 42) {
192
+ return {
193
+ width: buffer.readUInt16LE(chunkDataOffset + 6) & 16383,
194
+ height: buffer.readUInt16LE(chunkDataOffset + 8) & 16383
195
+ };
196
+ }
197
+ offset = chunkDataOffset + chunkSize + chunkSize % 2;
198
+ }
199
+ return void 0;
200
+ }
201
+ function createRunId(plan, date) {
202
+ const time = `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}-${padMilliseconds(
203
+ date.getMilliseconds()
204
+ )}`;
205
+ const preset = slug(plan.presetName);
206
+ const provider2 = slug(plan.providerName);
207
+ return `${time}-${preset}-${provider2}`;
208
+ }
209
+ function formatDate(date) {
210
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
211
+ }
212
+ function pad(value) {
213
+ return String(value).padStart(2, "0");
214
+ }
215
+ function padMilliseconds(value) {
216
+ return String(value).padStart(3, "0");
217
+ }
218
+ function slug(value) {
219
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
220
+ }
221
+ function redactProviderImageData(value, key) {
222
+ if (Array.isArray(value)) {
223
+ return value.map((item) => redactProviderImageData(item));
224
+ }
225
+ if (!value || typeof value !== "object") {
226
+ return shouldRedactImageDataKey(key) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
227
+ }
228
+ return Object.fromEntries(
229
+ Object.entries(value).map(([entryKey, entryValue]) => [
230
+ entryKey,
231
+ redactProviderImageData(entryValue, entryKey)
232
+ ])
233
+ );
234
+ }
235
+ function shouldRedactImageDataKey(key) {
236
+ return key === "b64_json" || key === "data" || key === "thoughtSignature" || key === "thought_signature";
237
+ }
238
+ function redactedProviderDataPlaceholder(value) {
239
+ return `[redacted provider data: ${value.length} chars]`;
240
+ }
241
+
242
+ // src/assets/reference.ts
243
+ import { stat } from "fs/promises";
244
+ import { extname as extname2, resolve } from "path";
245
+ async function resolveReferenceImages(paths = []) {
246
+ const images = [];
247
+ for (const inputPath of paths) {
248
+ const absolutePath = resolve(inputPath);
249
+ const file = await stat(absolutePath).catch(() => {
250
+ throw new Error(`Reference image not found: ${inputPath}`);
251
+ });
252
+ if (!file.isFile()) {
253
+ throw new Error(`Reference image is not a file: ${inputPath}`);
254
+ }
255
+ images.push({
256
+ path: absolutePath,
257
+ mime_type: mimeTypeFromPath(absolutePath),
258
+ bytes: file.size
259
+ });
260
+ }
261
+ return images;
262
+ }
263
+ function mimeTypeFromPath(path) {
264
+ switch (extname2(path).toLowerCase()) {
265
+ case ".png":
266
+ return "image/png";
267
+ case ".jpg":
268
+ case ".jpeg":
269
+ return "image/jpeg";
270
+ case ".webp":
271
+ return "image/webp";
272
+ default:
273
+ throw new Error(`Unsupported reference image format: ${path}`);
274
+ }
275
+ }
276
+
277
+ // src/config/store.ts
278
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
279
+ import { dirname, join as join2 } from "path";
280
+ import { homedir } from "os";
281
+ import YAML from "yaml";
282
+
283
+ // src/config/defaults.ts
284
+ var defaultConfig = {
285
+ default_preset: "default",
286
+ routing: {
287
+ default_mode: "balanced",
288
+ default_provider: "openai_official",
289
+ fallback_providers: ["gemini_official"]
290
+ },
291
+ providers: {
292
+ openai_official: {
293
+ enabled: true,
294
+ protocol: "openai-images",
295
+ channel: "official",
296
+ base_url: "https://api.openai.com",
297
+ api_key_env: "OPENAI_API_KEY",
298
+ models: ["gpt-image-2"],
299
+ capabilities: ["text-to-image"]
300
+ },
301
+ gemini_official: {
302
+ enabled: true,
303
+ protocol: "gemini",
304
+ channel: "official",
305
+ base_url: "https://generativelanguage.googleapis.com",
306
+ api_key_env: "GEMINI_API_KEY",
307
+ models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"],
308
+ capabilities: ["text-to-image", "reference-image"]
309
+ }
310
+ },
311
+ modes: {
312
+ fast: {
313
+ preferred_models: ["gemini-3.1-flash-image-preview", "gpt-image-2"]
314
+ },
315
+ balanced: {
316
+ preferred_models: ["gpt-image-2", "gemini-3.1-flash-image-preview"]
317
+ },
318
+ premium: {
319
+ preferred_models: ["gemini-3-pro-image-preview", "gpt-image-2"]
320
+ }
321
+ },
322
+ presets: {
323
+ default: {
324
+ mode: "balanced",
325
+ aspect_ratio: "1:1",
326
+ size: "medium",
327
+ quality: "auto",
328
+ n: 1,
329
+ output_format: "png"
330
+ },
331
+ poster: {
332
+ mode: "premium",
333
+ aspect_ratio: "3:4",
334
+ size: "large",
335
+ quality: "high",
336
+ n: 2,
337
+ output_format: "png"
338
+ },
339
+ "social-cover": {
340
+ mode: "balanced",
341
+ aspect_ratio: "16:9",
342
+ size: "large",
343
+ quality: "high",
344
+ n: 2,
345
+ output_format: "png"
346
+ },
347
+ "product-shot": {
348
+ mode: "premium",
349
+ aspect_ratio: "1:1",
350
+ size: "large",
351
+ quality: "high",
352
+ n: 2,
353
+ output_format: "png"
354
+ },
355
+ "fast-draft": {
356
+ mode: "fast",
357
+ aspect_ratio: "1:1",
358
+ size: "medium",
359
+ quality: "low",
360
+ n: 1,
361
+ output_format: "jpeg"
362
+ }
363
+ }
364
+ };
365
+
366
+ // src/config/schema.ts
367
+ import { z } from "zod";
368
+ var providerSchema = z.object({
369
+ enabled: z.boolean().default(true),
370
+ protocol: z.enum(["openai-images", "gemini"]),
371
+ channel: z.enum(["official", "third_party"]),
372
+ base_url: z.string().url(),
373
+ api_key_env: z.string().min(1),
374
+ models: z.array(z.string().min(1)).min(1),
375
+ test_model: z.string().min(1).optional(),
376
+ capabilities: z.array(z.enum(["text-to-image", "reference-image"])).optional()
377
+ }).transform((provider2) => ({
378
+ ...provider2,
379
+ capabilities: provider2.capabilities ?? defaultCapabilitiesForProtocol(provider2.protocol)
380
+ }));
381
+ var presetSchema = z.object({
382
+ mode: z.string().min(1),
383
+ aspect_ratio: z.string().min(1),
384
+ size: z.string().min(1),
385
+ quality: z.string().min(1),
386
+ n: z.number().int().positive(),
387
+ output_format: z.enum(["png", "jpeg", "webp"])
388
+ });
389
+ var routingSchema = z.object({
390
+ default_mode: z.string().min(1),
391
+ default_provider: z.string().min(1).optional(),
392
+ fallback_providers: z.array(z.string().min(1)).optional(),
393
+ provider_priority: z.array(z.string().min(1)).optional()
394
+ }).transform((routing) => {
395
+ if (routing.default_provider) {
396
+ return {
397
+ default_mode: routing.default_mode,
398
+ default_provider: routing.default_provider,
399
+ fallback_providers: routing.fallback_providers ?? []
400
+ };
401
+ }
402
+ const [defaultProvider, ...fallbackProviders] = routing.provider_priority ?? [];
403
+ if (!defaultProvider) {
404
+ throw new Error("routing.default_provider is required.");
405
+ }
406
+ return {
407
+ default_mode: routing.default_mode,
408
+ default_provider: defaultProvider,
409
+ fallback_providers: routing.fallback_providers ?? fallbackProviders
410
+ };
411
+ });
412
+ var picgenConfigSchema = z.object({
413
+ default_preset: z.string().min(1),
414
+ routing: routingSchema,
415
+ providers: z.record(providerSchema),
416
+ modes: z.record(
417
+ z.object({
418
+ preferred_models: z.array(z.string().min(1)).min(1)
419
+ })
420
+ ),
421
+ presets: z.record(presetSchema)
422
+ });
423
+ function defaultCapabilitiesForProtocol(protocol) {
424
+ if (protocol === "gemini") {
425
+ return ["text-to-image", "reference-image"];
426
+ }
427
+ return ["text-to-image"];
428
+ }
429
+
430
+ // src/config/store.ts
431
+ function getConfigPath() {
432
+ return process.env.PICGEN_CONFIG ?? join2(homedir(), ".picgen", "config.yaml");
433
+ }
434
+ async function loadConfig() {
435
+ const path = getConfigPath();
436
+ try {
437
+ const raw = await readFile(path, "utf8");
438
+ const parsed = YAML.parse(raw);
439
+ return picgenConfigSchema.parse(parsed);
440
+ } catch (error) {
441
+ if (error.code === "ENOENT") {
442
+ return structuredClone(defaultConfig);
443
+ }
444
+ throw error;
445
+ }
446
+ }
447
+ async function saveConfig(config) {
448
+ const parsed = picgenConfigSchema.parse(config);
449
+ const path = getConfigPath();
450
+ await mkdir2(dirname(path), { recursive: true });
451
+ await writeFile2(path, YAML.stringify(parsed), "utf8");
452
+ }
453
+ async function ensureConfig() {
454
+ const config = await loadConfig();
455
+ await saveConfig(config);
456
+ return config;
457
+ }
458
+
459
+ // src/providers/gemini.ts
460
+ import { readFile as readFile2 } from "fs/promises";
461
+
462
+ // src/providers/urls.ts
463
+ function normalizeProviderBaseUrl(baseUrl) {
464
+ const url = new URL(baseUrl);
465
+ url.pathname = stripKnownApiVersionPath(url.pathname);
466
+ url.search = "";
467
+ url.hash = "";
468
+ return url.toString().replace(/\/+$/, "");
469
+ }
470
+ function buildOpenAIProtocolUrl(baseUrl, path) {
471
+ return `${normalizeProviderBaseUrl(baseUrl)}/v1/${path.replace(/^\/+/, "")}`;
472
+ }
473
+ function buildGeminiProtocolUrl(baseUrl, path) {
474
+ return `${normalizeProviderBaseUrl(baseUrl)}/v1beta/${path.replace(/^\/+/, "")}`;
475
+ }
476
+ function defaultProviderBaseUrl(protocol) {
477
+ return protocol === "openai-images" ? "https://api.openai.com" : "https://generativelanguage.googleapis.com";
478
+ }
479
+ function stripKnownApiVersionPath(pathname) {
480
+ const normalized = pathname.replace(/\/+$/, "");
481
+ if (normalized === "" || normalized === "/") return "";
482
+ if (normalized === "/v1" || normalized === "/v1beta") return "";
483
+ return normalized;
484
+ }
485
+
486
+ // src/providers/gemini.ts
487
+ var GeminiAdapter = class {
488
+ protocol = "gemini";
489
+ async generate(plan, run) {
490
+ const apiKey = process.env[plan.provider.api_key_env];
491
+ if (!apiKey) {
492
+ throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
493
+ }
494
+ const responses = [];
495
+ const providerImages = [];
496
+ const requestCount = Math.max(1, plan.preset.n);
497
+ const referenceParts = await readReferenceImageParts(plan);
498
+ for (let index = 0; index < requestCount; index += 1) {
499
+ const response = await fetch(buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model), {
500
+ method: "POST",
501
+ headers: {
502
+ "x-goog-api-key": apiKey,
503
+ "Content-Type": "application/json"
504
+ },
505
+ body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
506
+ });
507
+ const raw = await readJsonResponse(response);
508
+ if (!response.ok) {
509
+ throw new Error(formatGeminiError(response.status, response.statusText, raw));
510
+ }
511
+ responses.push(raw);
512
+ providerImages.push(...extractGeminiImages(raw));
513
+ }
514
+ const images = await writeProviderImages(run, providerImages);
515
+ return {
516
+ images,
517
+ provider_response: responses.length === 1 ? responses[0] : responses
518
+ };
519
+ }
520
+ };
521
+ function buildGeminiGenerateContentRequest(plan, referenceParts = []) {
522
+ return {
523
+ contents: [
524
+ {
525
+ role: "user",
526
+ parts: [
527
+ {
528
+ text: plan.prompt
529
+ },
530
+ ...referenceParts
531
+ ]
532
+ }
533
+ ],
534
+ generationConfig: {
535
+ responseModalities: ["IMAGE"],
536
+ imageConfig: removeUndefined({
537
+ aspectRatio: plan.preset.aspect_ratio,
538
+ imageSize: mapGeminiImageSize(plan.preset.size)
539
+ })
540
+ }
541
+ };
542
+ }
543
+ async function readReferenceImageParts(plan) {
544
+ return Promise.all(
545
+ plan.referenceImages.map(async (image) => ({
546
+ inlineData: {
547
+ mimeType: image.mime_type,
548
+ data: (await readFile2(image.path)).toString("base64")
549
+ }
550
+ }))
551
+ );
552
+ }
553
+ function extractGeminiImages(response) {
554
+ const parts = response.candidates?.flatMap((candidate) => candidate.content?.parts ?? []) ?? [];
555
+ const imageParts = parts.filter((part) => part.thought !== true).map((part) => part.inlineData ?? normalizeInlineData(part.inline_data)).filter(Boolean);
556
+ if (imageParts.length === 0) {
557
+ throw new Error("Gemini response did not include generated image data.");
558
+ }
559
+ return imageParts.map((image, index) => {
560
+ if (!image?.data) {
561
+ throw new Error(`Gemini image part ${index + 1} did not include base64 data.`);
562
+ }
563
+ return {
564
+ kind: "base64",
565
+ data: image.data,
566
+ mime_type: image.mimeType ?? "image/png"
567
+ };
568
+ });
569
+ }
570
+ function buildGeminiGenerateContentUrl(baseUrl, model) {
571
+ return buildGeminiProtocolUrl(baseUrl, `models/${encodeURIComponent(model)}:generateContent`);
572
+ }
573
+ function normalizeInlineData(inlineData) {
574
+ if (!inlineData) return void 0;
575
+ return {
576
+ data: inlineData.data,
577
+ mimeType: inlineData.mime_type
578
+ };
579
+ }
580
+ function mapGeminiImageSize(size) {
581
+ switch (size) {
582
+ case "small":
583
+ return "512";
584
+ case "medium":
585
+ return "1K";
586
+ case "large":
587
+ return "2K";
588
+ case "auto":
589
+ return void 0;
590
+ default:
591
+ return /^(512|1K|2K|4K)$/.test(size) ? size : void 0;
592
+ }
593
+ }
594
+ function removeUndefined(input3) {
595
+ return Object.fromEntries(Object.entries(input3).filter(([, value]) => value !== void 0));
596
+ }
597
+ async function readJsonResponse(response) {
598
+ const text = await response.text();
599
+ if (!text.trim()) return {};
600
+ try {
601
+ return JSON.parse(text);
602
+ } catch {
603
+ throw new Error("Gemini response was not valid JSON.");
604
+ }
605
+ }
606
+ function formatGeminiError(status, statusText, response) {
607
+ const message = extractErrorMessage(response);
608
+ return `Gemini request failed: ${status} ${statusText}${message ? ` - ${message}` : ""}`;
609
+ }
610
+ function extractErrorMessage(response) {
611
+ const error = response.error;
612
+ if (typeof error === "object" && error && "message" in error) {
613
+ const message = error.message;
614
+ return typeof message === "string" ? message : void 0;
615
+ }
616
+ return void 0;
617
+ }
618
+
619
+ // src/providers/openaiImages.ts
620
+ var OpenAIImagesAdapter = class {
621
+ protocol = "openai-images";
622
+ async generate(plan, run) {
623
+ const apiKey = process.env[plan.provider.api_key_env];
624
+ if (!apiKey) {
625
+ throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
626
+ }
627
+ const response = await fetch(buildOpenAIImagesUrl(plan.provider.base_url), {
628
+ method: "POST",
629
+ headers: {
630
+ Authorization: `Bearer ${apiKey}`,
631
+ "Content-Type": "application/json"
632
+ },
633
+ body: JSON.stringify(buildOpenAIImagesRequest(plan))
634
+ });
635
+ const raw = await readJsonResponse2(response);
636
+ if (!response.ok) {
637
+ throw new Error(formatOpenAIImagesError(response.status, response.statusText, raw));
638
+ }
639
+ const providerImages = extractOpenAIImages(raw);
640
+ const images = await writeProviderImages(run, providerImages);
641
+ const revisedPrompts = raw.data?.map((item) => item.revised_prompt);
642
+ return {
643
+ images: images.map((image, index) => ({
644
+ ...image,
645
+ revised_prompt: revisedPrompts?.[index]
646
+ })),
647
+ provider_response: raw
648
+ };
649
+ }
650
+ };
651
+ function buildOpenAIImagesRequest(plan) {
652
+ if (plan.referenceImages.length > 0) {
653
+ throw new Error(
654
+ "Reference images are not supported by the openai-images generation adapter yet. Use a Gemini provider for reference-image generation."
655
+ );
656
+ }
657
+ return removeUndefined2({
658
+ model: plan.model,
659
+ prompt: plan.prompt,
660
+ n: plan.preset.n,
661
+ size: mapOpenAIImageSize(plan.preset.aspect_ratio, plan.preset.size),
662
+ quality: mapOpenAIImageQuality(plan.preset.quality),
663
+ output_format: plan.preset.output_format,
664
+ response_format: "b64_json"
665
+ });
666
+ }
667
+ function extractOpenAIImages(response) {
668
+ if (!Array.isArray(response.data) || response.data.length === 0) {
669
+ throw new Error("OpenAI images response did not include generated image data.");
670
+ }
671
+ return response.data.map((item, index) => {
672
+ if (item.b64_json) {
673
+ return {
674
+ kind: "base64",
675
+ data: item.b64_json,
676
+ mime_type: void 0
677
+ };
678
+ }
679
+ if (item.url) {
680
+ return {
681
+ kind: "url",
682
+ url: item.url,
683
+ mime_type: void 0
684
+ };
685
+ }
686
+ throw new Error(`OpenAI images response item ${index + 1} did not include b64_json or url.`);
687
+ });
688
+ }
689
+ function buildOpenAIImagesUrl(baseUrl) {
690
+ return buildOpenAIProtocolUrl(baseUrl, "images/generations");
691
+ }
692
+ function mapOpenAIImageSize(aspectRatio, size) {
693
+ if (/^\d+x\d+$/.test(size) || size === "auto") return size;
694
+ switch (aspectRatio) {
695
+ case "3:4":
696
+ case "2:3":
697
+ return "1024x1536";
698
+ case "4:3":
699
+ case "3:2":
700
+ case "16:9":
701
+ case "9:5":
702
+ return "1536x1024";
703
+ case "1:1":
704
+ return "1024x1024";
705
+ default:
706
+ return "auto";
707
+ }
708
+ }
709
+ function mapOpenAIImageQuality(quality) {
710
+ if (["low", "medium", "high", "auto"].includes(quality)) return quality;
711
+ return void 0;
712
+ }
713
+ function removeUndefined2(input3) {
714
+ return Object.fromEntries(Object.entries(input3).filter(([, value]) => value !== void 0));
715
+ }
716
+ async function readJsonResponse2(response) {
717
+ const text = await response.text();
718
+ if (!text.trim()) return {};
719
+ try {
720
+ return JSON.parse(text);
721
+ } catch {
722
+ throw new Error("OpenAI images response was not valid JSON.");
723
+ }
724
+ }
725
+ function formatOpenAIImagesError(status, statusText, response) {
726
+ const message = extractErrorMessage2(response);
727
+ return `OpenAI images request failed: ${status} ${statusText}${message ? ` - ${message}` : ""}`;
728
+ }
729
+ function extractErrorMessage2(response) {
730
+ const error = response.error;
731
+ if (typeof error === "object" && error && "message" in error) {
732
+ const message = error.message;
733
+ return typeof message === "string" ? message : void 0;
734
+ }
735
+ return void 0;
736
+ }
737
+
738
+ // src/providers/adapters.ts
739
+ var NotImplementedAdapter = class {
740
+ constructor(protocol) {
741
+ this.protocol = protocol;
742
+ }
743
+ protocol;
744
+ async generate(plan, _run) {
745
+ throw new Error(
746
+ `Real generation is not implemented for ${this.protocol} yet. Dry-run plan is ready for ${plan.providerName}/${plan.model}.`
747
+ );
748
+ }
749
+ };
750
+ function getAdapter(protocol) {
751
+ if (protocol === "openai-images") {
752
+ return new OpenAIImagesAdapter();
753
+ }
754
+ if (protocol === "gemini") {
755
+ return new GeminiAdapter();
756
+ }
757
+ return new NotImplementedAdapter(protocol);
758
+ }
759
+
760
+ // src/routing/resolve.ts
761
+ import { join as join3 } from "path";
762
+ import { cwd } from "process";
763
+ function resolveGenerationPlan(config, options) {
764
+ const presetName = options.presetName ?? config.default_preset;
765
+ const preset = config.presets[presetName];
766
+ if (!preset) {
767
+ throw new Error(`Unknown preset: ${presetName}`);
768
+ }
769
+ const modeName = options.modeName ?? preset.mode ?? config.routing.default_mode;
770
+ const mode = config.modes[modeName];
771
+ if (!mode) {
772
+ throw new Error(`Unknown mode: ${modeName}`);
773
+ }
774
+ const providerCandidates = options.providerName ? [options.providerName] : [config.routing.default_provider, ...config.routing.fallback_providers];
775
+ const requiredCapability = requiredCapabilityForOptions(options);
776
+ const unsupportedProviders = [];
777
+ for (const providerName of providerCandidates) {
778
+ const provider2 = config.providers[providerName];
779
+ if (!provider2 || !provider2.enabled) continue;
780
+ if (!provider2.capabilities.includes(requiredCapability)) {
781
+ unsupportedProviders.push(providerName);
782
+ continue;
783
+ }
784
+ const modelCandidates = options.model ? [options.model] : mode.preferred_models;
785
+ const model = modelCandidates.find((candidate) => provider2.models.includes(candidate));
786
+ if (!model) continue;
787
+ return {
788
+ prompt: options.prompt,
789
+ providerName,
790
+ provider: provider2,
791
+ model,
792
+ presetName,
793
+ preset,
794
+ modeName,
795
+ outputDirectory: options.outputDirectory ?? join3(cwd(), "outputs", "picgen"),
796
+ referenceImages: options.referenceImages ?? []
797
+ };
798
+ }
799
+ if (options.providerName && unsupportedProviders.includes(options.providerName)) {
800
+ throw new Error(
801
+ `Provider "${options.providerName}" does not support ${requiredCapability}.`
802
+ );
803
+ }
804
+ throw new Error(
805
+ `No enabled provider can satisfy preset "${presetName}" with mode "${modeName}" and capability "${requiredCapability}".`
806
+ );
807
+ }
808
+ function requiredCapabilityForOptions(options) {
809
+ return options.referenceImages && options.referenceImages.length > 0 ? "reference-image" : "text-to-image";
810
+ }
811
+
812
+ // src/commands/confirm.ts
813
+ import { confirm } from "@inquirer/prompts";
814
+ async function confirmGeneration(plan, options) {
815
+ if (options.yes) {
816
+ return { confirmed: true, skipped: true };
817
+ }
818
+ console.log("PicGen generation preview:");
819
+ console.log(formatGenerationPreview(plan));
820
+ const confirmed = await confirm({
821
+ message: "Generate now? This may consume provider quota.",
822
+ default: false
823
+ });
824
+ return { confirmed, skipped: false };
825
+ }
826
+ function formatGenerationPreview(plan) {
827
+ return [
828
+ `Provider: ${plan.provider} (${plan.protocol})`,
829
+ `Model: ${plan.model}`,
830
+ `Preset: ${plan.preset}`,
831
+ `Images: ${plan.n}`,
832
+ `Reference images: ${plan.reference_images.length}`,
833
+ `Aspect ratio: ${plan.aspect_ratio}`,
834
+ `Output: ${plan.output_directory}`
835
+ ].join("\n");
836
+ }
837
+
838
+ // src/commands/create.ts
839
+ async function runCreate(promptParts, options) {
840
+ const prompt = promptParts.join(" ").trim();
841
+ if (!prompt) {
842
+ throw new Error("Prompt is required.");
843
+ }
844
+ const config = await loadConfig();
845
+ const referenceImages = await resolveReferenceImages(options.reference ?? []);
846
+ const plan = resolveGenerationPlan(config, {
847
+ prompt,
848
+ presetName: options.preset,
849
+ providerName: options.provider,
850
+ modeName: options.mode,
851
+ model: options.model,
852
+ outputDirectory: options.outDir,
853
+ referenceImages
854
+ });
855
+ const planOutput = toPlanOutput(plan);
856
+ if (options.dryRun) {
857
+ if (options.json) {
858
+ console.log(
859
+ JSON.stringify(
860
+ {
861
+ ok: true,
862
+ dry_run: true,
863
+ provider_called: false,
864
+ requires_confirmation: true,
865
+ plan: planOutput
866
+ },
867
+ null,
868
+ 2
869
+ )
870
+ );
871
+ } else {
872
+ console.log("PicGen dry-run plan:");
873
+ console.log(YAML2.stringify(planOutput));
874
+ }
875
+ return;
876
+ }
877
+ const confirmation = await confirmGeneration(planOutput, { yes: options.yes });
878
+ if (!confirmation.confirmed) {
879
+ const cancelledOutput = {
880
+ ok: false,
881
+ cancelled: true,
882
+ provider_called: false,
883
+ plan: planOutput
884
+ };
885
+ if (options.json) {
886
+ console.log(JSON.stringify(cancelledOutput, null, 2));
887
+ } else {
888
+ console.log("Generation cancelled.");
889
+ }
890
+ return;
891
+ }
892
+ const run = await createGenerationRun(plan);
893
+ const runtimePlan = {
894
+ ...plan,
895
+ outputDirectory: run.outputDirectory
896
+ };
897
+ const runtimePlanOutput = toPlanOutput(runtimePlan);
898
+ await writeGenerationMetadata(run, {
899
+ plan: runtimePlanOutput,
900
+ run: {
901
+ id: run.id,
902
+ output_directory: run.outputDirectory,
903
+ metadata_path: run.metadataPath,
904
+ prompt_path: run.promptPath
905
+ }
906
+ });
907
+ const adapter = getAdapter(plan.provider.protocol);
908
+ let result;
909
+ try {
910
+ result = await adapter.generate(runtimePlan, run);
911
+ } catch (error) {
912
+ await writeGenerationMetadata(run, {
913
+ plan: runtimePlanOutput,
914
+ run: {
915
+ id: run.id,
916
+ output_directory: run.outputDirectory,
917
+ metadata_path: run.metadataPath,
918
+ prompt_path: run.promptPath
919
+ },
920
+ error: {
921
+ message: error instanceof Error ? error.message : String(error),
922
+ name: error instanceof Error ? error.name : void 0
923
+ }
924
+ });
925
+ throw error;
926
+ }
927
+ await writeGenerationMetadata(run, {
928
+ plan: runtimePlanOutput,
929
+ run: {
930
+ id: run.id,
931
+ output_directory: run.outputDirectory,
932
+ metadata_path: run.metadataPath,
933
+ prompt_path: run.promptPath
934
+ },
935
+ provider_response: result.provider_response,
936
+ images: result.images
937
+ });
938
+ console.log(
939
+ JSON.stringify(
940
+ {
941
+ ok: true,
942
+ dry_run: false,
943
+ output_dir: run.outputDirectory,
944
+ metadata_path: run.metadataPath,
945
+ images: result.images
946
+ },
947
+ null,
948
+ 2
949
+ )
950
+ );
951
+ }
952
+ function toPlanOutput(plan) {
953
+ return {
954
+ prompt: plan.prompt,
955
+ provider: plan.providerName,
956
+ protocol: plan.provider.protocol,
957
+ channel: plan.provider.channel,
958
+ model: plan.model,
959
+ preset: plan.presetName,
960
+ mode: plan.modeName,
961
+ aspect_ratio: plan.preset.aspect_ratio,
962
+ size: plan.preset.size,
963
+ quality: plan.preset.quality,
964
+ n: plan.preset.n,
965
+ output_format: plan.preset.output_format,
966
+ output_directory: plan.outputDirectory,
967
+ reference_images: plan.referenceImages.map((image) => ({
968
+ path: image.path,
969
+ mime_type: image.mime_type,
970
+ bytes: image.bytes
971
+ }))
972
+ };
973
+ }
974
+
975
+ // src/config/doctor.ts
976
+ function inspectProviders(config) {
977
+ return Object.entries(config.providers).map(([name, provider2]) => {
978
+ const hasApiKey = Boolean(process.env[provider2.api_key_env]);
979
+ const status = !provider2.enabled ? "disabled" : hasApiKey ? "ok" : "missing_api_key";
980
+ return {
981
+ name,
982
+ enabled: provider2.enabled,
983
+ protocol: provider2.protocol,
984
+ channel: provider2.channel,
985
+ base_url: provider2.base_url,
986
+ api_key_env: provider2.api_key_env,
987
+ has_api_key: hasApiKey,
988
+ models: provider2.models,
989
+ capabilities: provider2.capabilities,
990
+ status
991
+ };
992
+ });
993
+ }
994
+
995
+ // src/commands/doctor.ts
996
+ async function runDoctor(options) {
997
+ const config = await loadConfig();
998
+ const providers = inspectProviders(config);
999
+ const configured = providers.some((provider2) => provider2.status === "ok");
1000
+ if (options.json) {
1001
+ console.log(
1002
+ JSON.stringify(
1003
+ {
1004
+ configured,
1005
+ config_path: getConfigPath(),
1006
+ default_preset: config.default_preset,
1007
+ default_mode: config.routing.default_mode,
1008
+ default_provider: config.routing.default_provider,
1009
+ fallback_providers: config.routing.fallback_providers,
1010
+ providers
1011
+ },
1012
+ null,
1013
+ 2
1014
+ )
1015
+ );
1016
+ return;
1017
+ }
1018
+ console.log(`Config: ${getConfigPath()}`);
1019
+ console.log(`Default preset: ${config.default_preset}`);
1020
+ console.log(`Default mode: ${config.routing.default_mode}`);
1021
+ console.log(`Default provider: ${config.routing.default_provider}`);
1022
+ console.log(`Fallback providers: ${config.routing.fallback_providers.join(", ") || "(none)"}`);
1023
+ console.log("");
1024
+ console.log("Providers:");
1025
+ for (const provider2 of providers) {
1026
+ console.log(
1027
+ `- ${provider2.name} [${provider2.protocol}] ${provider2.status} key=${provider2.api_key_env} capabilities=${provider2.capabilities.join(", ")} models=${provider2.models.join(", ")}`
1028
+ );
1029
+ }
1030
+ if (!configured) {
1031
+ console.log("");
1032
+ console.log("No usable provider found. Run `picgen setup` or set the API key env vars above.");
1033
+ }
1034
+ }
1035
+
1036
+ // src/commands/provider.ts
1037
+ import { input, select } from "@inquirer/prompts";
1038
+
1039
+ // src/config/preferences.ts
1040
+ function setPreferredProvider(config, name) {
1041
+ if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
1042
+ const previousDefault = config.routing.default_provider;
1043
+ config.routing.default_provider = name;
1044
+ config.routing.fallback_providers = [
1045
+ .../* @__PURE__ */ new Set([
1046
+ previousDefault,
1047
+ ...config.routing.fallback_providers.filter((providerName) => providerName !== name)
1048
+ ])
1049
+ ].filter((providerName) => providerName && providerName !== name);
1050
+ return config;
1051
+ }
1052
+ function setPreferredMode(config, name) {
1053
+ if (!config.modes[name]) throw new Error(`Unknown mode: ${name}`);
1054
+ config.routing.default_mode = name;
1055
+ return config;
1056
+ }
1057
+ function setPreferredPreset(config, name) {
1058
+ if (!config.presets[name]) throw new Error(`Unknown preset: ${name}`);
1059
+ config.default_preset = name;
1060
+ return config;
1061
+ }
1062
+
1063
+ // src/providers/health.ts
1064
+ async function testProvider(name, provider2) {
1065
+ const base = baseResult(name, provider2);
1066
+ if (!provider2.enabled) {
1067
+ return {
1068
+ ...base,
1069
+ ok: false,
1070
+ status: "disabled",
1071
+ message: "Provider is disabled."
1072
+ };
1073
+ }
1074
+ const apiKey = process.env[provider2.api_key_env];
1075
+ if (!apiKey) {
1076
+ return {
1077
+ ...base,
1078
+ ok: false,
1079
+ status: "missing_api_key",
1080
+ message: `Missing API key environment variable: ${provider2.api_key_env}`
1081
+ };
1082
+ }
1083
+ const model = provider2.test_model ?? provider2.models[0];
1084
+ try {
1085
+ const response = provider2.protocol === "openai-images" ? await testOpenAICompatibleProvider(provider2, apiKey, model) : await testGeminiProvider(provider2, apiKey, model);
1086
+ if (!response.ok) {
1087
+ const message = await readProviderError(response);
1088
+ return {
1089
+ ...base,
1090
+ ok: false,
1091
+ model,
1092
+ status: "provider_error",
1093
+ message: message ?? `Provider check failed: ${response.status} ${response.statusText}`.trim(),
1094
+ http_status: response.status
1095
+ };
1096
+ }
1097
+ return {
1098
+ ...base,
1099
+ ok: true,
1100
+ model,
1101
+ status: "ok",
1102
+ message: "Provider check passed.",
1103
+ http_status: response.status
1104
+ };
1105
+ } catch (error) {
1106
+ return {
1107
+ ...base,
1108
+ ok: false,
1109
+ model,
1110
+ status: "network_error",
1111
+ message: error instanceof Error ? error.message : String(error)
1112
+ };
1113
+ }
1114
+ }
1115
+ function baseResult(name, provider2) {
1116
+ return {
1117
+ ok: false,
1118
+ name,
1119
+ protocol: provider2.protocol,
1120
+ enabled: provider2.enabled,
1121
+ base_url: provider2.base_url,
1122
+ api_key_env: provider2.api_key_env,
1123
+ has_api_key: Boolean(process.env[provider2.api_key_env])
1124
+ };
1125
+ }
1126
+ async function testOpenAICompatibleProvider(provider2, apiKey, model) {
1127
+ return fetch(buildOpenAIProtocolUrl(provider2.base_url, `models/${encodeURIComponent(model)}`), {
1128
+ method: "GET",
1129
+ headers: {
1130
+ Authorization: `Bearer ${apiKey}`
1131
+ }
1132
+ });
1133
+ }
1134
+ async function testGeminiProvider(provider2, apiKey, model) {
1135
+ return fetch(buildGeminiProtocolUrl(provider2.base_url, `models/${encodeURIComponent(model)}:generateContent`), {
1136
+ method: "POST",
1137
+ headers: {
1138
+ "x-goog-api-key": apiKey,
1139
+ "Content-Type": "application/json"
1140
+ },
1141
+ body: JSON.stringify({
1142
+ contents: [
1143
+ {
1144
+ role: "user",
1145
+ parts: [{ text: "Say OK only." }]
1146
+ }
1147
+ ]
1148
+ })
1149
+ });
1150
+ }
1151
+ async function readProviderError(response) {
1152
+ const text = await response.text();
1153
+ if (!text.trim()) return void 0;
1154
+ try {
1155
+ const parsed = JSON.parse(text);
1156
+ const message = parsed.error?.message;
1157
+ if (typeof message === "string") return message;
1158
+ } catch {
1159
+ return text.slice(0, 300);
1160
+ }
1161
+ return text.slice(0, 300);
1162
+ }
1163
+
1164
+ // src/commands/provider.ts
1165
+ async function listProviders() {
1166
+ const config = await loadConfig();
1167
+ for (const [name, provider2] of Object.entries(config.providers)) {
1168
+ const preference = name === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(name) ? "fallback" : "manual";
1169
+ console.log(
1170
+ `${name} ${provider2.enabled ? "enabled" : "disabled"} ${preference} ${provider2.protocol} ${provider2.channel} ${provider2.capabilities.join(",")} ${provider2.models.join(",")}`
1171
+ );
1172
+ }
1173
+ }
1174
+ async function addProvider() {
1175
+ const config = await loadConfig();
1176
+ const provider2 = await promptProvider(config);
1177
+ addProviderToConfig(config, provider2.name, provider2.config);
1178
+ await saveConfig(config);
1179
+ console.log(`Added provider: ${provider2.name}`);
1180
+ }
1181
+ function addProviderToConfig(config, name, provider2) {
1182
+ config.providers[name] = provider2;
1183
+ const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
1184
+ if (!knownProviders.includes(name)) {
1185
+ config.routing.fallback_providers.push(name);
1186
+ }
1187
+ }
1188
+ async function editProvider(name) {
1189
+ const config = await loadConfig();
1190
+ if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
1191
+ const provider2 = await promptProvider(config, name, config.providers[name]);
1192
+ delete config.providers[name];
1193
+ config.providers[provider2.name] = provider2.config;
1194
+ if (config.routing.default_provider === name) {
1195
+ config.routing.default_provider = provider2.name;
1196
+ }
1197
+ config.routing.fallback_providers = config.routing.fallback_providers.map(
1198
+ (item) => item === name ? provider2.name : item
1199
+ );
1200
+ await saveConfig(config);
1201
+ console.log(`Updated provider: ${provider2.name}`);
1202
+ }
1203
+ async function setProviderEnabled(name, enabled) {
1204
+ const config = await loadConfig();
1205
+ const provider2 = config.providers[name];
1206
+ if (!provider2) throw new Error(`Unknown provider: ${name}`);
1207
+ provider2.enabled = enabled;
1208
+ await saveConfig(config);
1209
+ console.log(`${enabled ? "Enabled" : "Disabled"} provider: ${name}`);
1210
+ }
1211
+ async function removeProvider(name) {
1212
+ const config = await loadConfig();
1213
+ if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
1214
+ delete config.providers[name];
1215
+ config.routing.fallback_providers = config.routing.fallback_providers.filter(
1216
+ (item) => item !== name
1217
+ );
1218
+ if (config.routing.default_provider === name) {
1219
+ const [nextDefault, ...remainingFallbacks] = config.routing.fallback_providers;
1220
+ if (!nextDefault) {
1221
+ throw new Error("Cannot remove the default provider because no fallback provider remains.");
1222
+ }
1223
+ config.routing.default_provider = nextDefault;
1224
+ config.routing.fallback_providers = remainingFallbacks;
1225
+ }
1226
+ await saveConfig(config);
1227
+ console.log(`Removed provider: ${name}`);
1228
+ }
1229
+ async function preferProvider(name) {
1230
+ const config = await loadConfig();
1231
+ setPreferredProvider(config, name);
1232
+ await saveConfig(config);
1233
+ console.log(`Preferred provider: ${name}`);
1234
+ }
1235
+ async function runProviderTest(name, options) {
1236
+ const config = await loadConfig();
1237
+ const provider2 = config.providers[name];
1238
+ if (!provider2) throw new Error(`Unknown provider: ${name}`);
1239
+ const result = await testProvider(name, provider2);
1240
+ if (options.json) {
1241
+ console.log(JSON.stringify(result, null, 2));
1242
+ return;
1243
+ }
1244
+ console.log(`${result.ok ? "OK" : "FAILED"} ${result.name} [${result.protocol}]`);
1245
+ console.log(result.message);
1246
+ if (result.model) console.log(`Model: ${result.model}`);
1247
+ if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
1248
+ }
1249
+ async function promptProvider(config, existingName, existing) {
1250
+ const protocol = await select({
1251
+ message: "Select protocol",
1252
+ default: existing?.protocol ?? "openai-images",
1253
+ choices: [
1254
+ { name: "OpenAI-compatible Images API", value: "openai-images" },
1255
+ { name: "Gemini image API", value: "gemini" }
1256
+ ]
1257
+ });
1258
+ const channel = await select({
1259
+ message: "Select channel",
1260
+ default: existing?.channel ?? "official",
1261
+ choices: [
1262
+ { name: "Official", value: "official" },
1263
+ { name: "Third-party proxy / aggregator", value: "third_party" }
1264
+ ]
1265
+ });
1266
+ const defaultName = existingName ?? (protocol === "openai-images" ? channel === "official" ? "openai_official" : "openai_proxy" : channel === "official" ? "gemini_official" : "gemini_proxy");
1267
+ const name = await input({
1268
+ message: "Provider name",
1269
+ default: nextAvailableProviderName(config, defaultName, existingName)
1270
+ });
1271
+ const baseUrl = await input({
1272
+ message: "Provider host URL (do not include /v1 or /v1beta)",
1273
+ default: existing?.base_url ?? defaultProviderBaseUrl(protocol)
1274
+ });
1275
+ const apiKeyEnv = await input({
1276
+ message: "API key environment variable",
1277
+ default: existing?.api_key_env ?? (protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY")
1278
+ });
1279
+ const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
1280
+ const modelsRaw = await input({
1281
+ message: "Models (comma separated)",
1282
+ default: existing?.models.join(",") ?? defaultModels
1283
+ });
1284
+ return {
1285
+ name,
1286
+ config: {
1287
+ enabled: existing?.enabled ?? true,
1288
+ protocol,
1289
+ channel,
1290
+ base_url: normalizeProviderBaseUrl(baseUrl),
1291
+ api_key_env: apiKeyEnv,
1292
+ models: modelsRaw.split(",").map((model) => model.trim()).filter(Boolean),
1293
+ capabilities: defaultCapabilitiesForProtocol2(protocol)
1294
+ }
1295
+ };
1296
+ }
1297
+ function nextAvailableProviderName(config, baseName, existingName) {
1298
+ if (existingName) return existingName;
1299
+ if (!config.providers[baseName]) return baseName;
1300
+ let index = 2;
1301
+ while (config.providers[`${baseName}_${index}`]) index += 1;
1302
+ return `${baseName}_${index}`;
1303
+ }
1304
+ function defaultCapabilitiesForProtocol2(protocol) {
1305
+ return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
1306
+ }
1307
+
1308
+ // src/commands/preferences.ts
1309
+ async function preferMode(name) {
1310
+ const config = await loadConfig();
1311
+ setPreferredMode(config, name);
1312
+ await saveConfig(config);
1313
+ console.log(`Preferred mode: ${name}`);
1314
+ }
1315
+ async function preferPreset(name) {
1316
+ const config = await loadConfig();
1317
+ setPreferredPreset(config, name);
1318
+ await saveConfig(config);
1319
+ console.log(`Preferred preset: ${name}`);
1320
+ }
1321
+
1322
+ // src/commands/quickstart.ts
1323
+ function runQuickstart() {
1324
+ console.log(formatQuickstart());
1325
+ }
1326
+ function formatQuickstart() {
1327
+ return [
1328
+ "PicGen quickstart",
1329
+ "",
1330
+ "Install:",
1331
+ " npm install -g @ai-agent-tools/picgen",
1332
+ "",
1333
+ "Configure:",
1334
+ " picgen setup",
1335
+ " picgen doctor --json",
1336
+ "",
1337
+ "Preview before spending quota:",
1338
+ ' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
1339
+ "",
1340
+ "Generate after confirmation:",
1341
+ ' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
1342
+ "",
1343
+ "Use a reference image:",
1344
+ ' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
1345
+ "",
1346
+ "Agent prompt:",
1347
+ " \u8BF7\u5B89\u88C5\u5E76\u4F53\u9A8C @ai-agent-tools/picgen\uFF1A\u5168\u5C40\u5B89\u88C5 npm install -g @ai-agent-tools/picgen\uFF0C\u8FD0\u884C picgen setup \u914D\u7F6E\uFF0C\u7136\u540E\u5148 dry-run \u9884\u89C8\uFF0C\u518D\u786E\u8BA4\u751F\u6210\u4E00\u5F20\u6D4B\u8BD5\u56FE\u3002\u5982\u679C\u6211\u8981\u7528\u53C2\u8003\u56FE\uFF0C\u8BF7\u4F7F\u7528 --reference <\u56FE\u7247\u8DEF\u5F84>\u3002",
1348
+ "",
1349
+ "Notes:",
1350
+ " - Provider host URLs should not include /v1 or /v1beta.",
1351
+ " - Agent workflows should dry-run before real generation.",
1352
+ " - Generated images are saved locally; do not paste base64 into chat.",
1353
+ " - First-user rollout checklist: docs/release-alpha.md"
1354
+ ].join("\n");
1355
+ }
1356
+
1357
+ // src/commands/setup.ts
1358
+ import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
1359
+ async function runSetup() {
1360
+ await ensureConfig();
1361
+ console.log(`PicGen config: ${getConfigPath()}`);
1362
+ let done = false;
1363
+ while (!done) {
1364
+ console.log("");
1365
+ await printSetupSummary();
1366
+ console.log("");
1367
+ const action = await select2({
1368
+ message: "What do you want to configure?",
1369
+ choices: [
1370
+ { name: "Quick add a common provider/channel", value: "quick-add" },
1371
+ { name: "Choose default provider/channel", value: "provider" },
1372
+ { name: "Choose generation preference", value: "mode" },
1373
+ { name: "Test a provider", value: "test" },
1374
+ { name: "Advanced: add a custom provider/channel", value: "add" },
1375
+ { name: "Finish setup", value: "done" }
1376
+ ]
1377
+ });
1378
+ if (action === "quick-add") {
1379
+ await quickAddProvider();
1380
+ } else if (action === "provider") {
1381
+ await chooseDefaultProvider();
1382
+ } else if (action === "mode") {
1383
+ await chooseDefaultMode();
1384
+ } else if (action === "test") {
1385
+ await chooseProviderToTest();
1386
+ } else if (action === "add") {
1387
+ await addProvider();
1388
+ } else {
1389
+ done = true;
1390
+ console.log("Setup complete.");
1391
+ }
1392
+ }
1393
+ }
1394
+ async function printSetupSummary() {
1395
+ const config = await loadConfig();
1396
+ console.log(`Default provider: ${config.routing.default_provider}`);
1397
+ console.log(`Generation preference: ${modeLabel(config.routing.default_mode)}`);
1398
+ console.log("Providers:");
1399
+ for (const [providerName, provider2] of Object.entries(config.providers)) {
1400
+ const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
1401
+ console.log(
1402
+ `- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
1403
+ );
1404
+ }
1405
+ }
1406
+ async function chooseDefaultProvider() {
1407
+ const config = await loadConfig();
1408
+ const name = await select2({
1409
+ message: "Choose the provider PicGen should use by default",
1410
+ default: config.routing.default_provider,
1411
+ choices: Object.entries(config.providers).map(([providerName, provider2]) => ({
1412
+ name: `${providerName} (${provider2.protocol}, ${provider2.enabled ? "enabled" : "disabled"})`,
1413
+ value: providerName
1414
+ }))
1415
+ });
1416
+ setPreferredProvider(config, name);
1417
+ await saveConfig(config);
1418
+ console.log(`Preferred provider: ${name}`);
1419
+ }
1420
+ async function chooseDefaultMode() {
1421
+ const config = await loadConfig();
1422
+ const name = await select2({
1423
+ message: "Choose the default generation preference",
1424
+ default: config.routing.default_mode,
1425
+ choices: Object.keys(config.modes).map((modeName) => ({
1426
+ name: modeLabel(modeName),
1427
+ value: modeName
1428
+ }))
1429
+ });
1430
+ setPreferredMode(config, name);
1431
+ await saveConfig(config);
1432
+ console.log(`Preferred mode: ${name}`);
1433
+ }
1434
+ async function chooseProviderToTest() {
1435
+ const config = await loadConfig();
1436
+ const name = await select2({
1437
+ message: "Choose a provider to test",
1438
+ default: config.routing.default_provider,
1439
+ choices: Object.keys(config.providers).map((providerName) => ({
1440
+ name: providerName,
1441
+ value: providerName
1442
+ }))
1443
+ });
1444
+ const result = await testProvider(name, config.providers[name]);
1445
+ console.log(`${result.ok ? "OK" : "FAILED"} ${result.name} [${result.protocol}]`);
1446
+ console.log(result.message);
1447
+ if (result.model) console.log(`Model: ${result.model}`);
1448
+ if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
1449
+ }
1450
+ async function quickAddProvider() {
1451
+ const config = await loadConfig();
1452
+ const template = await select2({
1453
+ message: "Choose the provider/channel you want to add",
1454
+ choices: [
1455
+ {
1456
+ name: "Third-party OpenAI-compatible channel",
1457
+ value: "openai_proxy"
1458
+ },
1459
+ {
1460
+ name: "Third-party Gemini channel",
1461
+ value: "gemini_proxy"
1462
+ },
1463
+ {
1464
+ name: "OpenAI official",
1465
+ value: "openai_official"
1466
+ },
1467
+ {
1468
+ name: "Gemini official",
1469
+ value: "gemini_official"
1470
+ }
1471
+ ]
1472
+ });
1473
+ const defaults = quickProviderDefaults(template);
1474
+ const name = await input2({
1475
+ message: "Provider name",
1476
+ default: nextAvailableProviderName(config, defaults.name)
1477
+ });
1478
+ const baseUrl = await input2({
1479
+ message: "Provider host URL (do not include /v1 or /v1beta)",
1480
+ default: defaults.base_url
1481
+ });
1482
+ const apiKeyEnv = await input2({
1483
+ message: "API key environment variable",
1484
+ default: defaults.api_key_env
1485
+ });
1486
+ const modelsRaw = await input2({
1487
+ message: "Models (comma separated, press Enter for recommended defaults)",
1488
+ default: defaults.models.join(",")
1489
+ });
1490
+ const provider2 = {
1491
+ enabled: true,
1492
+ protocol: defaults.protocol,
1493
+ channel: defaults.channel,
1494
+ base_url: normalizeProviderBaseUrl(baseUrl),
1495
+ api_key_env: apiKeyEnv,
1496
+ models: parseModels(modelsRaw),
1497
+ capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
1498
+ };
1499
+ addProviderToConfig(config, name, provider2);
1500
+ const useAsDefault = await confirm2({
1501
+ message: "Use this provider as the default?",
1502
+ default: true
1503
+ });
1504
+ if (useAsDefault) {
1505
+ setPreferredProvider(config, name);
1506
+ }
1507
+ await saveConfig(config);
1508
+ console.log(`Added provider: ${name}`);
1509
+ console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
1510
+ }
1511
+ function quickProviderDefaults(template) {
1512
+ switch (template) {
1513
+ case "openai_proxy":
1514
+ return {
1515
+ name: "openai_proxy",
1516
+ protocol: "openai-images",
1517
+ channel: "third_party",
1518
+ base_url: "https://www.pandai.vip",
1519
+ api_key_env: "OPENAI_API_KEY",
1520
+ models: ["gpt-image-2"]
1521
+ };
1522
+ case "gemini_proxy":
1523
+ return {
1524
+ name: "gemini_proxy",
1525
+ protocol: "gemini",
1526
+ channel: "third_party",
1527
+ base_url: "https://www.pandai.vip",
1528
+ api_key_env: "GEMINI_API_KEY",
1529
+ models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
1530
+ };
1531
+ case "openai_official":
1532
+ return {
1533
+ name: "openai_official",
1534
+ protocol: "openai-images",
1535
+ channel: "official",
1536
+ base_url: defaultProviderBaseUrl("openai-images"),
1537
+ api_key_env: "OPENAI_API_KEY",
1538
+ models: ["gpt-image-2"]
1539
+ };
1540
+ case "gemini_official":
1541
+ return {
1542
+ name: "gemini_official",
1543
+ protocol: "gemini",
1544
+ channel: "official",
1545
+ base_url: defaultProviderBaseUrl("gemini"),
1546
+ api_key_env: "GEMINI_API_KEY",
1547
+ models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
1548
+ };
1549
+ }
1550
+ }
1551
+ function parseModels(raw) {
1552
+ return raw.split(",").map((model) => model.trim()).filter(Boolean);
1553
+ }
1554
+ function providerLabel(provider2) {
1555
+ if (provider2.channel === "official") {
1556
+ return provider2.protocol === "gemini" ? "Gemini official" : "OpenAI-compatible official";
1557
+ }
1558
+ return provider2.protocol === "gemini" ? "Gemini third-party" : "OpenAI-compatible third-party";
1559
+ }
1560
+ function modeLabel(modeName) {
1561
+ switch (modeName) {
1562
+ case "fast":
1563
+ return "fast - quick drafts";
1564
+ case "balanced":
1565
+ return "balanced - recommended";
1566
+ case "premium":
1567
+ return "premium - higher quality";
1568
+ default:
1569
+ return modeName;
1570
+ }
1571
+ }
1572
+
1573
+ // src/cli.ts
1574
+ var program = new Command();
1575
+ program.name("picgen").description("Lightweight image generation connector for AI agents.").version("0.1.0-alpha.0");
1576
+ program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
1577
+ program.command("quickstart").description("Print install and first-run guidance.").action(runQuickstart);
1578
+ program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
1579
+ program.command("create").description("Create an image generation plan or generate images.").argument("<prompt...>", "Prompt text.").option("--dry-run", "Plan generation without calling a provider.").option("--preset <name>", "Preset name.").option("--provider <name>", "Provider name.").option("--mode <name>", "Mode name.").option("--model <name>", "Model name.").option("--out-dir <path>", "Output directory.").option(
1580
+ "--reference <path>",
1581
+ "Reference image path for Gemini generation. Can be repeated.",
1582
+ collectOption,
1583
+ []
1584
+ ).option("--json", "Print machine-readable JSON.").option("-y, --yes", "Skip confirmation for real generation.").action(runCreate);
1585
+ var provider = program.command("provider").description("Manage providers/channels.");
1586
+ provider.command("list").description("List providers.").action(listProviders);
1587
+ provider.command("add").description("Add a provider.").action(addProvider);
1588
+ provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
1589
+ provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
1590
+ provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
1591
+ provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
1592
+ provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
1593
+ provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
1594
+ program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
1595
+ program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
1596
+ program.parseAsync().catch((error) => {
1597
+ console.error(error instanceof Error ? error.message : String(error));
1598
+ process.exitCode = 1;
1599
+ });
1600
+ function collectOption(value, previous) {
1601
+ return [...previous, value];
1602
+ }