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

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,7 +457,7 @@ async function ensureConfig() {
622
457
  }
623
458
 
624
459
  // src/providers/gemini.ts
625
- import { readFile as readFile3 } from "fs/promises";
460
+ import { readFile as readFile2 } from "fs/promises";
626
461
 
627
462
  // src/providers/timeout.ts
628
463
  var FAST_PROVIDER_TIMEOUT_MS = 12e4;
@@ -762,7 +597,7 @@ async function readReferenceImageParts(plan) {
762
597
  plan.referenceImages.map(async (image) => ({
763
598
  inlineData: {
764
599
  mimeType: image.mime_type,
765
- data: (await readFile3(image.path)).toString("base64")
600
+ data: (await readFile2(image.path)).toString("base64")
766
601
  }
767
602
  }))
768
603
  );
@@ -979,7 +814,7 @@ function getAdapter(protocol) {
979
814
  }
980
815
 
981
816
  // src/routing/resolve.ts
982
- import { join as join4 } from "path";
817
+ import { join as join3 } from "path";
983
818
  import { cwd } from "process";
984
819
  function resolveGenerationPlan(config, options) {
985
820
  const presetName = options.presetName ?? config.default_preset;
@@ -1013,7 +848,7 @@ function resolveGenerationPlan(config, options) {
1013
848
  presetName,
1014
849
  preset,
1015
850
  modeName,
1016
- outputDirectory: options.outputDirectory ?? join4(cwd(), "outputs", "picgen"),
851
+ outputDirectory: options.outputDirectory ?? join3(cwd(), "outputs", "picgen"),
1017
852
  referenceImages: options.referenceImages ?? []
1018
853
  };
1019
854
  }
@@ -1214,13 +1049,13 @@ function inspectProviders(config) {
1214
1049
  }
1215
1050
 
1216
1051
  // 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";
1052
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1053
+ import { dirname as dirname2, join as join4 } from "path";
1054
+ import { homedir as homedir2 } from "os";
1220
1055
 
1221
1056
  // src/version.ts
1222
1057
  var PACKAGE_NAME = "@ai-agent-tools/picgen";
1223
- var VERSION = "0.1.0-alpha.10";
1058
+ var VERSION = "0.1.0-alpha.3";
1224
1059
 
1225
1060
  // src/commands/update.ts
1226
1061
  var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -1315,7 +1150,7 @@ async function fetchLatestVersion() {
1315
1150
  }
