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

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/README.md CHANGED
@@ -91,6 +91,14 @@ OPENAI_API_KEY=...
91
91
  GEMINI_API_KEY=...
92
92
  ```
93
93
 
94
+ For non-technical users, `picgen setup` can save API keys for you in:
95
+
96
+ ```text
97
+ ~/.picgen/.env
98
+ ```
99
+
100
+ PicGen loads this managed env file automatically. Shell environment variables take priority, and a project `.env` can override the managed file for local testing.
101
+
94
102
  You can start from the included example:
95
103
 
96
104
  ```bash
@@ -101,6 +109,12 @@ Provider `base_url` values should be host-only. Do not include `/v1` or `/v1beta
101
109
 
102
110
  Providers may optionally set `test_model` in `~/.picgen/config.yaml` when health checks should use a lightweight model instead of the first generation model.
103
111
 
112
+ Generation requests use adaptive provider timeouts: fast draft requests allow 120s, balanced requests allow 180s, and high quality or large requests allow 300s. If a third-party channel is slower, override it with `PICGEN_PROVIDER_TIMEOUT_MS`:
113
+
114
+ ```bash
115
+ PICGEN_PROVIDER_TIMEOUT_MS=450000 picgen create --yes --preset poster "一张产品发布会主视觉"
116
+ ```
117
+
104
118
  Providers expose capabilities such as `text-to-image` and `reference-image`. Old configs that omit capabilities are upgraded in memory from the provider protocol: Gemini supports both text and reference-image generation, while OpenAI-compatible `/v1/images/generations` supports text-to-image only.
105
119
 
106
120
  Generated image data and provider-only fields such as base64 image payloads and Gemini thought signatures are redacted from metadata. PicGen keeps the generated assets as local image files and keeps stdout compact for agent workflows.
package/dist/cli.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import "dotenv/config";
5
4
  import { Command } from "commander";
6
5
 
7
6
  // src/commands/create.ts
@@ -459,6 +458,53 @@ async function ensureConfig() {
459
458
  // src/providers/gemini.ts
460
459
  import { readFile as readFile2 } from "fs/promises";
461
460
 
461
+ // src/providers/timeout.ts
462
+ var FAST_PROVIDER_TIMEOUT_MS = 12e4;
463
+ var DEFAULT_PROVIDER_TIMEOUT_MS = 18e4;
464
+ var SLOW_PROVIDER_TIMEOUT_MS = 3e5;
465
+ function resolveProviderTimeoutMs(plan) {
466
+ const override = parseTimeoutOverride(process.env.PICGEN_PROVIDER_TIMEOUT_MS);
467
+ if (override !== void 0) return override;
468
+ if (plan.presetName === "fast-draft" || plan.modeName === "fast" || plan.preset.quality === "low") {
469
+ return FAST_PROVIDER_TIMEOUT_MS;
470
+ }
471
+ if (plan.modeName === "premium" || plan.preset.size === "large" || plan.preset.quality === "high") {
472
+ return SLOW_PROVIDER_TIMEOUT_MS;
473
+ }
474
+ return DEFAULT_PROVIDER_TIMEOUT_MS;
475
+ }
476
+ async function fetchWithProviderTimeout(input3, init, timeoutMs) {
477
+ const controller = new AbortController();
478
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
479
+ try {
480
+ return await fetch(input3, {
481
+ ...init,
482
+ signal: controller.signal
483
+ });
484
+ } catch (error) {
485
+ if (isAbortError(error)) {
486
+ throw new Error(formatProviderTimeoutError(timeoutMs));
487
+ }
488
+ throw error;
489
+ } finally {
490
+ clearTimeout(timeout);
491
+ }
492
+ }
493
+ function formatProviderTimeoutError(timeoutMs) {
494
+ return `Provider request timed out after ${Math.ceil(
495
+ timeoutMs / 1e3
496
+ )}s. The provider may still be processing or temporarily unavailable. Try again, use a faster preset, or increase PICGEN_PROVIDER_TIMEOUT_MS.`;
497
+ }
498
+ function parseTimeoutOverride(value) {
499
+ if (!value) return void 0;
500
+ const timeoutMs = Number(value);
501
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) return void 0;
502
+ return timeoutMs;
503
+ }
504
+ function isAbortError(error) {
505
+ return error instanceof Error && error.name === "AbortError";
506
+ }
507
+
462
508
  // src/providers/urls.ts
463
509
  function normalizeProviderBaseUrl(baseUrl) {
464
510
  const url = new URL(baseUrl);
@@ -495,15 +541,20 @@ var GeminiAdapter = class {
495
541
  const providerImages = [];
496
542
  const requestCount = Math.max(1, plan.preset.n);
497
543
  const referenceParts = await readReferenceImageParts(plan);
544
+ const timeoutMs = resolveProviderTimeoutMs(plan);
498
545
  for (let index = 0; index < requestCount; index += 1) {
499
- const response = await fetch(buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model), {
500
- method: "POST",
501
- headers: {
502
- "x-goog-api-key": apiKey,
503
- "Content-Type": "application/json"
546
+ const response = await fetchWithProviderTimeout(
547
+ buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model),
548
+ {
549
+ method: "POST",
550
+ headers: {
551
+ "x-goog-api-key": apiKey,
552
+ "Content-Type": "application/json"
553
+ },
554
+ body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
504
555
  },
505
- body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
506
- });
556
+ timeoutMs
557
+ );
507
558
  const raw = await readJsonResponse(response);
508
559
  if (!response.ok) {
509
560
  throw new Error(formatGeminiError(response.status, response.statusText, raw));
@@ -624,14 +675,18 @@ var OpenAIImagesAdapter = class {
624
675
  if (!apiKey) {
625
676
  throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
626
677
  }
627
- const response = await fetch(buildOpenAIImagesUrl(plan.provider.base_url), {
628
- method: "POST",
629
- headers: {
630
- Authorization: `Bearer ${apiKey}`,
631
- "Content-Type": "application/json"
678
+ const response = await fetchWithProviderTimeout(
679
+ buildOpenAIImagesUrl(plan.provider.base_url),
680
+ {
681
+ method: "POST",
682
+ headers: {
683
+ Authorization: `Bearer ${apiKey}`,
684
+ "Content-Type": "application/json"
685
+ },
686
+ body: JSON.stringify(buildOpenAIImagesRequest(plan))
632
687
  },
633
- body: JSON.stringify(buildOpenAIImagesRequest(plan))
634
- });
688
+ resolveProviderTimeoutMs(plan)
689
+ );
635
690
  const raw = await readJsonResponse2(response);
636
691
  if (!response.ok) {
637
692
  throw new Error(formatOpenAIImagesError(response.status, response.statusText, raw));
@@ -999,7 +1054,7 @@ import { homedir as homedir2 } from "os";
999
1054
 
1000
1055
  // src/version.ts
1001
1056
  var PACKAGE_NAME = "@ai-agent-tools/picgen";
1002
- var VERSION = "0.1.0-alpha.2";
1057
+ var VERSION = "0.1.0-alpha.4";
1003
1058
 
1004
1059
  // src/commands/update.ts
1005
1060
  var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -1469,7 +1524,7 @@ function formatQuickstart() {
1469
1524
  " npm install -g @ai-agent-tools/picgen",
1470
1525
  "",
1471
1526
  "Configure:",
1472
- " picgen setup",
1527
+ " picgen setup # can save provider API keys for you",
1473
1528
  " picgen doctor --json",
1474
1529
  "",
1475
1530
  "Preview before spending quota:",
@@ -1482,10 +1537,11 @@ function formatQuickstart() {
1482
1537
  ' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
1483
1538
  "",
1484
1539
  "Agent prompt:",
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",
1540
+ " \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 \u5F15\u5BFC\u6211\u914D\u7F6E provider \u548C API key\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",
1486
1541
  "",
1487
1542
  "Notes:",
1488
1543
  " - Provider host URLs should not include /v1 or /v1beta.",
1544
+ " - picgen setup can store API keys in ~/.picgen/.env.",
1489
1545
  " - Agent workflows should dry-run before real generation.",
1490
1546
  " - Generated images are saved locally; do not paste base64 into chat.",
1491
1547
  " - First-user rollout checklist: docs/release-alpha.md"
@@ -1493,7 +1549,62 @@ function formatQuickstart() {
1493
1549
  }
1494
1550
 
1495
1551
  // src/commands/setup.ts
1496
- import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
1552
+ import { confirm as confirm2, input as input2, password, select as select2 } from "@inquirer/prompts";
1553
+
1554
+ // src/config/env.ts
1555
+ import { chmod, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1556
+ import { existsSync } from "fs";
1557
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
1558
+ import { homedir as homedir3 } from "os";
1559
+ import { parse } from "dotenv";
1560
+ function getManagedEnvPath() {
1561
+ return process.env.PICGEN_ENV_PATH ?? join5(homedir3(), ".picgen", ".env");
1562
+ }
1563
+ async function loadPicgenEnv() {
1564
+ const shellEnv = new Set(Object.keys(process.env));
1565
+ await loadEnvFile(getManagedEnvPath(), shellEnv, false);
1566
+ await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true);
1567
+ }
1568
+ async function saveManagedEnvVar(name, value) {
1569
+ const path = getManagedEnvPath();
1570
+ const current = await readManagedEnvFile(path);
1571
+ const next = {
1572
+ ...current,
1573
+ [name]: value
1574
+ };
1575
+ await mkdir4(dirname3(path), { recursive: true });
1576
+ await writeFile4(path, stringifyEnv(next), "utf8");
1577
+ await chmod(path, 384);
1578
+ process.env[name] = value;
1579
+ return path;
1580
+ }
1581
+ async function loadEnvFile(path, shellEnv, overrideManagedValues) {
1582
+ if (!existsSync(path)) return;
1583
+ const parsed = parse(await readFile4(path, "utf8"));
1584
+ for (const [name, value] of Object.entries(parsed)) {
1585
+ if (shellEnv.has(name)) continue;
1586
+ if (!overrideManagedValues && process.env[name] !== void 0) continue;
1587
+ process.env[name] = value;
1588
+ }
1589
+ }
1590
+ async function readManagedEnvFile(path) {
1591
+ try {
1592
+ return parse(await readFile4(path, "utf8"));
1593
+ } catch (error) {
1594
+ if (error.code === "ENOENT") return {};
1595
+ throw error;
1596
+ }
1597
+ }
1598
+ function stringifyEnv(values) {
1599
+ return `${Object.entries(values).map(([name, value]) => `${name}=${quoteEnvValue(value)}`).join("\n")}
1600
+ `;
1601
+ }
1602
+ function quoteEnvValue(value) {
1603
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
1604
+ return JSON.stringify(value);
1605
+ }
1606
+
1607
+ // src/commands/setup.ts
1497
1608
  async function runSetup() {
1498
1609
  await ensureConfig();
1499
1610
  console.log(`PicGen config: ${getConfigPath()}`);
@@ -1508,6 +1619,7 @@ async function runSetup() {
1508
1619
  { name: "Quick add a common provider/channel", value: "quick-add" },
1509
1620
  { name: "Choose default provider/channel", value: "provider" },
1510
1621
  { name: "Choose generation preference", value: "mode" },
1622
+ { name: "Configure API key", value: "key" },
1511
1623
  { name: "Test a provider", value: "test" },
1512
1624
  { name: "Advanced: add a custom provider/channel", value: "add" },
1513
1625
  { name: "Finish setup", value: "done" }
@@ -1519,6 +1631,8 @@ async function runSetup() {
1519
1631
  await chooseDefaultProvider();
1520
1632
  } else if (action === "mode") {
1521
1633
  await chooseDefaultMode();
1634
+ } else if (action === "key") {
1635
+ await chooseProviderKeyToConfigure();
1522
1636
  } else if (action === "test") {
1523
1637
  await chooseProviderToTest();
1524
1638
  } else if (action === "add") {
@@ -1537,7 +1651,7 @@ async function printSetupSummary() {
1537
1651
  for (const [providerName, provider2] of Object.entries(config.providers)) {
1538
1652
  const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
1539
1653
  console.log(
1540
- `- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
1654
+ `- ${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(",")}`
1541
1655
  );
