@ai-agent-tools/picgen 0.1.0-alpha.10 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import "dotenv/config";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/commands/create.ts
@@ -217,12 +218,12 @@ function padMilliseconds(value) {
217
218
  function slug(value) {
218
219
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
219
220
  }
220
- function redactProviderImageData(value, key2) {
221
+ function redactProviderImageData(value, key) {
221
222
  if (Array.isArray(value)) {
222
223
  return value.map((item) => redactProviderImageData(item));
223
224
  }
224
225
  if (!value || typeof value !== "object") {
225
- return shouldRedactImageDataKey(key2) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
226
+ return shouldRedactImageDataKey(key) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
226
227
  }
227
228
  return Object.fromEntries(
228
229
  Object.entries(value).map(([entryKey, entryValue]) => [
@@ -231,8 +232,8 @@ function redactProviderImageData(value, key2) {
231
232
  ])
232
233
  );
233
234
  }
234
- function shouldRedactImageDataKey(key2) {
235
- return key2 === "b64_json" || key2 === "data" || key2 === "thoughtSignature" || key2 === "thought_signature";
235
+ function shouldRedactImageDataKey(key) {
236
+ return key === "b64_json" || key === "data" || key === "thoughtSignature" || key === "thought_signature";
236
237
  }
237
238
  function redactedProviderDataPlaceholder(value) {
238
239
  return `[redacted provider data: ${value.length} chars]`;
@@ -274,9 +275,9 @@ function mimeTypeFromPath(path) {
274
275
  }
275
276
 
276
277
  // src/config/store.ts
277
- import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
278
- import { dirname as dirname2, join as join3 } from "path";
279
- import { homedir as homedir2 } from "os";
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";
280
281
  import YAML from "yaml";
281
282
 
282
283
  // src/config/defaults.ts
@@ -362,164 +363,6 @@ var defaultConfig = {
362
363
  }
363
364
  };
364
365
 
365
- // src/config/env.ts
366
- import { chmod, mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
367
- import { existsSync } from "fs";
368
- import { createHash } from "crypto";
369
- import { dirname, join as join2, resolve as resolve2 } from "path";
370
- import { homedir } from "os";
371
- import { parse } from "dotenv";
372
- var loadedEnvSources = /* @__PURE__ */ new Map();
373
- function getManagedEnvPath() {
374
- return process.env.PICGEN_ENV_PATH ?? join2(homedir(), ".picgen", ".env");
375
- }
376
- async function loadPicgenEnv() {
377
- const shellEnv = new Set(Object.keys(process.env));
378
- await loadEnvFile(getManagedEnvPath(), shellEnv, false, "managed");
379
- await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true, "project");
380
- }
381
- async function saveManagedEnvVar(name, value) {
382
- const path = getManagedEnvPath();
383
- const current = await readManagedEnvFile(path);
384
- const next = {
385
- ...current,
386
- [name]: value
387
- };
388
- await mkdir2(dirname(path), { recursive: true });
389
- await writeFile2(path, stringifyEnv(next), "utf8");
390
- await chmod(path, 384);
391
- process.env[name] = value;
392
- loadedEnvSources.set(name, { source: "managed", path });
393
- return path;
394
- }
395
- async function inspectEnvVar(name) {
396
- const shellValue = process.env[name];
397
- if (shellValue !== void 0) {
398
- const loadedSource = loadedEnvSources.get(name);
399
- return describeEnvValue(
400
- name,
401
- shellValue,
402
- loadedSource?.source ?? "shell",
403
- loadedSource?.path
404
- );
405
- }
406
- const projectPath = resolve2(process.cwd(), ".env");
407
- const project = await readEnvFile(projectPath);
408
- if (project[name] !== void 0) {
409
- return describeEnvValue(name, project[name], "project", projectPath);
410
- }
411
- const managedPath = getManagedEnvPath();
412
- const managed = await readEnvFile(managedPath);
413
- if (managed[name] !== void 0) {
414
- return describeEnvValue(name, managed[name], "managed", managedPath);
415
- }
416
- return {
417
- name,
418
- set: false
419
- };
420
- }
421
- async function inspectEnvVars(names) {
422
- const uniqueNames = [...new Set(names)];
423
- return Promise.all(uniqueNames.map((name) => inspectEnvVar(name)));
424
- }
425
- async function readEnvVarValue(name) {
426
- if (process.env[name] !== void 0) return process.env[name];
427
- const project = await readEnvFile(resolve2(process.cwd(), ".env"));
428
- if (project[name] !== void 0) return project[name];
429
- const managed = await readEnvFile(getManagedEnvPath());
430
- return managed[name];
431
- }
432
- async function loadEnvFile(path, shellEnv, overrideManagedValues, source) {
433
- if (!existsSync(path)) return;
434
- const parsed = parse(await readFile(path, "utf8"));
435
- for (const [name, value] of Object.entries(parsed)) {
436
- if (shellEnv.has(name)) continue;
437
- if (!overrideManagedValues && process.env[name] !== void 0) continue;
438
- process.env[name] = value;
439
- loadedEnvSources.set(name, { source, path });
440
- }
441
- }
442
- async function readManagedEnvFile(path) {
443
- return readEnvFile(path);
444
- }
445
- async function readEnvFile(path) {
446
- try {
447
- return parse(await readFile(path, "utf8"));
448
- } catch (error) {
449
- if (error.code === "ENOENT") return {};
450
- throw error;
451
- }
452
- }
453
- function stringifyEnv(values) {
454
- return `${Object.entries(values).map(([name, value]) => `${name}=${quoteEnvValue(value)}`).join("\n")}
455
- `;
456
- }
457
- function quoteEnvValue(value) {
458
- if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
459
- return JSON.stringify(value);
460
- }
461
- function describeEnvValue(name, value, source, path) {
462
- return {
463
- name,
464
- set: true,
465
- source,
466
- path,
467
- length: value.length,
468
- preview: maskSecret(value),
469
- fingerprint: createHash("sha256").update(value).digest("hex").slice(0, 12)
470
- };
471
- }
472
- function maskSecret(value) {
473
- if (value.length <= 11) return "*".repeat(value.length);
474
- return `${value.slice(0, 7)}...${value.slice(-4)}`;
475
- }
476
-
477
- // src/config/providerKeys.ts
478
- function nextAvailableProviderApiKeyEnv(config, baseEnv, providerName, existingEnv) {
479
- if (existingEnv) return existingEnv;
480
- const usedEnvs = new Set(Object.values(config.providers).map((provider2) => provider2.api_key_env));
481
- if (!usedEnvs.has(baseEnv)) return baseEnv;
482
- const providerEnv = providerNameToApiKeyEnv(providerName);
483
- if (!usedEnvs.has(providerEnv)) return providerEnv;
484
- let index = 2;
485
- while (usedEnvs.has(`${providerEnv}_${index}`)) index += 1;
486
- return `${providerEnv}_${index}`;
487
- }
488
- function providerNameToApiKeyEnv(providerName) {
489
- const safeName = providerName.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
490
- return `PICGEN_${safeName || "PROVIDER"}_KEY`;
491
- }
492
-
493
- // src/config/migrations.ts
494
- async function migrateConfig(config) {
495
- const migrated = structuredClone(config);
496
- let changed = false;
497
- const usedEnvs = /* @__PURE__ */ new Set();
498
- for (const [providerName, provider2] of Object.entries(migrated.providers)) {
499
- const currentEnv = provider2.api_key_env;
500
- if (!usedEnvs.has(currentEnv)) {
501
- usedEnvs.add(currentEnv);
502
- continue;
503
- }
504
- const nextEnv = nextUniqueProviderEnv(usedEnvs, providerName);
505
- const currentValue = await readEnvVarValue(currentEnv);
506
- provider2.api_key_env = nextEnv;
507
- usedEnvs.add(nextEnv);
508
- changed = true;
509
- if (currentValue && !await readEnvVarValue(nextEnv)) {
510
- await saveManagedEnvVar(nextEnv, currentValue);
511
- }
512
- }
513
- return { config: migrated, changed };
514
- }
515
- function nextUniqueProviderEnv(usedEnvs, providerName) {
516
- const baseEnv = providerNameToApiKeyEnv(providerName);
517
- if (!usedEnvs.has(baseEnv)) return baseEnv;
518
- let index = 2;
519
- while (usedEnvs.has(`${baseEnv}_${index}`)) index += 1;
520
- return `${baseEnv}_${index}`;
521
- }
522
-
523
366
  // src/config/schema.ts
524
367
  import { z } from "zod";
525
368
  var providerSchema = z.object({
@@ -586,19 +429,14 @@ function defaultCapabilitiesForProtocol(protocol) {
586
429
 
587
430
  // src/config/store.ts
588
431
  function getConfigPath() {
589
- return process.env.PICGEN_CONFIG ?? join3(homedir2(), ".picgen", "config.yaml");
432
+ return process.env.PICGEN_CONFIG ?? join2(homedir(), ".picgen", "config.yaml");
590
433
  }
591
434
  async function loadConfig() {
592
435
  const path = getConfigPath();
593
436
  try {
594
- const raw = await readFile2(path, "utf8");
437
+ const raw = await readFile(path, "utf8");
595
438
  const parsed = YAML.parse(raw);
596
- const config = picgenConfigSchema.parse(parsed);
597
- const migrated = await migrateConfig(config);
598
- if (migrated.changed) {
599
- await writeConfigFile(path, migrated.config);
600
- }
601
- return migrated.config;
439
+ return picgenConfigSchema.parse(parsed);
602
440
  } catch (error) {
603
441
  if (error.code === "ENOENT") {
604
442
  return structuredClone(defaultConfig);
@@ -609,11 +447,8 @@ async function loadConfig() {
609
447
  async function saveConfig(config) {
610
448
  const parsed = picgenConfigSchema.parse(config);
611
449
  const path = getConfigPath();
612
- await writeConfigFile(path, parsed);
613
- }
614
- async function writeConfigFile(path, config) {
615
- await mkdir3(dirname2(path), { recursive: true });
616
- await writeFile3(path, YAML.stringify(config), "utf8");
450
+ await mkdir2(dirname(path), { recursive: true });
451
+ await writeFile2(path, YAML.stringify(parsed), "utf8");
617
452
  }
618
453
  async function ensureConfig() {
619
454
  const config = await loadConfig();
@@ -622,54 +457,7 @@ async function ensureConfig() {
622
457
  }
623
458
 
624
459
  // src/providers/gemini.ts
625
- import { readFile as readFile3 } from "fs/promises";
626
-
627
- // src/providers/timeout.ts
628
- var FAST_PROVIDER_TIMEOUT_MS = 12e4;
629
- var DEFAULT_PROVIDER_TIMEOUT_MS = 18e4;
630
- var SLOW_PROVIDER_TIMEOUT_MS = 3e5;
631
- function resolveProviderTimeoutMs(plan) {
632
- const override = parseTimeoutOverride(process.env.PICGEN_PROVIDER_TIMEOUT_MS);
633
- if (override !== void 0) return override;
634
- if (plan.presetName === "fast-draft" || plan.modeName === "fast" || plan.preset.quality === "low") {
635
- return FAST_PROVIDER_TIMEOUT_MS;
636
- }
637
- if (plan.modeName === "premium" || plan.preset.size === "large" || plan.preset.quality === "high") {
638
- return SLOW_PROVIDER_TIMEOUT_MS;
639
- }
640
- return DEFAULT_PROVIDER_TIMEOUT_MS;
641
- }
642
- async function fetchWithProviderTimeout(input3, init, timeoutMs) {
643
- const controller = new AbortController();
644
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
645
- try {
646
- return await fetch(input3, {
647
- ...init,
648
- signal: controller.signal
649
- });
650
- } catch (error) {
651
- if (isAbortError(error)) {
652
- throw new Error(formatProviderTimeoutError(timeoutMs));
653
- }
654
- throw error;
655
- } finally {
656
- clearTimeout(timeout);
657
- }
658
- }
659
- function formatProviderTimeoutError(timeoutMs) {
660
- return `Provider request timed out after ${Math.ceil(
661
- timeoutMs / 1e3
662
- )}s. The provider may still be processing or temporarily unavailable. Try again, use a faster preset, or increase PICGEN_PROVIDER_TIMEOUT_MS.`;
663
- }
664
- function parseTimeoutOverride(value) {
665
- if (!value) return void 0;
666
- const timeoutMs = Number(value);
667
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) return void 0;
668
- return timeoutMs;
669
- }
670
- function isAbortError(error) {
671
- return error instanceof Error && error.name === "AbortError";
672
- }
460
+ import { readFile as readFile2 } from "fs/promises";
673
461
 
674
462
  // src/providers/urls.ts
675
463
  function normalizeProviderBaseUrl(baseUrl) {
@@ -707,20 +495,15 @@ var GeminiAdapter = class {
707
495
  const providerImages = [];
708
496
  const requestCount = Math.max(1, plan.preset.n);
709
497
  const referenceParts = await readReferenceImageParts(plan);
710
- const timeoutMs = resolveProviderTimeoutMs(plan);
711
498
  for (let index = 0; index < requestCount; index += 1) {
712
- const response = await fetchWithProviderTimeout(
713
- buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model),
714
- {
715
- method: "POST",
716
- headers: {
717
- "x-goog-api-key": apiKey,
718
- "Content-Type": "application/json"
719
- },
720
- body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
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"
721
504
  },
722
- timeoutMs
723
- );
505
+ body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
506
+ });
724
507
  const raw = await readJsonResponse(response);
725
508
  if (!response.ok) {
726
509
  throw new Error(formatGeminiError(response.status, response.statusText, raw));
@@ -762,7 +545,7 @@ async function readReferenceImageParts(plan) {
762
545
  plan.referenceImages.map(async (image) => ({
763
546
  inlineData: {
764
547
  mimeType: image.mime_type,
765
- data: (await readFile3(image.path)).toString("base64")
548
+ data: (await readFile2(image.path)).toString("base64")
766
549
  }
767
550
  }))
768
551
  );
@@ -841,18 +624,14 @@ var OpenAIImagesAdapter = class {
841
624
  if (!apiKey) {
842
625
  throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
843
626
  }
844
- const response = await fetchWithProviderTimeout(
845
- buildOpenAIImagesUrl(plan.provider.base_url),
846
- {
847
- method: "POST",
848
- headers: {
849
- Authorization: `Bearer ${apiKey}`,
850
- "Content-Type": "application/json"
851
- },
852
- body: JSON.stringify(buildOpenAIImagesRequest(plan))
627
+ const response = await fetch(buildOpenAIImagesUrl(plan.provider.base_url), {
628
+ method: "POST",
629
+ headers: {
630
+ Authorization: `Bearer ${apiKey}`,
631
+ "Content-Type": "application/json"
853
632
  },
854
- resolveProviderTimeoutMs(plan)
855
- );
633
+ body: JSON.stringify(buildOpenAIImagesRequest(plan))
634
+ });
856
635
  const raw = await readJsonResponse2(response);
857
636
  if (!response.ok) {
858
637
  throw new Error(formatOpenAIImagesError(response.status, response.statusText, raw));
@@ -979,7 +758,7 @@ function getAdapter(protocol) {
979
758
  }
980
759
 
981
760
  // src/routing/resolve.ts
982
- import { join as join4 } from "path";
761
+ import { join as join3 } from "path";
983
762
  import { cwd } from "process";
984
763
  function resolveGenerationPlan(config, options) {
985
764
  const presetName = options.presetName ?? config.default_preset;
@@ -1013,7 +792,7 @@ function resolveGenerationPlan(config, options) {
1013
792
  presetName,
1014
793
  preset,
1015
794
  modeName,
1016
- outputDirectory: options.outputDirectory ?? join4(cwd(), "outputs", "picgen"),
795
+ outputDirectory: options.outputDirectory ?? join3(cwd(), "outputs", "picgen"),
1017
796
  referenceImages: options.referenceImages ?? []
1018
797
  };
1019
798
  }
@@ -1214,13 +993,13 @@ function inspectProviders(config) {
1214
993
  }
1215
994
 
1216
995
  // src/commands/update.ts
1217
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1218
- import { dirname as dirname3, join as join5 } from "path";
1219
- import { homedir as homedir3 } from "os";
996
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
997
+ import { dirname as dirname2, join as join4 } from "path";
998
+ import { homedir as homedir2 } from "os";
1220
999
 
1221
1000
  // src/version.ts
1222
1001
  var PACKAGE_NAME = "@ai-agent-tools/picgen";
1223
- var VERSION = "0.1.0-alpha.10";
1002
+ var VERSION = "0.1.0-alpha.2";
1224
1003
 
1225
1004
  // src/commands/update.ts
1226
1005
  var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -1315,7 +1094,7 @@ async function fetchLatestVersion() {
1315
1094
  }
1316
1095
  async function readFreshCache() {
1317
1096
  try {
1318
- const cache = JSON.parse(await readFile4(getUpdateCachePath(), "utf8"));
1097
+ const cache = JSON.parse(await readFile3(getUpdateCachePath(), "utf8"));
1319
1098
  const checkedAt = Date.parse(cache.checked_at);
1320
1099
  if (!Number.isFinite(checkedAt)) return void 0;
1321
1100
  if (Date.now() - checkedAt > UPDATE_CACHE_TTL_MS) return void 0;
@@ -1327,12 +1106,12 @@ async function readFreshCache() {
1327
1106
  }
1328
1107
  async function writeCache(cache) {
1329
1108
  const path = getUpdateCachePath();
1330
- await mkdir4(dirname3(path), { recursive: true });
1331
- await writeFile4(path, JSON.stringify(cache, null, 2), "utf8");
1109
+ await mkdir3(dirname2(path), { recursive: true });
1110
+ await writeFile3(path, JSON.stringify(cache, null, 2), "utf8");
1332
1111
  }
1333
1112
  function getUpdateCachePath() {
1334
1113
  if (process.env.PICGEN_UPDATE_CACHE_PATH) return process.env.PICGEN_UPDATE_CACHE_PATH;
1335
- return join5(homedir3(), ".picgen", "update-check.json");
1114
+ return join4(homedir2(), ".picgen", "update-check.json");
1336
1115
  }
1337
1116
  function parseVersion(version) {
1338
1117
  const [core, prerelease] = version.split("-", 2);
@@ -1391,84 +1170,6 @@ async function runDoctor(options) {
1391
1170
  await maybePrintUpdateHint();
1392
1171
  }
1393
1172
 
1394
- // src/commands/key.ts
1395
- import { execFile } from "child_process";
1396
- import { promisify } from "util";
1397
- import { password } from "@inquirer/prompts";
1398
- var execFileAsync = promisify(execFile);
1399
- async function setApiKey(name, options) {
1400
- validateEnvName(name);
1401
- const value = options.clipboard ? await readClipboard() : options.stdin ? await readStdin() : options.value ? options.value : await password({
1402
- message: `Paste API key for ${name}`,
1403
- mask: "*"
1404
- });
1405
- if (!value.trim()) {
1406
- throw new Error("API key is empty.");
1407
- }
1408
- const path = await saveManagedEnvVar(name, value.trim());
1409
- console.log(`Saved ${name} to ${path}`);
1410
- }
1411
- async function listApiKeys(options) {
1412
- const config = await loadConfig();
1413
- const names = Object.values(config.providers).map((provider2) => provider2.api_key_env);
1414
- const inspections = await inspectEnvVars(names);
1415
- if (options.json) {
1416
- console.log(JSON.stringify(inspections, null, 2));
1417
- return;
1418
- }
1419
- for (const inspection of inspections) {
1420
- printInspection(inspection);
1421
- }
1422
- }
1423
- async function showApiKey(name, options) {
1424
- validateEnvName(name);
1425
- const inspection = await inspectEnvVar(name);
1426
- if (options.json) {
1427
- console.log(JSON.stringify(inspection, null, 2));
1428
- return;
1429
- }
1430
- printInspection(inspection);
1431
- }
1432
- function validateEnvName(name) {
1433
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1434
- throw new Error(`Invalid environment variable name: ${name}`);
1435
- }
1436
- }
1437
- async function readStdin() {
1438
- const chunks = [];
1439
- for await (const chunk of process.stdin) {
1440
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1441
- }
1442
- return Buffer.concat(chunks).toString("utf8");
1443
- }
1444
- async function readClipboard() {
1445
- try {
1446
- const { stdout } = await execFileAsync("pbpaste");
1447
- return stdout;
1448
- } catch {
1449
- throw new Error("Could not read clipboard. Use --stdin or run picgen key set without flags.");
1450
- }
1451
- }
1452
- function printInspection(inspection) {
1453
- if (!inspection.set) {
1454
- console.log(`${inspection.name}: missing`);
1455
- return;
1456
- }
1457
- const location = inspection.path ? ` ${inspection.path}` : "";
1458
- console.log(
1459
- `${inspection.name}: set source=${inspection.source}${location} length=${inspection.length} preview=${inspection.preview} fingerprint=${inspection.fingerprint}`
1460
- );
1461
- }
1462
-
1463
- // src/commands/open.ts
1464
- import { createServer } from "http";
1465
- import { randomBytes } from "crypto";
1466
- import { execFile as execFile2 } from "child_process";
1467
- import { readdir, readFile as readFile5, stat as stat2 } from "fs/promises";
1468
- import { createReadStream } from "fs";
1469
- import { extname as extname3, join as join6, resolve as resolve3 } from "path";
1470
- import { cwd as cwd2 } from "process";
1471
-
1472
1173
  // src/commands/provider.ts
1473
1174
  import { input, select } from "@inquirer/prompts";
1474
1175
 
@@ -1614,32 +1315,6 @@ async function addProvider() {
1614
1315
  await saveConfig(config);
1615
1316
  console.log(`Added provider: ${provider2.name}`);
1616
1317
  }
1617
- async function quickAddProvider(templateName, options) {
1618
- const config = await loadConfig();
1619
- const template = quickProviderTemplate(templateName);
1620
- const name = options.name ?? nextAvailableProviderName(config, template.name);
1621
- const apiKeyEnv = options.keyEnv ?? nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
1622
- const provider2 = {
1623
- enabled: true,
1624
- protocol: template.protocol,
1625
- channel: template.channel,
1626
- base_url: normalizeProviderBaseUrl(options.host ?? template.base_url),
1627
- api_key_env: apiKeyEnv,
1628
- models: parseModels(options.models ?? template.models.join(",")),
1629
- capabilities: defaultCapabilitiesForProtocol2(template.protocol)
1630
- };
1631
- addProviderToConfig(config, name, provider2);
1632
- if (options.prefer) {
1633
- setPreferredProvider(config, name);
1634
- }
1635
- await saveConfig(config);
1636
- console.log(`Added provider: ${name}`);
1637
- console.log(`Protocol: ${provider2.protocol}`);
1638
- console.log(`Host: ${provider2.base_url}`);
1639
- console.log(`API key env: ${provider2.api_key_env}`);
1640
- console.log(`Models: ${provider2.models.join(",")}`);
1641
- if (options.prefer) console.log(`Preferred provider: ${name}`);
1642
- }
1643
1318
  function addProviderToConfig(config, name, provider2) {
1644
1319
  config.providers[name] = provider2;
1645
1320
  const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
@@ -1736,11 +1411,7 @@ async function promptProvider(config, existingName, existing) {
1736
1411
  });
1737
1412
  const apiKeyEnv = await input({
1738
1413
  message: "API key environment variable",
1739
- default: existing?.api_key_env ?? nextAvailableProviderApiKeyEnv(
1740
- config,
1741
- protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
1742
- name
1743
- )
1414
+ 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")
1744
1415
  });
1745
1416
  const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
1746
1417
  const modelsRaw = await input({
@@ -1770,562 +1441,6 @@ function nextAvailableProviderName(config, baseName, existingName) {
1770
1441
  function defaultCapabilitiesForProtocol2(protocol) {
1771
1442
  return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
1772
1443
  }
1773
- function quickProviderTemplate(templateName) {
1774
- switch (templateName.replaceAll("_", "-")) {
1775
- case "openai-proxy":
1776
- return {
1777
- name: "openai_proxy",
1778
- protocol: "openai-images",
1779
- channel: "third_party",
1780
- base_url: "https://www.pandai.vip",
1781
- api_key_env: "PICGEN_OPENAI_PROXY_KEY",
1782
- models: ["gpt-image-2"]
1783
- };
1784
- case "gemini-proxy":
1785
- return {
1786
- name: "gemini_proxy",
1787
- protocol: "gemini",
1788
- channel: "third_party",
1789
- base_url: "https://www.pandai.vip",
1790
- api_key_env: "PICGEN_GEMINI_PROXY_KEY",
1791
- models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
1792
- };
1793
- case "openai-official":
1794
- return {
1795
- name: "openai_official",
1796
- protocol: "openai-images",
1797
- channel: "official",
1798
- base_url: defaultProviderBaseUrl("openai-images"),
1799
- api_key_env: "OPENAI_API_KEY",
1800
- models: ["gpt-image-2"]
1801
- };
1802
- case "gemini-official":
1803
- return {
1804
- name: "gemini_official",
1805
- protocol: "gemini",
1806
- channel: "official",
1807
- base_url: defaultProviderBaseUrl("gemini"),
1808
- api_key_env: "GEMINI_API_KEY",
1809
- models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
1810
- };
1811
- default:
1812
- throw new Error(
1813
- `Unknown provider template: ${templateName}. Use openai-proxy, gemini-proxy, openai-official, or gemini-official.`
1814
- );
1815
- }
1816
- }
1817
- function parseModels(raw) {
1818
- return raw.split(",").map((model) => model.trim()).filter(Boolean);
1819
- }
1820
-
1821
- // src/commands/open.ts
1822
- var DEFAULT_PORT = 8188;
1823
- var MAX_PORT_ATTEMPTS = 30;
1824
- async function runOpen(options) {
1825
- const token = randomBytes(24).toString("hex");
1826
- const port = await listenOnAvailablePort(Number(options.port ?? DEFAULT_PORT), token);
1827
- const url = `http://127.0.0.1:${port}/?token=${token}`;
1828
- console.log(`PicGen is open: ${url}`);
1829
- console.log("Keep this terminal running while using PicGen. Press Ctrl+C to close.");
1830
- if (options.open !== false) {
1831
- await openBrowser(url);
1832
- }
1833
- }
1834
- async function listenOnAvailablePort(startPort, token) {
1835
- for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
1836
- const port = startPort + offset;
1837
- const server = createServer((request, response) => {
1838
- handleRequest(request, response, token).catch((error) => {
1839
- sendJson(response, 500, {
1840
- ok: false,
1841
- error: error instanceof Error ? error.message : String(error)
1842
- });
1843
- });
1844
- });
1845
- const result = await tryListen(server, port);
1846
- if (result) return port;
1847
- }
1848
- throw new Error(`Could not find an available port starting at ${startPort}.`);
1849
- }
1850
- function tryListen(server, port) {
1851
- return new Promise((resolveListen, reject) => {
1852
- server.once("error", (error) => {
1853
- if (error.code === "EADDRINUSE") {
1854
- resolveListen(false);
1855
- return;
1856
- }
1857
- reject(error);
1858
- });
1859
- server.listen(port, "127.0.0.1", () => resolveListen(true));
1860
- });
1861
- }
1862
- async function handleRequest(request, response, token) {
1863
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
1864
- if (url.pathname === "/" || url.pathname === "/index.html") {
1865
- if (url.searchParams.get("token") !== token) {
1866
- sendText(response, 403, "Invalid PicGen session token.");
1867
- return;
1868
- }
1869
- sendHtml(response, appHtml);
1870
- return;
1871
- }
1872
- if (!isAuthorized(request, url, token)) {
1873
- sendJson(response, 403, { ok: false, error: "Invalid PicGen session token." });
1874
- return;
1875
- }
1876
- if (request.method === "GET" && url.pathname === "/api/state") {
1877
- sendJson(response, 200, await buildState());
1878
- return;
1879
- }
1880
- if (request.method === "POST" && url.pathname === "/api/providers") {
1881
- sendJson(response, 200, await addProviderFromBody(await readJson(request)));
1882
- return;
1883
- }
1884
- const providerTestMatch = /^\/api\/providers\/([^/]+)\/test$/.exec(url.pathname);
1885
- if (request.method === "POST" && providerTestMatch) {
1886
- const config = await loadConfig();
1887
- const name = decodeURIComponent(providerTestMatch[1]);
1888
- const provider2 = config.providers[name];
1889
- if (!provider2) throw new Error(`Unknown provider: ${name}`);
1890
- sendJson(response, 200, await testProvider(name, provider2));
1891
- return;
1892
- }
1893
- const providerDefaultMatch = /^\/api\/providers\/([^/]+)\/default$/.exec(url.pathname);
1894
- if (request.method === "POST" && providerDefaultMatch) {
1895
- const config = await loadConfig();
1896
- const name = decodeURIComponent(providerDefaultMatch[1]);
1897
- setPreferredProvider(config, name);
1898
- await saveConfig(config);
1899
- sendJson(response, 200, await buildState());
1900
- return;
1901
- }
1902
- const providerMatch = /^\/api\/providers\/([^/]+)$/.exec(url.pathname);
1903
- if (providerMatch) {
1904
- const name = decodeURIComponent(providerMatch[1]);
1905
- if (request.method === "PATCH") {
1906
- sendJson(response, 200, await patchProvider(name, await readJson(request)));
1907
- return;
1908
- }
1909
- if (request.method === "DELETE") {
1910
- sendJson(response, 200, await deleteProvider(name));
1911
- return;
1912
- }
1913
- }
1914
- if (request.method === "POST" && url.pathname === "/api/key") {
1915
- const body = await readJson(request);
1916
- if (typeof body.name !== "string" || typeof body.value !== "string") {
1917
- throw new Error("Key name and value are required.");
1918
- }
1919
- await saveManagedEnvVar(body.name, body.value);
1920
- sendJson(response, 200, { ok: true, state: await buildState() });
1921
- return;
1922
- }
1923
- const keyMatch = /^\/api\/key\/([^/]+)$/.exec(url.pathname);
1924
- if (request.method === "GET" && keyMatch) {
1925
- const name = decodeURIComponent(keyMatch[1]);
1926
- sendJson(response, 200, { ok: true, value: await readEnvVarValue(name) });
1927
- return;
1928
- }
1929
- if (request.method === "POST" && url.pathname === "/api/plan") {
1930
- sendJson(response, 200, await planGeneration(await readJson(request)));
1931
- return;
1932
- }
1933
- if (request.method === "POST" && url.pathname === "/api/generate") {
1934
- sendJson(response, 200, await generate(await readJson(request)));
1935
- return;
1936
- }
1937
- if (request.method === "GET" && url.pathname === "/api/history") {
1938
- sendJson(response, 200, { ok: true, runs: await listHistory() });
1939
- return;
1940
- }
1941
- if (request.method === "GET" && url.pathname === "/api/file") {
1942
- await sendOutputFile(response, url.searchParams.get("path"));
1943
- return;
1944
- }
1945
- sendJson(response, 404, { ok: false, error: "Not found." });
1946
- }
1947
- function isAuthorized(request, url, token) {
1948
- return request.headers["x-picgen-token"] === token || url.searchParams.get("token") === token;
1949
- }
1950
- async function buildState() {
1951
- const config = await loadConfig();
1952
- const keyInspections = await inspectEnvVars(
1953
- Object.values(config.providers).map((provider2) => provider2.api_key_env)
1954
- );
1955
- const keys = Object.fromEntries(keyInspections.map((key2) => [key2.name, key2]));
1956
- return {
1957
- ok: true,
1958
- config_path: getConfigPath(),
1959
- key_file_path: getManagedEnvPath(),
1960
- default_provider: config.routing.default_provider,
1961
- fallback_providers: config.routing.fallback_providers,
1962
- default_preset: config.default_preset,
1963
- default_mode: config.routing.default_mode,
1964
- providers: Object.entries(config.providers).map(([name, provider2]) => ({
1965
- name,
1966
- ...provider2,
1967
- key: keys[provider2.api_key_env],
1968
- preference: name === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(name) ? "fallback" : "manual"
1969
- })),
1970
- presets: config.presets,
1971
- modes: config.modes
1972
- };
1973
- }
1974
- async function addProviderFromBody(body) {
1975
- const config = await loadConfig();
1976
- const protocol = body.protocol === "gemini" ? "gemini" : "openai-images";
1977
- const channel = body.channel === "official" ? "official" : "third_party";
1978
- const template = defaultProviderTemplate(protocol, channel);
1979
- const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : nextAvailableProviderName(config, template.name);
1980
- const apiKeyEnv = typeof body.api_key_env === "string" && body.api_key_env.trim() ? body.api_key_env.trim() : nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
1981
- const models = typeof body.models === "string" && body.models.trim() ? parseModels2(body.models) : template.models;
1982
- const provider2 = {
1983
- enabled: true,
1984
- protocol,
1985
- channel,
1986
- base_url: normalizeProviderBaseUrl(String(body.base_url ?? template.base_url)),
1987
- api_key_env: apiKeyEnv,
1988
- models,
1989
- capabilities: defaultCapabilitiesForProtocol2(protocol)
1990
- };
1991
- addProviderToConfig(config, name, provider2);
1992
- if (body.prefer === true || !config.providers[config.routing.default_provider]) {
1993
- setPreferredProvider(config, name);
1994
- }
1995
- await saveConfig(config);
1996
- if (typeof body.api_key === "string" && body.api_key.trim()) {
1997
- await saveManagedEnvVar(apiKeyEnv, body.api_key.trim());
1998
- }
1999
- return { ok: true, state: await buildState() };
2000
- }
2001
- async function patchProvider(name, body) {
2002
- const config = await loadConfig();
2003
- const provider2 = config.providers[name];
2004
- if (!provider2) throw new Error(`Unknown provider: ${name}`);
2005
- if (typeof body.enabled === "boolean") provider2.enabled = body.enabled;
2006
- if (typeof body.base_url === "string") provider2.base_url = normalizeProviderBaseUrl(body.base_url);
2007
- if (typeof body.models === "string") provider2.models = parseModels2(body.models);
2008
- if (typeof body.api_key === "string" && body.api_key.trim()) {
2009
- await saveManagedEnvVar(provider2.api_key_env, body.api_key.trim());
2010
- }
2011
- await saveConfig(config);
2012
- return { ok: true, state: await buildState() };
2013
- }
2014
- async function deleteProvider(name) {
2015
- const config = await loadConfig();
2016
- if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
2017
- delete config.providers[name];
2018
- config.routing.fallback_providers = config.routing.fallback_providers.filter((item) => item !== name);
2019
- if (config.routing.default_provider === name) {
2020
- const nextDefault = config.routing.fallback_providers[0] ?? Object.keys(config.providers)[0];
2021
- if (nextDefault) {
2022
- config.routing.default_provider = nextDefault;
2023
- config.routing.fallback_providers = config.routing.fallback_providers.filter(
2024
- (item) => item !== nextDefault
2025
- );
2026
- }
2027
- }
2028
- await saveConfig(config);
2029
- return { ok: true, state: await buildState() };
2030
- }
2031
- async function planGeneration(body) {
2032
- const config = await loadConfig();
2033
- const plan = resolveGenerationPlan(config, {
2034
- prompt: String(body.prompt ?? "").trim(),
2035
- presetName: asOptionalString(body.preset),
2036
- providerName: asOptionalString(body.provider),
2037
- modeName: asOptionalString(body.mode),
2038
- model: asOptionalString(body.model),
2039
- outputDirectory: asOptionalString(body.output_directory)
2040
- });
2041
- return {
2042
- ok: true,
2043
- dry_run: true,
2044
- provider_called: false,
2045
- plan: toPlanOutput(plan)
2046
- };
2047
- }
2048
- async function generate(body) {
2049
- const config = await loadConfig();
2050
- const plan = resolveGenerationPlan(config, {
2051
- prompt: String(body.prompt ?? "").trim(),
2052
- presetName: asOptionalString(body.preset),
2053
- providerName: asOptionalString(body.provider),
2054
- modeName: asOptionalString(body.mode),
2055
- model: asOptionalString(body.model),
2056
- outputDirectory: asOptionalString(body.output_directory)
2057
- });
2058
- const run = await createGenerationRun(plan);
2059
- const runtimePlan = { ...plan, outputDirectory: run.outputDirectory };
2060
- const runtimePlanOutput = toPlanOutput(runtimePlan);
2061
- await writeGenerationMetadata(run, {
2062
- plan: runtimePlanOutput,
2063
- run: {
2064
- id: run.id,
2065
- output_directory: run.outputDirectory,
2066
- metadata_path: run.metadataPath,
2067
- prompt_path: run.promptPath
2068
- }
2069
- });
2070
- const adapter = getAdapter(plan.provider.protocol);
2071
- try {
2072
- const result = await adapter.generate(runtimePlan, run);
2073
- await writeGenerationMetadata(run, {
2074
- plan: runtimePlanOutput,
2075
- run: {
2076
- id: run.id,
2077
- output_directory: run.outputDirectory,
2078
- metadata_path: run.metadataPath,
2079
- prompt_path: run.promptPath
2080
- },
2081
- provider_response: result.provider_response,
2082
- images: result.images
2083
- });
2084
- return {
2085
- ok: true,
2086
- output_dir: run.outputDirectory,
2087
- metadata_path: run.metadataPath,
2088
- images: result.images
2089
- };
2090
- } catch (error) {
2091
- await writeGenerationMetadata(run, {
2092
- plan: runtimePlanOutput,
2093
- run: {
2094
- id: run.id,
2095
- output_directory: run.outputDirectory,
2096
- metadata_path: run.metadataPath,
2097
- prompt_path: run.promptPath
2098
- },
2099
- error: {
2100
- message: error instanceof Error ? error.message : String(error),
2101
- name: error instanceof Error ? error.name : void 0
2102
- }
2103
- });
2104
- throw error;
2105
- }
2106
- }
2107
- async function listHistory() {
2108
- const baseDir = resolve3(cwd2(), "outputs", "picgen");
2109
- const dates = await safeReaddir(baseDir);
2110
- const runs = [];
2111
- for (const date of dates) {
2112
- const datePath = join6(baseDir, date);
2113
- if (!await isDirectory(datePath)) continue;
2114
- for (const runName of await safeReaddir(datePath)) {
2115
- const runPath = join6(datePath, runName);
2116
- if (!await isDirectory(runPath)) continue;
2117
- const metadataPath = join6(runPath, "metadata.json");
2118
- const promptPath = join6(runPath, "prompt.txt");
2119
- const metadata = await readJsonFile(metadataPath);
2120
- const prompt = await readTextFile(promptPath);
2121
- const images = (metadata?.images ?? []).filter((image) => image.path).map((image) => ({
2122
- ...image,
2123
- url: `/api/file?path=${encodeURIComponent(image.path)}`
2124
- }));
2125
- const info = await stat2(runPath);
2126
- runs.push({
2127
- id: runName,
2128
- date,
2129
- created_at: info.mtime.toISOString(),
2130
- output_dir: runPath,
2131
- metadata_path: metadataPath,
2132
- prompt,
2133
- plan: metadata?.plan,
2134
- images
2135
- });
2136
- }
2137
- }
2138
- return runs.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))).slice(0, 60);
2139
- }
2140
- async function sendOutputFile(response, path) {
2141
- if (!path) {
2142
- sendJson(response, 400, { ok: false, error: "Missing file path." });
2143
- return;
2144
- }
2145
- const resolved = resolve3(path);
2146
- const allowedRoot = resolve3(cwd2(), "outputs", "picgen");
2147
- if (!resolved.startsWith(`${allowedRoot}/`)) {
2148
- sendJson(response, 403, { ok: false, error: "File is outside PicGen outputs." });
2149
- return;
2150
- }
2151
- const extension = extname3(resolved).toLowerCase();
2152
- const mimeType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : extension === ".png" ? "image/png" : "application/octet-stream";
2153
- response.writeHead(200, {
2154
- "Content-Type": mimeType,
2155
- "Cache-Control": "no-store"
2156
- });
2157
- createReadStream(resolved).pipe(response);
2158
- }
2159
- function defaultProviderTemplate(protocol, channel) {
2160
- if (protocol === "gemini") {
2161
- return {
2162
- name: channel === "official" ? "gemini_official" : "gemini_proxy",
2163
- base_url: channel === "official" ? "https://generativelanguage.googleapis.com" : "https://www.pandai.vip",
2164
- api_key_env: channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
2165
- models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
2166
- };
2167
- }
2168
- return {
2169
- name: channel === "official" ? "openai_official" : "openai_proxy",
2170
- base_url: channel === "official" ? "https://api.openai.com" : "https://www.pandai.vip",
2171
- api_key_env: channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY",
2172
- models: ["gpt-image-2"]
2173
- };
2174
- }
2175
- function parseModels2(raw) {
2176
- return raw.split(",").map((item) => item.trim()).filter(Boolean);
2177
- }
2178
- function asOptionalString(value) {
2179
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2180
- }
2181
- async function readJson(request) {
2182
- const chunks = [];
2183
- for await (const chunk of request) {
2184
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2185
- }
2186
- const text = Buffer.concat(chunks).toString("utf8");
2187
- if (!text.trim()) return {};
2188
- return JSON.parse(text);
2189
- }
2190
- async function safeReaddir(path) {
2191
- try {
2192
- return await readdir(path);
2193
- } catch {
2194
- return [];
2195
- }
2196
- }
2197
- async function isDirectory(path) {
2198
- try {
2199
- return (await stat2(path)).isDirectory();
2200
- } catch {
2201
- return false;
2202
- }
2203
- }
2204
- async function readJsonFile(path) {
2205
- try {
2206
- return JSON.parse(await readFile5(path, "utf8"));
2207
- } catch {
2208
- return void 0;
2209
- }
2210
- }
2211
- async function readTextFile(path) {
2212
- try {
2213
- return await readFile5(path, "utf8");
2214
- } catch {
2215
- return void 0;
2216
- }
2217
- }
2218
- function sendJson(response, status, value) {
2219
- response.writeHead(status, {
2220
- "Content-Type": "application/json; charset=utf-8",
2221
- "Cache-Control": "no-store"
2222
- });
2223
- response.end(JSON.stringify(value, null, 2));
2224
- }
2225
- function sendHtml(response, value) {
2226
- response.writeHead(200, {
2227
- "Content-Type": "text/html; charset=utf-8",
2228
- "Cache-Control": "no-store"
2229
- });
2230
- response.end(value);
2231
- }
2232
- function sendText(response, status, value) {
2233
- response.writeHead(status, {
2234
- "Content-Type": "text/plain; charset=utf-8",
2235
- "Cache-Control": "no-store"
2236
- });
2237
- response.end(value);
2238
- }
2239
- async function openBrowser(url) {
2240
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2241
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2242
- await new Promise((resolveOpen) => {
2243
- execFile2(command, args, () => resolveOpen());
2244
- });
2245
- }
2246
- var appHtml = `<!doctype html>
2247
- <html lang="zh-CN">
2248
- <head>
2249
- <meta charset="utf-8">
2250
- <meta name="viewport" content="width=device-width, initial-scale=1">
2251
- <title>PicGen</title>
2252
- <style>
2253
- :root{--bg:#f6f7f9;--panel:#fff;--text:#17202a;--muted:#667085;--border:#d9dee7;--soft:#eef2f6;--accent:#2563eb;--accent2:#0f766e;--danger:#b42318;--ok:#087443;--warn:#b54708;--radius:8px}
2254
- *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,select,textarea{font:inherit}button{border:1px solid var(--border);background:#fff;color:var(--text);border-radius:7px;padding:7px 10px;cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.ghost{background:transparent}button.danger{color:var(--danger)}button:disabled{opacity:.55;cursor:not-allowed}input,select,textarea{width:100%;border:1px solid var(--border);border-radius:7px;padding:8px 9px;background:#fff;color:var(--text)}textarea{min-height:120px;resize:vertical}.wrap{max-width:1120px;margin:0 auto;padding:22px}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.brand h1{font-size:22px;margin:0 0 3px}.brand p,.muted{color:var(--muted);margin:0}.notice{border:1px solid #bfdbfe;background:#eff6ff;color:#1e3a8a;border-radius:var(--radius);padding:10px 12px;margin-bottom:16px}.tabs{display:flex;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--border)}.tab{border:0;border-radius:7px 7px 0 0;background:transparent;padding:9px 12px}.tab.active{background:#fff;border:1px solid var(--border);border-bottom-color:#fff;margin-bottom:-1px}.grid{display:grid;gap:14px}.cols{display:grid;grid-template-columns:1fr 360px;gap:14px}.panel{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px}.panel h2{font-size:15px;margin:0 0 12px}.row{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:center;margin:9px 0}.actions{display:flex;gap:8px;flex-wrap:wrap}.provider{display:grid;grid-template-columns:1fr auto;gap:12px;border:1px solid var(--border);border-radius:var(--radius);padding:12px;background:#fff}.provider+.provider{margin-top:10px}.title{font-weight:650}.badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.badge{display:inline-flex;align-items:center;border-radius:999px;background:var(--soft);color:#344054;font-size:12px;padding:2px 8px}.badge.ok{background:#dcfae6;color:var(--ok)}.badge.warn{background:#fef0c7;color:var(--warn)}.badge.default{background:#dbeafe;color:#1d4ed8}.keyline{margin-top:8px;color:var(--muted)}.keyline code,.path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f2f4f7;border-radius:5px;padding:2px 5px}.source{display:inline-flex;align-items:center;border-radius:5px;padding:2px 6px;background:#ecfdf3;color:#067647;font-size:12px;margin-left:4px}.source.project{background:#fff7ed;color:#9a3412}.source.shell{background:#eef4ff;color:#3538cd}.source.missing{background:#fef3f2;color:#b42318}.hint{margin-top:8px;color:var(--muted);font-size:12px}.warning{margin-top:8px;color:var(--warn);font-size:12px}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:31px;height:31px;padding:0;vertical-align:middle}.icon-btn svg{width:16px;height:16px;stroke:currentColor}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.full{grid-column:1/-1}.plan{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}.run img,.result img{width:100%;height:150px;object-fit:contain;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius)}.run{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.hidden{display:none}.toast{position:fixed;right:18px;bottom:18px;background:#111827;color:#fff;border-radius:8px;padding:10px 12px;max-width:360px}.small{font-size:12px}.split{display:flex;justify-content:space-between;gap:8px;align-items:center}@media(max-width:860px){.cols{grid-template-columns:1fr}.row{grid-template-columns:1fr}.formgrid{grid-template-columns:1fr}.provider{grid-template-columns:1fr}.top{display:block}.wrap{padding:14px}}
2255
- </style>
2256
- </head>
2257
- <body>
2258
- <main class="wrap">
2259
- <div class="top">
2260
- <div class="brand"><h1>PicGen</h1><p>\u672C\u5730\u751F\u56FE\u5DE5\u4F5C\u53F0</p></div>
2261
- <div class="actions"><button id="refresh">\u5237\u65B0</button></div>
2262
- </div>
2263
- <div class="notice">\u8FD9\u662F\u672C\u673A\u4E34\u65F6\u9875\u9762\uFF0C\u53EA\u7ED1\u5B9A 127.0.0.1\u3002\u4F7F\u7528\u5B8C\u53EF\u4EE5\u5173\u95ED\u9875\u9762\uFF0C\u5E76\u5728\u542F\u52A8 PicGen \u7684\u7EC8\u7AEF\u6309 Ctrl+C \u9000\u51FA\u670D\u52A1\u3002</div>
2264
- <nav class="tabs">
2265
- <button class="tab active" data-tab="settings">\u914D\u7F6E</button>
2266
- <button class="tab" data-tab="generate">\u751F\u6210</button>
2267
- <button class="tab" data-tab="history">\u5386\u53F2</button>
2268
- </nav>
2269
- <section id="settings" class="grid"></section>
2270
- <section id="generate" class="hidden"></section>
2271
- <section id="history" class="hidden grid"></section>
2272
- </main>
2273
- <div id="toast" class="toast hidden"></div>
2274
- <script>
2275
- const token = new URLSearchParams(location.search).get('token');
2276
- const headers = {'Content-Type':'application/json','x-picgen-token':token};
2277
- const state = {data:null,plan:null};
2278
- const $ = (s,p=document)=>p.querySelector(s);
2279
- const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
2280
- function toast(msg){const el=$('#toast');el.textContent=msg;el.classList.remove('hidden');setTimeout(()=>el.classList.add('hidden'),3500)}
2281
- async function api(path, opts={}){const res=await fetch(path,{...opts,headers:{...headers,...opts.headers}});const data=await res.json().catch(()=>({ok:false,error:'Invalid JSON'}));if(!res.ok||data.ok===false)throw new Error(data.error||data.message||res.statusText);return data}
2282
- async function load(){state.data=await api('/api/state');renderSettings();renderGenerate();await renderHistory()}
2283
- function tab(name){document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));['settings','generate','history'].forEach(id=>$('#'+id).classList.toggle('hidden',id!==name))}
2284
- document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>tab(b.dataset.tab));$('#refresh').onclick=()=>load().then(()=>toast('\u5DF2\u5237\u65B0')).catch(e=>toast(e.message));
2285
- const labels={default:'\u9ED8\u8BA4',fallback:'\u5907\u7528',manual:'\u624B\u52A8',official:'\u5B98\u65B9',third_party:'\u7B2C\u4E09\u65B9','openai-images':'OpenAI \u517C\u5BB9',gemini:'Gemini',enabled:'\u5DF2\u542F\u7528',disabled:'\u5DF2\u505C\u7528',shell:'\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF',project:'\u5F53\u524D\u9879\u76EE .env',managed:'PicGen \u7BA1\u7406\u6587\u4EF6',missing:'\u672A\u914D\u7F6E'};
2286
- const eyeIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
2287
- const eyeOffIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m2 2 20 20"/><path d="M10.6 10.6A3 3 0 0 0 13.4 13.4"/><path d="M9.9 4.2A10.4 10.4 0 0 1 12 4.0c6.5 0 10 8 10 8a17.8 17.8 0 0 1-2.3 3.5"/><path d="M6.6 6.6C3.6 8.7 2 12 2 12s3.5 8 10 8c1.6 0 3-.3 4.2-.9"/></svg>';
2288
- function label(v){return labels[v]||v}
2289
- function keySource(key){return key?.set?label(key.source):'\u672A\u914D\u7F6E'}
2290
- function templateDefaults(template){return {gemini_proxy:{protocol:'gemini',channel:'third_party',name:'gemini_proxy',host:'https://www.pandai.vip',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_proxy:{protocol:'openai-images',channel:'third_party',name:'openai_proxy',host:'https://www.pandai.vip',models:'gpt-image-2'},gemini_official:{protocol:'gemini',channel:'official',name:'gemini_official',host:'https://generativelanguage.googleapis.com',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_official:{protocol:'openai-images',channel:'official',name:'openai_official',host:'https://api.openai.com',models:'gpt-image-2'}}[template]}
2291
- function renderSettings(){const s=state.data;$('#settings').innerHTML=\`
2292
- <div class="panel"><h2>\u8DEF\u5F84</h2><div class="row"><div>\u914D\u7F6E\u6587\u4EF6</div><div class="path">\${esc(s.config_path)}</div></div><div class="row"><div>PicGen \u5BC6\u94A5\u6587\u4EF6</div><div class="path">\${esc(s.key_file_path)}</div></div><p class="hint">key \u8BFB\u53D6\u4F18\u5148\u7EA7\uFF1A\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF &gt; \u5F53\u524D\u9879\u76EE .env &gt; PicGen \u7BA1\u7406\u6587\u4EF6\u3002</p></div>
2293
- <div class="panel"><div class="split"><h2>\u6E20\u9053</h2><button class="primary" id="addProviderBtn">\u6DFB\u52A0\u6E20\u9053</button></div>
2294
- <div class="hidden" id="providerForm" style="margin:12px 0 14px"><h2>\u6DFB\u52A0\u6E20\u9053</h2>
2295
- <div class="formgrid">
2296
- <label class="full">\u6E20\u9053\u7C7B\u578B<select id="newTemplate"><option value="gemini_proxy">\u7B2C\u4E09\u65B9 Gemini</option><option value="openai_proxy">\u7B2C\u4E09\u65B9 OpenAI \u517C\u5BB9</option><option value="gemini_official">\u5B98\u65B9 Gemini</option><option value="openai_official">\u5B98\u65B9 OpenAI</option></select></label>
2297
- <label>\u540D\u79F0<input id="newName" placeholder="\u81EA\u52A8"></label>
2298
- <label>Host<input id="newHost" value="https://www.pandai.vip"></label>
2299
- <label class="full">API key<input id="newKey" type="password" placeholder="\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4E0D\u5199\u5165\u804A\u5929"></label>
2300
- <p class="hint full">\u6BCF\u4E2A\u65B0\u6E20\u9053\u4F1A\u81EA\u52A8\u5206\u914D\u72EC\u7ACB\u7684 key \u540D\u79F0\uFF0C\u907F\u514D\u591A\u4E2A\u6E20\u9053\u4E92\u76F8\u8986\u76D6\u3002</p>
2301
- <label class="full">\u6A21\u578B\u5217\u8868<input id="newModels" placeholder="\u4F7F\u7528\u63A8\u8350\u9ED8\u8BA4\u503C"></label>
2302
- <label><input id="newPrefer" type="checkbox" checked style="width:auto"> \u8BBE\u4E3A\u9ED8\u8BA4\u6E20\u9053</label>
2303
- <div class="actions"><button class="primary" id="saveProvider">\u4FDD\u5B58\u6E20\u9053</button><button id="cancelProvider">\u53D6\u6D88</button></div>
2304
- </div>
2305
- </div><div id="providers"></div></div>\`;
2306
- $('#addProviderBtn').onclick=()=>{const form=$('#providerForm');form.classList.remove('hidden');form.scrollIntoView({block:'nearest'});};$('#cancelProvider').onclick=()=>$('#providerForm').classList.add('hidden');$('#saveProvider').onclick=saveProvider;$('#newTemplate').onchange=applyProviderTemplate;applyProviderTemplate();
2307
- $('#providers').innerHTML=s.providers.map(providerCard).join('') || '<p class="muted">\u8FD8\u6CA1\u6709\u914D\u7F6E\u6E20\u9053\u3002</p>';
2308
- bindProviderActions();
2309
- }
2310
- function providerCard(p){const key=p.key||{set:false};const source=key.set?\`<span class="source \${esc(key.source)}">\${esc(keySource(key))}</span>\`:'<span class="source missing">\u672A\u914D\u7F6E</span>';const sourcePath=key.path?\`<div class="keyline small">\u6765\u6E90\u8DEF\u5F84 <span class="path">\${esc(key.path)}</span></div>\`:'';const officialHint=p.channel==='official'?'<div class="warning">\u5B98\u65B9\u6E20\u9053\u901A\u5E38\u9700\u8981\u5B98\u65B9 API key\uFF1B\u5982\u679C\u8FD9\u91CC\u653E\u7684\u662F\u7B2C\u4E09\u65B9\u6E20\u9053 key\uFF0C\u6D4B\u8BD5\u53EF\u80FD\u5931\u8D25\u3002</div>':'';return \`<div class="provider"><div><div class="title">\${esc(p.name)}</div><div class="badges"><span class="badge \${p.preference==='default'?'default':''}">\${esc(label(p.preference))}</span><span class="badge">\${esc(label(p.protocol))}</span><span class="badge">\${esc(label(p.channel))}</span><span class="badge \${p.enabled?'ok':'warn'}">\${p.enabled?'\u5DF2\u542F\u7528':'\u5DF2\u505C\u7528'}</span></div><div class="keyline">\${esc(p.base_url)}</div><div class="keyline">Key <code>\${esc(p.api_key_env)}</code>: \${key.set?\`<code data-key-preview="\${esc(p.api_key_env)}">\${esc(key.preview)}</code> <button class="icon-btn" title="\u663E\u793A\u5B8C\u6574 key" aria-label="\u663E\u793A\u5B8C\u6574 key" data-reveal="\${esc(p.api_key_env)}" data-visible="false">\${eyeIcon}</button>\`:'\u672A\u914D\u7F6E'} \${source} \${key.fingerprint?'<span class="small">fingerprint '+esc(key.fingerprint)+'</span>':''}</div>\${sourcePath}\${officialHint}</div><div class="actions"><button data-test="\${esc(p.name)}">\u6D4B\u8BD5</button><button data-default="\${esc(p.name)}">\u8BBE\u4E3A\u9ED8\u8BA4</button><button data-toggle="\${esc(p.name)}">\${p.enabled?'\u505C\u7528':'\u542F\u7528'}</button><button class="danger" data-delete="\${esc(p.name)}">\u79FB\u9664</button></div></div>\`}
2311
- function bindProviderActions(){document.querySelectorAll('[data-test]').forEach(b=>b.onclick=()=>testProvider(b.dataset.test));document.querySelectorAll('[data-default]').forEach(b=>b.onclick=()=>post('/api/providers/'+encodeURIComponent(b.dataset.default)+'/default',{}));document.querySelectorAll('[data-toggle]').forEach(b=>{const p=state.data.providers.find(x=>x.name===b.dataset.toggle);b.onclick=()=>patch('/api/providers/'+encodeURIComponent(p.name),{enabled:!p.enabled})});document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>confirm('\u786E\u8BA4\u79FB\u9664\u8FD9\u4E2A\u6E20\u9053\uFF1F')&&del('/api/providers/'+encodeURIComponent(b.dataset.delete)));document.querySelectorAll('[data-reveal]').forEach(b=>b.onclick=()=>toggleKey(b.dataset.reveal,b))}
2312
- function applyProviderTemplate(){const t=templateDefaults($('#newTemplate').value);$('#newName').placeholder=t.name;$('#newHost').value=t.host;$('#newModels').placeholder=t.models}
2313
- async function saveProvider(){const t=templateDefaults($('#newTemplate').value);await post('/api/providers',{protocol:t.protocol,channel:t.channel,name:$('#newName').value,base_url:$('#newHost').value,api_key:$('#newKey').value,models:$('#newModels').value,prefer:$('#newPrefer').checked});$('#providerForm').classList.add('hidden');toast('\u6E20\u9053\u5DF2\u4FDD\u5B58')}
2314
- async function testProvider(name){const r=await api('/api/providers/'+encodeURIComponent(name)+'/test',{method:'POST'});toast((r.ok?'\u6D4B\u8BD5\u901A\u8FC7\uFF1A':'\u6D4B\u8BD5\u5931\u8D25\uFF1A')+r.message)}
2315
- async function toggleKey(name,btn){const slot=document.querySelector('[data-key-preview="'+CSS.escape(name)+'"]');if(btn.dataset.visible==='true'){const key=state.data.providers.find(p=>p.api_key_env===name)?.key;slot.textContent=key?.preview||'';btn.dataset.visible='false';btn.title='\u663E\u793A\u5B8C\u6574 key';btn.setAttribute('aria-label','\u663E\u793A\u5B8C\u6574 key');btn.innerHTML=eyeIcon;return}const r=await api('/api/key/'+encodeURIComponent(name));slot.textContent=r.value||'';btn.dataset.visible='true';btn.title='\u9690\u85CF key';btn.setAttribute('aria-label','\u9690\u85CF key');btn.innerHTML=eyeOffIcon}
2316
- async function post(path,body){await api(path,{method:'POST',body:JSON.stringify(body)});await load()}
2317
- async function patch(path,body){await api(path,{method:'PATCH',body:JSON.stringify(body)});await load()}
2318
- async function del(path){await api(path,{method:'DELETE'});await load()}
2319
- function renderGenerate(){const s=state.data;const prefs=JSON.parse(localStorage.getItem('picgen:prefs')||'{}');$('#generate').innerHTML=\`<div class="cols"><div class="panel"><h2>\u751F\u6210\u56FE\u7247</h2><label>\u63D0\u793A\u8BCD<textarea id="prompt">\${esc(prefs.prompt||'\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE\uFF0C\u767D\u8272\u80CC\u666F\uFF0C\u5C11\u91CF\u84DD\u7EFF\u8272\u79D1\u6280\u611F\u70B9\u7F00')}</textarea></label><div class="formgrid"><label>\u6E20\u9053<select id="genProvider"><option value="">\u81EA\u52A8\u9009\u62E9</option>\${s.providers.map(p=>'<option value="'+esc(p.name)+'" '+(prefs.provider===p.name?'selected':'')+'>'+esc(p.name)+'</option>').join('')}</select></label><label>\u9884\u8BBE<select id="genPreset">\${Object.keys(s.presets).map(p=>'<option '+((prefs.preset||'fast-draft')===p?'selected':'')+'>'+esc(p)+'</option>').join('')}</select></label><label>\u6A21\u578B<input id="genModel" value="\${esc(prefs.model||'')}" placeholder="\u53EF\u9009"></label><label>\u6A21\u5F0F<input id="genMode" value="\${esc(prefs.mode||'')}" placeholder="\u53EF\u9009"></label></div><div class="actions" style="margin-top:12px"><button id="preview" class="primary">\u9884\u89C8\u65B9\u6848</button><button id="generateBtn" disabled>\u5F00\u59CB\u751F\u6210</button></div></div><div class="panel"><h2>\u65B9\u6848 / \u7ED3\u679C</h2><div id="plan" class="plan">\u8FD8\u6CA1\u6709\u9884\u89C8\u3002</div><div id="result" class="result" style="margin-top:12px"></div></div></div>\`;$('#preview').onclick=preview;$('#generateBtn').onclick=generateNow}
2320
- function genBody(){const body={prompt:$('#prompt').value,preset:$('#genPreset').value,provider:$('#genProvider').value,model:$('#genModel').value,mode:$('#genMode').value};localStorage.setItem('picgen:prefs',JSON.stringify(body));return body}
2321
- async function preview(){const r=await api('/api/plan',{method:'POST',body:JSON.stringify(genBody())});state.plan=r.plan;$('#plan').textContent=JSON.stringify(r.plan,null,2);$('#generateBtn').disabled=false}
2322
- async function generateNow(){if(!state.plan&&!confirm('\u8FD8\u6CA1\u6709\u9884\u89C8\u65B9\u6848\uFF0C\u786E\u5B9A\u76F4\u63A5\u751F\u6210\uFF1F'))return;$('#generateBtn').disabled=true;$('#result').innerHTML='<p class="muted">\u6B63\u5728\u751F\u6210...</p>';try{const r=await api('/api/generate',{method:'POST',body:JSON.stringify(genBody())});$('#result').innerHTML='<p>\u5DF2\u4FDD\u5B58\u5230 <span class="path">'+esc(r.output_dir)+'</span></p>'+r.images.map(img=>'<img src="/api/file?path='+encodeURIComponent(img.path)+'&token='+token+'"><div class="path">'+esc(img.path)+'</div>').join('');await renderHistory()}catch(e){$('#result').innerHTML='<p style="color:var(--danger)">'+esc(e.message)+'</p>'}finally{$('#generateBtn').disabled=false}}
2323
- async function renderHistory(){const h=$('#history');if(!state.data)return;const r=await api('/api/history');h.innerHTML='<div class="panel"><h2>\u5386\u53F2\u8BB0\u5F55</h2><p class="muted">\u627E\u56DE\u6700\u8FD1\u751F\u6210\u7684\u56FE\u7247\u548C\u672C\u5730\u4FDD\u5B58\u8DEF\u5F84\u3002</p></div><div class="gallery">'+r.runs.map(runCard).join('')+'</div>'}
2324
- function runCard(r){const img=(r.images&&r.images[0])?'<img src="'+r.images[0].url+'&token='+token+'">':'';const plan=r.plan||{};return '<div class="run">'+img+'<div class="title">'+esc((r.prompt||'\u672A\u547D\u540D').slice(0,60))+'</div><div class="muted small">'+esc(new Date(r.created_at).toLocaleString())+' \xB7 '+esc(plan.provider||'')+' \xB7 '+esc(plan.preset||'')+'</div><div class="path small">'+esc(r.output_dir)+'</div><div class="actions" style="margin-top:8px"><button onclick="navigator.clipboard.writeText(\\''+esc(String(r.output_dir)).replaceAll("'","\\\\'")+'\\')">\u590D\u5236\u6587\u4EF6\u5939</button></div></div>'}
2325
- load().catch(e=>toast(e.message));
2326
- </script>
2327
- </body>
2328
- </html>`;
2329
1444
 
2330
1445
  // src/commands/preferences.ts
2331
1446
  async function preferMode(name) {
@@ -2351,39 +1466,26 @@ function formatQuickstart() {
2351
1466
  "PicGen quickstart",
2352
1467
  "",
2353
1468
  "Install:",
2354
- " node -v",
2355
- " npm -v",
2356
1469
  " npm install -g @ai-agent-tools/picgen",
2357
- " npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy",
2358
- " picgen skill install codex",
2359
- "",
2360
- "Open local web UI:",
2361
- " picgen open",
2362
1470
  "",
2363
1471
  "Configure:",
2364
- " picgen setup # can save provider API keys for you",
2365
- " picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer",
2366
- " picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard",
2367
- " picgen key set PICGEN_GEMINI_PROXY_KEY --stdin",
2368
- " picgen key list --json",
1472
+ " picgen setup",
2369
1473
  " picgen doctor --json",
2370
1474
  "",
2371
1475
  "Preview before spending quota:",
2372
- ' picgen create --dry-run --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1476
+ ' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
2373
1477
  "",
2374
1478
  "Generate after confirmation:",
2375
- ' picgen create --yes --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1479
+ ' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
2376
1480
  "",
2377
1481
  "Use a reference image:",
2378
1482
  ' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
2379
1483
  "",
2380
1484
  "Agent prompt:",
2381
- " \u8BF7\u5E2E\u6211\u5B89\u88C5\u5E76\u914D\u7F6E PicGen \u751F\u56FE\u5DE5\u5177\u3002\u8BF7\u5148\u9605\u8BFB\u5E76\u6309\u8FD9\u4E2A\u6307\u5357\u6267\u884C\uFF1Ahttps://raw.githubusercontent.com/ai-agent-tools/picgen/refs/heads/main/docs/agent-install.md \u3002\u4F60\u8D1F\u8D23\u5224\u65AD\u662F\u5426\u5728\u672C\u673A\u6301\u4E45\u73AF\u5883\u3001\u5B89\u88C5 CLI \u548C skill\u3001\u5F15\u5BFC\u6211\u914D\u7F6E provider/API key\uFF0C\u5E76\u5148\u9884\u89C8\u751F\u6210\u65B9\u6848\uFF0C\u7B49\u6211\u786E\u8BA4\u540E\u518D\u751F\u6210\u6D4B\u8BD5\u56FE\u3002\u4E0D\u8981\u8BA9\u6211\u7406\u89E3\u547D\u4EE4\u7EC6\u8282\uFF0C\u4E5F\u4E0D\u8981\u8BA9\u6211\u628A API key \u53D1\u5230\u804A\u5929\u91CC\u3002",
1485
+ " \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",
2382
1486
  "",
2383
1487
  "Notes:",
2384
1488
  " - Provider host URLs should not include /v1 or /v1beta.",
2385
- " - picgen setup can store API keys in ~/.picgen/.env.",
2386
- " - picgen open starts a foreground local web UI on 127.0.0.1:8188.",
2387
1489
  " - Agent workflows should dry-run before real generation.",
2388
1490
  " - Generated images are saved locally; do not paste base64 into chat.",
2389
1491
  " - First-user rollout checklist: docs/release-alpha.md"
@@ -2391,7 +1493,7 @@ function formatQuickstart() {
2391
1493
  }
2392
1494
 
2393
1495
  // src/commands/setup.ts
2394
- import { confirm as confirm2, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
1496
+ import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
2395
1497
  async function runSetup() {
2396
1498
  await ensureConfig();
2397
1499
  console.log(`PicGen config: ${getConfigPath()}`);
@@ -2406,20 +1508,17 @@ async function runSetup() {
2406
1508
  { name: "Quick add a common provider/channel", value: "quick-add" },
2407
1509
  { name: "Choose default provider/channel", value: "provider" },
2408
1510
  { name: "Choose generation preference", value: "mode" },
2409
- { name: "Configure API key", value: "key" },
2410
1511
  { name: "Test a provider", value: "test" },
2411
1512
  { name: "Advanced: add a custom provider/channel", value: "add" },
2412
1513
  { name: "Finish setup", value: "done" }
2413
1514
  ]
2414
1515
  });
2415
1516
  if (action === "quick-add") {
2416
- await quickAddProvider2();
1517
+ await quickAddProvider();
2417
1518
  } else if (action === "provider") {
2418
1519
  await chooseDefaultProvider();
2419
1520
  } else if (action === "mode") {
2420
1521
  await chooseDefaultMode();
2421
- } else if (action === "key") {
2422
- await chooseProviderKeyToConfigure();
2423
1522
  } else if (action === "test") {
2424
1523
  await chooseProviderToTest();
2425
1524
  } else if (action === "add") {
@@ -2438,7 +1537,7 @@ async function printSetupSummary() {
2438
1537
  for (const [providerName, provider2] of Object.entries(config.providers)) {
2439
1538
  const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
2440
1539
  console.log(
2441
- `- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, key=${provider2.api_key_env}${process.env[provider2.api_key_env] ? " set" : " missing"}, capabilities=${provider2.capabilities.join(",")}`
1540
+ `- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
2442
1541
  );
2443
1542
  }
2444
1543
  }
@@ -2486,19 +1585,7 @@ async function chooseProviderToTest() {
2486
1585
  if (result.model) console.log(`Model: ${result.model}`);
2487
1586
  if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
2488
1587
  }
2489
- async function chooseProviderKeyToConfigure() {
2490
- const config = await loadConfig();
2491
- const name = await select2({
2492
- message: "Choose the provider key to configure",
2493
- default: config.routing.default_provider,
2494
- choices: Object.entries(config.providers).map(([providerName, provider2]) => ({
2495
- name: `${providerName} (${provider2.api_key_env}${process.env[provider2.api_key_env] ? ", currently set" : ", missing"})`,
2496
- value: providerName
2497
- }))
2498
- });
2499
- await configureProviderApiKey(config.providers[name]);
2500
- }
2501
- async function quickAddProvider2() {
1588
+ async function quickAddProvider() {
2502
1589
  const config = await loadConfig();
2503
1590
  const template = await select2({
2504
1591
  message: "Choose the provider/channel you want to add",
@@ -2532,7 +1619,7 @@ async function quickAddProvider2() {
2532
1619
  });
2533
1620
  const apiKeyEnv = await input2({
2534
1621
  message: "API key environment variable",
2535
- default: nextAvailableProviderApiKeyEnv(config, defaults.api_key_env, name)
1622
+ default: defaults.api_key_env
2536
1623
  });
2537
1624
  const modelsRaw = await input2({
2538
1625
  message: "Models (comma separated, press Enter for recommended defaults)",
@@ -2544,7 +1631,7 @@ async function quickAddProvider2() {
2544
1631
  channel: defaults.channel,
2545
1632
  base_url: normalizeProviderBaseUrl(baseUrl),
2546
1633
  api_key_env: apiKeyEnv,
2547
- models: parseModels3(modelsRaw),
1634
+ models: parseModels(modelsRaw),
2548
1635
  capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
2549
1636
  };
2550
1637
  addProviderToConfig(config, name, provider2);
@@ -2557,27 +1644,7 @@ async function quickAddProvider2() {
2557
1644
  }
2558
1645
  await saveConfig(config);
2559
1646
  console.log(`Added provider: ${name}`);
2560
- await configureProviderApiKey(provider2);
2561
- }
2562
- async function configureProviderApiKey(provider2) {
2563
- if (process.env[provider2.api_key_env]) {
2564
- const replace = await confirm2({
2565
- message: `${provider2.api_key_env} is already available. Replace the saved PicGen key?`,
2566
- default: false
2567
- });
2568
- if (!replace) return;
2569
- }
2570
- const value = await password2({
2571
- message: `Paste API key for ${provider2.api_key_env} (leave empty to skip)`,
2572
- mask: "*"
2573
- });
2574
- if (!value.trim()) {
2575
- console.log(`Skipped API key. You can configure it later with picgen setup.`);
2576
- return;
2577
- }
2578
- const path = await saveManagedEnvVar(provider2.api_key_env, value.trim());
2579
- console.log(`Saved ${provider2.api_key_env} to ${path}`);
2580
- console.log(`PicGen loads this file automatically. Advanced users can override it with shell env vars or a project .env.`);
1647
+ console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
2581
1648
  }
2582
1649
  function quickProviderDefaults(template) {
2583
1650
  switch (template) {
@@ -2587,7 +1654,7 @@ function quickProviderDefaults(template) {
2587
1654
  protocol: "openai-images",
2588
1655
  channel: "third_party",
2589
1656
  base_url: "https://www.pandai.vip",
2590
- api_key_env: "PICGEN_OPENAI_PROXY_KEY",
1657
+ api_key_env: "OPENAI_API_KEY",
2591
1658
  models: ["gpt-image-2"]
2592
1659
  };
2593
1660
  case "gemini_proxy":
@@ -2596,7 +1663,7 @@ function quickProviderDefaults(template) {
2596
1663
  protocol: "gemini",
2597
1664
  channel: "third_party",
2598
1665
  base_url: "https://www.pandai.vip",
2599
- api_key_env: "PICGEN_GEMINI_PROXY_KEY",
1666
+ api_key_env: "GEMINI_API_KEY",
2600
1667
  models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
2601
1668
  };
2602
1669
  case "openai_official":
@@ -2619,7 +1686,7 @@ function quickProviderDefaults(template) {
2619
1686
  };
2620
1687
  }
2621
1688
  }
2622
- function parseModels3(raw) {
1689
+ function parseModels(raw) {
2623
1690
  return raw.split(",").map((model) => model.trim()).filter(Boolean);
2624
1691
  }
2625
1692
  function providerLabel(provider2) {
@@ -2641,54 +1708,11 @@ function modeLabel(modeName) {
2641
1708
  }
2642
1709
  }
2643
1710
 
2644
- // src/commands/skill.ts
2645
- import { access, cp, mkdir as mkdir5 } from "fs/promises";
2646
- import { constants } from "fs";
2647
- import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
2648
- import { homedir as homedir4 } from "os";
2649
- import { fileURLToPath } from "url";
2650
- async function installSkill(target, options) {
2651
- if (target !== "codex") {
2652
- throw new Error(`Unsupported skill target: ${target}. Supported target: codex.`);
2653
- }
2654
- const source = await findBundledPicgenSkill();
2655
- const destination = join7(getCodexHome(), "skills", "picgen");
2656
- await mkdir5(dirname4(destination), { recursive: true });
2657
- await cp(source, destination, {
2658
- recursive: true,
2659
- force: options.force ?? false,
2660
- errorOnExist: !(options.force ?? false)
2661
- });
2662
- console.log(`Installed PicGen skill for Codex: ${destination}`);
2663
- console.log("Restart Codex or start a new Codex session if the skill is not visible yet.");
2664
- }
2665
- function getCodexHome() {
2666
- return process.env.CODEX_HOME ?? join7(homedir4(), ".codex");
2667
- }
2668
- async function findBundledPicgenSkill() {
2669
- const here = dirname4(fileURLToPath(import.meta.url));
2670
- const candidates = [
2671
- resolve4(here, "../skills/picgen"),
2672
- resolve4(here, "../../skills/picgen"),
2673
- resolve4(process.cwd(), "skills/picgen")
2674
- ];
2675
- for (const candidate of candidates) {
2676
- try {
2677
- await access(join7(candidate, "SKILL.md"), constants.R_OK);
2678
- return candidate;
2679
- } catch {
2680
- }
2681
- }
2682
- throw new Error("Bundled PicGen skill not found. Reinstall @ai-agent-tools/picgen and try again.");
2683
- }
2684
-
2685
1711
  // src/cli.ts
2686
- await loadPicgenEnv();
2687
1712
  var program = new Command();
2688
1713
  program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
2689
1714
  program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
2690
1715
  program.command("quickstart").description("Print install and first-run guidance.").action(runQuickstart);
2691
- program.command("open").description("Open the local PicGen web interface.").option("--port <port>", "Preferred local port. Defaults to 8188.").option("--no-open", "Print the URL without opening the browser.").action(runOpen);
2692
1716
  program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
2693
1717
  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(
2694
1718
  "--reference <path>",
@@ -2699,18 +1723,12 @@ program.command("create").description("Create an image generation plan or genera
2699
1723
  var provider = program.command("provider").description("Manage providers/channels.");
2700
1724
  provider.command("list").description("List providers.").action(listProviders);
2701
1725
  provider.command("add").description("Add a provider.").action(addProvider);
2702
- provider.command("quick-add").argument("<template>", "openai-proxy, gemini-proxy, openai-official, or gemini-official").description("Add a common provider/channel without interactive prompts.").option("--name <name>", "Provider name.").option("--host <url>", "Provider host URL. Do not include /v1 or /v1beta.").option("--key-env <name>", "API key environment variable.").option("--models <models>", "Comma-separated model list.").option("--prefer", "Use this provider as the default.").action(quickAddProvider);
2703
1726
  provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
2704
1727
  provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
2705
1728
  provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
2706
1729
  provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
2707
1730
  provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
2708
1731
  provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
2709
- var key = program.command("key").description("Manage PicGen API keys.");
2710
- key.command("set").argument("<env-name>", "Environment variable name, such as PICGEN_GEMINI_PROXY_KEY.").description("Save an API key to PicGen's managed env file.").option("--stdin", "Read the key value from stdin.").option("--clipboard", "Read the key value from the macOS clipboard.").option("--value <value>", "Set the key value directly. Prefer --stdin for agent workflows.").action(setApiKey);
2711
- key.command("list").description("List configured provider keys without revealing secret values.").option("--json", "Print machine-readable JSON.").action(listApiKeys);
2712
- key.command("show").argument("<env-name>", "Environment variable name.").description("Show one configured key without revealing the secret value.").option("--json", "Print machine-readable JSON.").action(showApiKey);
2713
- program.command("skill").description("Install PicGen agent skills.").command("install").argument("<target>", "Skill target. Currently supported: codex.").description("Install the bundled PicGen skill into an agent skill directory.").option("--force", "Overwrite an existing installed skill.").action(installSkill);
2714
1732
  program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
2715
1733
  program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
2716
1734
  program.command("update").description("Manage PicGen updates.").command("check").description("Check whether a newer PicGen version is available.").option("--json", "Print machine-readable JSON.").action(runUpdateCheck);