1316
1151
  async function readFreshCache() {
1317
1152
  try {
1318
- const cache = JSON.parse(await readFile4(getUpdateCachePath(), "utf8"));
1153
+ const cache = JSON.parse(await readFile3(getUpdateCachePath(), "utf8"));
1319
1154
  const checkedAt = Date.parse(cache.checked_at);
1320
1155
  if (!Number.isFinite(checkedAt)) return void 0;
1321
1156
  if (Date.now() - checkedAt > UPDATE_CACHE_TTL_MS) return void 0;
@@ -1327,12 +1162,12 @@ async function readFreshCache() {
1327
1162
  }
1328
1163
  async function writeCache(cache) {
1329
1164
  const path = getUpdateCachePath();
1330
- await mkdir4(dirname3(path), { recursive: true });
1331
- await writeFile4(path, JSON.stringify(cache, null, 2), "utf8");
1165
+ await mkdir3(dirname2(path), { recursive: true });
1166
+ await writeFile3(path, JSON.stringify(cache, null, 2), "utf8");
1332
1167
  }
1333
1168
  function getUpdateCachePath() {
1334
1169
  if (process.env.PICGEN_UPDATE_CACHE_PATH) return process.env.PICGEN_UPDATE_CACHE_PATH;
1335
- return join5(homedir3(), ".picgen", "update-check.json");
1170
+ return join4(homedir2(), ".picgen", "update-check.json");
1336
1171
  }
1337
1172
  function parseVersion(version) {
1338
1173
  const [core, prerelease] = version.split("-", 2);
@@ -1391,84 +1226,6 @@ async function runDoctor(options) {
1391
1226
  await maybePrintUpdateHint();
1392
1227
  }
1393
1228
 
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
1229
  // src/commands/provider.ts
1473
1230
  import { input, select } from "@inquirer/prompts";
1474
1231
 
@@ -1614,32 +1371,6 @@ async function addProvider() {
1614
1371
  await saveConfig(config);
1615
1372
  console.log(`Added provider: ${provider2.name}`);
1616
1373
  }
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
1374
  function addProviderToConfig(config, name, provider2) {
1644
1375
  config.providers[name] = provider2;
1645
1376
  const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
@@ -1736,11 +1467,7 @@ async function promptProvider(config, existingName, existing) {
1736
1467
  });
1737
1468
  const apiKeyEnv = await input({
1738
1469
  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
- )
1470
+ 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
1471
  });
1745
1472
  const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
1746
1473
  const modelsRaw = await input({
@@ -1770,562 +1497,6 @@ function nextAvailableProviderName(config, baseName, existingName) {
1770
1497
  function defaultCapabilitiesForProtocol2(protocol) {
1771
1498
  return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
1772
1499
  }
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
1500
 
2330
1501
  // src/commands/preferences.ts
2331
1502
  async function preferMode(name) {
@@ -2351,39 +1522,26 @@ function formatQuickstart() {
2351
1522
  "PicGen quickstart",
2352
1523
  "",
2353
1524
  "Install:",
2354
- " node -v",
2355
- " npm -v",
2356
1525
  " 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
1526
  "",
2363
1527
  "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",
1528
+ " picgen setup",
2369
1529
  " picgen doctor --json",
2370
1530
  "",
2371
1531
  "Preview before spending quota:",
2372
- ' picgen create --dry-run --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1532
+ ' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
2373
1533
  "",
2374
1534
  "Generate after confirmation:",
2375
- ' picgen create --yes --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1535
+ ' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
2376
1536
  "",
2377
1537
  "Use a reference image:",
2378
1538
  ' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
2379
1539
  "",
2380
1540
  "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",
1541
+ " \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
1542
  "",
2383
1543
  "Notes:",
2384
1544
  " - 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
1545
  " - Agent workflows should dry-run before real generation.",
2388
1546
  " - Generated images are saved locally; do not paste base64 into chat.",
2389
1547
  " - First-user rollout checklist: docs/release-alpha.md"
@@ -2391,7 +1549,7 @@ function formatQuickstart() {
2391
1549
  }
2392
1550
 
2393
1551
  // src/commands/setup.ts
2394
- import { confirm as confirm2, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
1552
+ import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
2395
1553
  async function runSetup() {
2396
1554
  await ensureConfig();
2397
1555
  console.log(`PicGen config: ${getConfigPath()}`);
@@ -2406,20 +1564,17 @@ async function runSetup() {
2406
1564
  { name: "Quick add a common provider/channel", value: "quick-add" },
2407
1565
  { name: "Choose default provider/channel", value: "provider" },
2408
1566
  { name: "Choose generation preference", value: "mode" },
2409
- { name: "Configure API key", value: "key" },
2410
1567
  { name: "Test a provider", value: "test" },
2411
1568
  { name: "Advanced: add a custom provider/channel", value: "add" },
2412
1569
  { name: "Finish setup", value: "done" }
2413
1570
  ]
2414
1571
  });
2415
1572
  if (action === "quick-add") {
2416
- await quickAddProvider2();
1573
+ await quickAddProvider();
2417
1574
  } else if (action === "provider") {
2418
1575
  await chooseDefaultProvider();
2419
1576
  } else if (action === "mode") {
2420
1577
  await chooseDefaultMode();
2421
- } else if (action === "key") {
2422
- await chooseProviderKeyToConfigure();
2423
1578
  } else if (action === "test") {
2424
1579
  await chooseProviderToTest();
2425
1580
  } else if (action === "add") {
@@ -2438,7 +1593,7 @@ async function printSetupSummary() {
2438
1593
  for (const [providerName, provider2] of Object.entries(config.providers)) {
2439
1594
  const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
2440
1595
  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(",")}`
1596
+ `- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
2442
1597
  );
2443
1598
  }
2444
1599
  }
@@ -2486,19 +1641,7 @@ async function chooseProviderToTest() {
2486
1641
  if (result.model) console.log(`Model: ${result.model}`);
2487
1642
  if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
2488
1643
  }
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() {
1644
+ async function quickAddProvider() {
2502
1645
  const config = await loadConfig();
2503
1646
  const template = await select2({
2504
1647
  message: "Choose the provider/channel you want to add",
@@ -2532,7 +1675,7 @@ async function quickAddProvider2() {
2532
1675
  });
2533
1676
  const apiKeyEnv = await input2({
2534
1677
  message: "API key environment variable",
2535
- default: nextAvailableProviderApiKeyEnv(config, defaults.api_key_env, name)
1678
+ default: defaults.api_key_env
2536
1679
  });
2537
1680
  const modelsRaw = await input2({
2538
1681
  message: "Models (comma separated, press Enter for recommended defaults)",
@@ -2544,7 +1687,7 @@ async function quickAddProvider2() {
2544
1687
  channel: defaults.channel,
2545
1688
  base_url: normalizeProviderBaseUrl(baseUrl),
2546
1689
  api_key_env: apiKeyEnv,
2547
- models: parseModels3(modelsRaw),
1690
+ models: parseModels(modelsRaw),
2548
1691
  capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
2549
1692
  };
2550
1693
  addProviderToConfig(config, name, provider2);
@@ -2557,27 +1700,7 @@ async function quickAddProvider2() {
2557
1700
  }
2558
1701
  await saveConfig(config);
2559
1702
  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.`);
1703
+ console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
2581
1704
  }
2582
1705
  function quickProviderDefaults(template) {
2583
1706
  switch (template) {
@@ -2587,7 +1710,7 @@ function quickProviderDefaults(template) {
2587
1710
  protocol: "openai-images",
2588
1711
  channel: "third_party",
2589
1712
  base_url: "https://www.pandai.vip",
2590
- api_key_env: "PICGEN_OPENAI_PROXY_KEY",
1713
+ api_key_env: "OPENAI_API_KEY",
2591
1714
  models: ["gpt-image-2"]
2592
1715
  };
2593
1716
  case "gemini_proxy":
@@ -2596,7 +1719,7 @@ function quickProviderDefaults(template) {
2596
1719
  protocol: "gemini",
2597
1720
  channel: "third_party",
2598
1721
  base_url: "https://www.pandai.vip",
2599
- api_key_env: "PICGEN_GEMINI_PROXY_KEY",
1722
+ api_key_env: "GEMINI_API_KEY",
2600
1723
  models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
2601
1724
  };
2602
1725
  case "openai_official":
@@ -2619,7 +1742,7 @@ function quickProviderDefaults(template) {
2619
1742
  };
2620
1743
  }
2621
1744
  }
2622
- function parseModels3(raw) {
1745
+ function parseModels(raw) {
2623
1746
  return raw.split(",").map((model) => model.trim()).filter(Boolean);
2624
1747
  }
2625
1748
  function providerLabel(provider2) {
@@ -2641,54 +1764,11 @@ function modeLabel(modeName) {
2641
1764
  }
2642
1765
  }
2643
1766
 
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
1767
  // src/cli.ts
2686
- await loadPicgenEnv();
2687
1768
  var program = new Command();
2688
1769
  program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
2689
1770
  program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
2690
1771
  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
1772
  program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
2693
1773
  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
1774
  "--reference <path>",
@@ -2699,18 +1779,12 @@ program.command("create").description("Create an image generation plan or genera
2699
1779
  var provider = program.command("provider").description("Manage providers/channels.");
2700
1780
  provider.command("list").description("List providers.").action(listProviders);
2701
1781
  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
1782
  provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
2704
1783
  provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
2705
1784
  provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
2706
1785
  provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
2707
1786
  provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
2708
1787
  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
1788
  program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
2715
1789
  program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
2716
1790
  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);