1542
1656
  }
1543
1657
  }
@@ -1585,6 +1699,18 @@ async function chooseProviderToTest() {
1585
1699
  if (result.model) console.log(`Model: ${result.model}`);
1586
1700
  if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
1587
1701
  }
1702
+ async function chooseProviderKeyToConfigure() {
1703
+ const config = await loadConfig();
1704
+ const name = await select2({
1705
+ message: "Choose the provider key to configure",
1706
+ default: config.routing.default_provider,
1707
+ choices: Object.entries(config.providers).map(([providerName, provider2]) => ({
1708
+ name: `${providerName} (${provider2.api_key_env}${process.env[provider2.api_key_env] ? ", currently set" : ", missing"})`,
1709
+ value: providerName
1710
+ }))
1711
+ });
1712
+ await configureProviderApiKey(config.providers[name]);
1713
+ }
1588
1714
  async function quickAddProvider() {
1589
1715
  const config = await loadConfig();
1590
1716
  const template = await select2({
@@ -1644,7 +1770,27 @@ async function quickAddProvider() {
1644
1770
  }
1645
1771
  await saveConfig(config);
1646
1772
  console.log(`Added provider: ${name}`);
1647
- console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
1773
+ await configureProviderApiKey(provider2);
1774
+ }
1775
+ async function configureProviderApiKey(provider2) {
1776
+ if (process.env[provider2.api_key_env]) {
1777
+ const replace = await confirm2({
1778
+ message: `${provider2.api_key_env} is already available. Replace the saved PicGen key?`,
1779
+ default: false
1780
+ });
1781
+ if (!replace) return;
1782
+ }
1783
+ const value = await password({
1784
+ message: `Paste API key for ${provider2.api_key_env} (leave empty to skip)`,
1785
+ mask: "*"
1786
+ });
1787
+ if (!value.trim()) {
1788
+ console.log(`Skipped API key. You can configure it later with picgen setup.`);
1789
+ return;
1790
+ }
1791
+ const path = await saveManagedEnvVar(provider2.api_key_env, value.trim());
1792
+ console.log(`Saved ${provider2.api_key_env} to ${path}`);
1793
+ console.log(`PicGen loads this file automatically. Advanced users can override it with shell env vars or a project .env.`);
1648
1794
  }
1649
1795
  function quickProviderDefaults(template) {
1650
1796
  switch (template) {
@@ -1709,6 +1855,7 @@ function modeLabel(modeName) {
1709
1855
  }
1710
1856
 
1711
1857
  // src/cli.ts
1858
+ await loadPicgenEnv();
1712
1859
  var program = new Command();
1713
1860
  program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
1714
1861
  program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
@@ -40,7 +40,17 @@ https://generativelanguage.googleapis.com
40
40
 
41
41
  Do not include `/v1` or `/v1beta`.
42
42
 
43
- 4. Set API keys in the shell or a local `.env` file:
43
+ 4. Configure API keys:
44
+
45
+ For non-technical users, prefer `picgen setup`. It can save provider API keys in PicGen's managed env file:
46
+
47
+ ```text
48
+ ~/.picgen/.env
49
+ ```
50
+
51
+ PicGen loads this file automatically.
52
+
53
+ Advanced users can still use shell environment variables or a local project `.env`:
44
54
 
45
55
  ```bash
46
56
  cp .env.example .env
@@ -143,6 +153,14 @@ picgen provider test <provider-name> --json
143
153
 
144
154
  Check `base_url`, API key, model name, and provider availability.
145
155
 
156
+ `Provider request timed out`
157
+
158
+ High quality, large, or slow third-party image channels can take longer. Try again, use a faster preset, or raise the request timeout:
159
+
160
+ ```bash
161
+ PICGEN_PROVIDER_TIMEOUT_MS=450000 picgen create --yes --preset poster "<prompt>"
162
+ ```
163
+
146
164
  `No enabled provider can satisfy...`
147
165
 
148
166
  Run `picgen provider list`, enable a provider, add a fallback provider, or adjust the selected mode/model.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-tools/picgen",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "A lightweight image generation connector for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@ Never silently spend user quota. Do not send full conversation context to provid
20
20
  ## Workflow
21
21
 
22
22
  1. Run `picgen doctor --json` to check configuration.
23
- 2. If no usable provider is configured, guide the user to run `picgen setup`.
23
+ 2. If no usable provider is configured, guide the user to run `picgen setup`. The setup wizard can configure providers and save API keys for non-technical users.
24
24
  3. Choose a preset from the user's intent, such as `poster`, `product-shot`, or `social-cover`.
25
25
  4. Run `picgen create --dry-run --preset <preset> "<prompt>"`.
26
26
  5. Present the dry-run as a user-facing generation preview. Do not expose `dry-run` as a technical term unless useful.
@@ -75,7 +75,7 @@ PicGen redacts generated image payloads and Gemini thought signatures from metad
75
75
 
76
76
  If `doctor` reports no usable provider, ask the user to run `picgen setup`.
77
77
 
78
- If an API key is missing, name the required environment variable.
78
+ If an API key is missing, guide the user to run `picgen setup` and choose `Configure API key`. Name the required environment variable only when useful for debugging.
79
79
 
80
80
  If a provider is disabled, suggest enabling it or using a one-off provider override.
81
81