@ai-agent-tools/picgen 0.1.0-alpha.7 → 0.1.0-alpha.9

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
@@ -54,14 +54,16 @@ npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy
54
54
  picgen skill install codex
55
55
  picgen update check
56
56
  picgen doctor --json
57
- picgen create --dry-run "一张产品发布会主视觉"
58
- picgen create --yes "一张产品发布会主视觉"
57
+ picgen create --dry-run --preset fast-draft "一张简洁的 PicGen 测试图"
58
+ picgen create --yes --preset fast-draft "一张简洁的 PicGen 测试图"
59
59
  picgen create --dry-run --provider gemini_official --reference ./reference.png "基于参考图生成一张品牌海报"
60
60
  picgen create --yes --provider gemini_official --reference ./reference.png "基于参考图生成一张品牌海报"
61
61
  picgen provider list
62
62
  picgen provider add
63
63
  picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer
64
+ picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard
64
65
  picgen key set PICGEN_GEMINI_PROXY_KEY --stdin
66
+ picgen key list --json
65
67
  picgen provider test openai_official --json
66
68
  picgen provider prefer gemini_official
67
69
  picgen provider disable gemini_proxy
@@ -109,6 +111,8 @@ For non-technical users, `picgen setup` can save API keys for you in:
109
111
 
110
112
  PicGen loads this managed env file automatically. Shell environment variables take priority, and a project `.env` can override the managed file for local testing.
111
113
 
114
+ When agents inspect key configuration, they should use `picgen key list/show` so chat output only contains masked key status. To inspect or edit the full saved key directly, open `~/.picgen/.env`; a project `.env` in the current directory may override it, and shell environment variables take highest priority.
115
+
112
116
  You can start from the included example:
113
117
 
114
118
  ```bash
package/dist/cli.js CHANGED
@@ -217,12 +217,12 @@ function padMilliseconds(value) {
217
217
  function slug(value) {
218
218
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
219
219
  }
220
- function redactProviderImageData(value, key) {
220
+ function redactProviderImageData(value, key2) {
221
221
  if (Array.isArray(value)) {
222
222
  return value.map((item) => redactProviderImageData(item));
223
223
  }
224
224
  if (!value || typeof value !== "object") {
225
- return shouldRedactImageDataKey(key) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
225
+ return shouldRedactImageDataKey(key2) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
226
226
  }
227
227
  return Object.fromEntries(
228
228
  Object.entries(value).map(([entryKey, entryValue]) => [
@@ -231,8 +231,8 @@ function redactProviderImageData(value, key) {
231
231
  ])
232
232
  );
233
233
  }
234
- function shouldRedactImageDataKey(key) {
235
- return key === "b64_json" || key === "data" || key === "thoughtSignature" || key === "thought_signature";
234
+ function shouldRedactImageDataKey(key2) {
235
+ return key2 === "b64_json" || key2 === "data" || key2 === "thoughtSignature" || key2 === "thought_signature";
236
236
  }
237
237
  function redactedProviderDataPlaceholder(value) {
238
238
  return `[redacted provider data: ${value.length} chars]`;
@@ -1054,7 +1054,7 @@ import { homedir as homedir2 } from "os";
1054
1054
 
1055
1055
  // src/version.ts
1056
1056
  var PACKAGE_NAME = "@ai-agent-tools/picgen";
1057
- var VERSION = "0.1.0-alpha.7";
1057
+ var VERSION = "0.1.0-alpha.9";
1058
1058
 
1059
1059
  // src/commands/update.ts
1060
1060
  var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -1226,21 +1226,25 @@ async function runDoctor(options) {
1226
1226
  }
1227
1227
 
1228
1228
  // src/commands/key.ts
1229
+ import { execFile } from "child_process";
1230
+ import { promisify } from "util";
1229
1231
  import { password } from "@inquirer/prompts";
1230
1232
 
1231
1233
  // src/config/env.ts
1232
1234
  import { chmod, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1233
1235
  import { existsSync } from "fs";
1236
+ import { createHash } from "crypto";
1234
1237
  import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
1235
1238
  import { homedir as homedir3 } from "os";
1236
1239
  import { parse } from "dotenv";
1240
+ var loadedEnvSources = /* @__PURE__ */ new Map();
1237
1241
  function getManagedEnvPath() {
1238
1242
  return process.env.PICGEN_ENV_PATH ?? join5(homedir3(), ".picgen", ".env");
1239
1243
  }
1240
1244
  async function loadPicgenEnv() {
1241
1245
  const shellEnv = new Set(Object.keys(process.env));
1242
- await loadEnvFile(getManagedEnvPath(), shellEnv, false);
1243
- await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true);
1246
+ await loadEnvFile(getManagedEnvPath(), shellEnv, false, "managed");
1247
+ await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true, "project");
1244
1248
  }
1245
1249
  async function saveManagedEnvVar(name, value) {
1246
1250
  const path = getManagedEnvPath();
@@ -1253,18 +1257,53 @@ async function saveManagedEnvVar(name, value) {
1253
1257
  await writeFile4(path, stringifyEnv(next), "utf8");
1254
1258
  await chmod(path, 384);
1255
1259
  process.env[name] = value;
1260
+ loadedEnvSources.set(name, { source: "managed", path });
1256
1261
  return path;
1257
1262
  }
1258
- async function loadEnvFile(path, shellEnv, overrideManagedValues) {
1263
+ async function inspectEnvVar(name) {
1264
+ const shellValue = process.env[name];
1265
+ if (shellValue !== void 0) {
1266
+ const loadedSource = loadedEnvSources.get(name);
1267
+ return describeEnvValue(
1268
+ name,
1269
+ shellValue,
1270
+ loadedSource?.source ?? "shell",
1271
+ loadedSource?.path
1272
+ );
1273
+ }
1274
+ const projectPath = resolve2(process.cwd(), ".env");
1275
+ const project = await readEnvFile(projectPath);
1276
+ if (project[name] !== void 0) {
1277
+ return describeEnvValue(name, project[name], "project", projectPath);
1278
+ }
1279
+ const managedPath = getManagedEnvPath();
1280
+ const managed = await readEnvFile(managedPath);
1281
+ if (managed[name] !== void 0) {
1282
+ return describeEnvValue(name, managed[name], "managed", managedPath);
1283
+ }
1284
+ return {
1285
+ name,
1286
+ set: false
1287
+ };
1288
+ }
1289
+ async function inspectEnvVars(names) {
1290
+ const uniqueNames = [...new Set(names)];
1291
+ return Promise.all(uniqueNames.map((name) => inspectEnvVar(name)));
1292
+ }
1293
+ async function loadEnvFile(path, shellEnv, overrideManagedValues, source) {
1259
1294
  if (!existsSync(path)) return;
1260
1295
  const parsed = parse(await readFile4(path, "utf8"));
1261
1296
  for (const [name, value] of Object.entries(parsed)) {
1262
1297
  if (shellEnv.has(name)) continue;
1263
1298
  if (!overrideManagedValues && process.env[name] !== void 0) continue;
1264
1299
  process.env[name] = value;
1300
+ loadedEnvSources.set(name, { source, path });
1265
1301
  }
1266
1302
  }
1267
1303
  async function readManagedEnvFile(path) {
1304
+ return readEnvFile(path);
1305
+ }
1306
+ async function readEnvFile(path) {
1268
1307
  try {
1269
1308
  return parse(await readFile4(path, "utf8"));
1270
1309
  } catch (error) {
@@ -1280,11 +1319,27 @@ function quoteEnvValue(value) {
1280
1319
  if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
1281
1320
  return JSON.stringify(value);
1282
1321
  }
1322
+ function describeEnvValue(name, value, source, path) {
1323
+ return {
1324
+ name,
1325
+ set: true,
1326
+ source,
1327
+ path,
1328
+ length: value.length,
1329
+ preview: maskSecret(value),
1330
+ fingerprint: createHash("sha256").update(value).digest("hex").slice(0, 12)
1331
+ };
1332
+ }
1333
+ function maskSecret(value) {
1334
+ if (value.length <= 11) return "*".repeat(value.length);
1335
+ return `${value.slice(0, 7)}...${value.slice(-4)}`;
1336
+ }
1283
1337
 
1284
1338
  // src/commands/key.ts
1339
+ var execFileAsync = promisify(execFile);
1285
1340
  async function setApiKey(name, options) {
1286
1341
  validateEnvName(name);
1287
- const value = options.stdin ? await readStdin() : options.value ? options.value : await password({
1342
+ const value = options.clipboard ? await readClipboard() : options.stdin ? await readStdin() : options.value ? options.value : await password({
1288
1343
  message: `Paste API key for ${name}`,
1289
1344
  mask: "*"
1290
1345
  });
@@ -1294,6 +1349,27 @@ async function setApiKey(name, options) {
1294
1349
  const path = await saveManagedEnvVar(name, value.trim());
1295
1350
  console.log(`Saved ${name} to ${path}`);
1296
1351
  }
1352
+ async function listApiKeys(options) {
1353
+ const config = await loadConfig();
1354
+ const names = Object.values(config.providers).map((provider2) => provider2.api_key_env);
1355
+ const inspections = await inspectEnvVars(names);
1356
+ if (options.json) {
1357
+ console.log(JSON.stringify(inspections, null, 2));
1358
+ return;
1359
+ }
1360
+ for (const inspection of inspections) {
1361
+ printInspection(inspection);
1362
+ }
1363
+ }
1364
+ async function showApiKey(name, options) {
1365
+ validateEnvName(name);
1366
+ const inspection = await inspectEnvVar(name);
1367
+ if (options.json) {
1368
+ console.log(JSON.stringify(inspection, null, 2));
1369
+ return;
1370
+ }
1371
+ printInspection(inspection);
1372
+ }
1297
1373
  function validateEnvName(name) {
1298
1374
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1299
1375
  throw new Error(`Invalid environment variable name: ${name}`);
@@ -1306,6 +1382,24 @@ async function readStdin() {
1306
1382
  }
1307
1383
  return Buffer.concat(chunks).toString("utf8");
1308
1384
  }
1385
+ async function readClipboard() {
1386
+ try {
1387
+ const { stdout } = await execFileAsync("pbpaste");
1388
+ return stdout;
1389
+ } catch {
1390
+ throw new Error("Could not read clipboard. Use --stdin or run picgen key set without flags.");
1391
+ }
1392
+ }
1393
+ function printInspection(inspection) {
1394
+ if (!inspection.set) {
1395
+ console.log(`${inspection.name}: missing`);
1396
+ return;
1397
+ }
1398
+ const location = inspection.path ? ` ${inspection.path}` : "";
1399
+ console.log(
1400
+ `${inspection.name}: set source=${inspection.source}${location} length=${inspection.length} preview=${inspection.preview} fingerprint=${inspection.fingerprint}`
1401
+ );
1402
+ }
1309
1403
 
1310
1404
  // src/commands/provider.ts
1311
1405
  import { input, select } from "@inquirer/prompts";
@@ -1684,14 +1778,16 @@ function formatQuickstart() {
1684
1778
  "Configure:",
1685
1779
  " picgen setup # can save provider API keys for you",
1686
1780
  " picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer",
1781
+ " picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard",
1687
1782
  " picgen key set PICGEN_GEMINI_PROXY_KEY --stdin",
1783
+ " picgen key list --json",
1688
1784
  " picgen doctor --json",
1689
1785
  "",
1690
1786
  "Preview before spending quota:",
1691
- ' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
1787
+ ' picgen create --dry-run --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1692
1788
  "",
1693
1789
  "Generate after confirmation:",
1694
- ' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
1790
+ ' picgen create --yes --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
1695
1791
  "",
1696
1792
  "Use a reference image:",
1697
1793
  ' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
@@ -2023,7 +2119,10 @@ provider.command("prefer").argument("<name>").description("Set the default provi
2023
2119
  provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
2024
2120
  provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
2025
2121
  provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
2026
- program.command("key").description("Manage PicGen API keys.").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("--value <value>", "Set the key value directly. Prefer --stdin for agent workflows.").action(setApiKey);
2122
+ var key = program.command("key").description("Manage PicGen API keys.");
2123
+ 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);
2124
+ key.command("list").description("List configured provider keys without revealing secret values.").option("--json", "Print machine-readable JSON.").action(listApiKeys);
2125
+ 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);
2027
2126
  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);
2028
2127
  program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
2029
2128
  program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
@@ -56,6 +56,8 @@ For non-technical users, prefer `picgen setup`. It can save provider API keys in
56
56
 
57
57
  PicGen loads this file automatically.
58
58
 
59
+ Agents should inspect keys with `picgen key list/show`, which only prints masked status. If a technical user needs the complete saved value, point them to `~/.picgen/.env`; a project `.env` may override it, and shell environment variables have highest priority.
60
+
59
61
  In agent environments where interactive terminal prompts are not visible, ask the user for provider type, host, and API key in chat, then use non-interactive commands. Example for a Gemini-compatible third-party channel:
60
62
 
61
63
  ```bash
@@ -92,7 +94,7 @@ picgen update check
92
94
  Always start with dry-run:
93
95
 
94
96
  ```bash
95
- picgen create --dry-run "一张极简科技感产品海报"
97
+ picgen create --dry-run --preset fast-draft "一张简洁的 PicGen 测试图"
96
98
  ```
97
99
 
98
100
  Dry-run does not call providers and does not spend quota.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-tools/picgen",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.9",
4
4
  "description": "A lightweight image generation connector for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@ When running inside an agent environment where interactive terminal prompts are
59
59
 
60
60
  - Provider type: Gemini or OpenAI-compatible.
61
61
  - Provider host: host only, such as `https://www.pandai.vip`; do not include `/v1` or `/v1beta`.
62
- - API key.
62
+ - API key. Prefer asking the user to copy the key to their clipboard and reply "copied"; avoid asking them to paste secrets into chat.
63
63
 
64
64
  Then use non-interactive commands.
65
65
 
@@ -67,7 +67,7 @@ Gemini-compatible third-party channel:
67
67
 
68
68
  ```bash
69
69
  picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer
70
- picgen key set PICGEN_GEMINI_PROXY_KEY --stdin
70
+ picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard
71
71
  picgen provider test gemini_proxy --json
72
72
  ```
73
73
 
@@ -75,11 +75,22 @@ OpenAI-compatible third-party channel:
75
75
 
76
76
  ```bash
77
77
  picgen provider quick-add openai-proxy --host https://www.pandai.vip --prefer
78
- picgen key set PICGEN_OPENAI_PROXY_KEY --stdin
78
+ picgen key set PICGEN_OPENAI_PROXY_KEY --clipboard
79
79
  picgen provider test openai_proxy --json
80
80
  ```
81
81
 
82
- Pass the API key through stdin for `picgen key set`; do not put secrets directly in shell history unless the user explicitly accepts that tradeoff. If the agent runtime cannot pass stdin safely, ask the user to run `picgen key set <ENV_NAME>` in their terminal and paste the key into the hidden prompt.
82
+ If clipboard access is unavailable, pass the API key through stdin for `picgen key set`; do not put secrets directly in shell history unless the user explicitly accepts that tradeoff. If the agent runtime cannot pass stdin safely, ask the user to run `picgen key set <ENV_NAME>` in their terminal and paste the key into the hidden prompt.
83
+
84
+ To inspect configured keys without revealing secret values:
85
+
86
+ ```bash
87
+ picgen key list --json
88
+ picgen key show PICGEN_GEMINI_PROXY_KEY --json
89
+ ```
90
+
91
+ These commands show source, length, masked preview, and fingerprint only. Never ask the user to paste a key into chat just to verify it.
92
+
93
+ When explaining key inspection to users, say: "In this conversation I only read masked key status, not the full secret. If you need to inspect or edit the complete saved key yourself, PicGen's managed key file is `~/.picgen/.env`; a project-level `.env` in the current directory may override it; shell environment variables take highest priority."
83
94
 
84
95
  For reference-image generation, pass local images with repeated `--reference <path>` flags:
85
96
 
@@ -92,6 +103,36 @@ Use Gemini providers for reference-image generation in Alpha. The OpenAI-compati
92
103
 
93
104
  PicGen routes by provider capabilities. When reference images are provided, agents may omit `--provider` and let PicGen select a provider that supports `reference-image`, unless the user explicitly requested a provider.
94
105
 
106
+ ## First Smoke Test
107
+
108
+ After configuring a provider, run the first test generation with a low-cost, fast, one-image plan. Do not use `poster`, `product-shot`, `social-cover`, premium modes, large sizes, or multi-image presets for initial verification.
109
+
110
+ For Gemini providers, prefer the flash image model for the first test:
111
+
112
+ ```bash
113
+ picgen create --dry-run --provider gemini_proxy --preset fast-draft --model gemini-3.1-flash-image-preview "一张简洁的 PicGen 测试图,白色背景,少量蓝绿色科技感点缀"
114
+ picgen create --yes --provider gemini_proxy --preset fast-draft --model gemini-3.1-flash-image-preview "一张简洁的 PicGen 测试图,白色背景,少量蓝绿色科技感点缀"
115
+ ```
116
+
117
+ For OpenAI-compatible providers:
118
+
119
+ ```bash
120
+ picgen create --dry-run --provider openai_proxy --preset fast-draft "一张简洁的 PicGen 测试图,白色背景,少量蓝绿色科技感点缀"
121
+ picgen create --yes --provider openai_proxy --preset fast-draft "一张简洁的 PicGen 测试图,白色背景,少量蓝绿色科技感点缀"
122
+ ```
123
+
124
+ Present the dry-run preview and ask for confirmation before the real generation unless the user explicitly asked to generate immediately. The first smoke test should generate one image.
125
+
126
+ ## After Provider Success
127
+
128
+ After one provider is configured and the first smoke test succeeds, tell the user the provider is ready and ask whether they want to add another channel as a fallback. Example:
129
+
130
+ ```text
131
+ Gemini 渠道已经配置并测试成功。你还可以继续添加另一个渠道作为备用,例如 OpenAI-compatible。要继续添加吗?
132
+ ```
133
+
134
+ If the user says yes, repeat provider setup and smoke testing for the next channel. If the user says no, stop setup and tell them PicGen is ready to use.
135
+
95
136
  ## Preferences and Overrides
96
137
 
97
138
  Treat `picgen create` flags as one-off overrides. They must not change user preferences:
@@ -127,7 +168,7 @@ PicGen redacts generated image payloads and Gemini thought signatures from metad
127
168
 
128
169
  If `doctor` reports no usable provider, configure a provider. Prefer non-interactive setup in agent environments.
129
170
 
130
- If an API key is missing, save it with `picgen key set <ENV_NAME> --stdin` or guide the user to run `picgen setup` when interactive prompts are visible. Name the required environment variable only when useful for debugging.
171
+ If an API key is missing, save it with `picgen key set <ENV_NAME> --clipboard`, `--stdin`, or guide the user to run `picgen setup` when interactive prompts are visible. Name the required environment variable only when useful for debugging.
131
172
 
132
173
  If a provider is disabled, suggest enabling it or using a one-off provider override.
133
174
 
@@ -146,14 +187,14 @@ Explicit:
146
187
  Confirmation:
147
188
 
148
189
  ```text
149
- 我可以用 PicGen 基于当前方案生成一版主视觉。要我现在生成吗?默认用 poster 预设,出 2 张。
190
+ 我可以先用 PicGen 做一次轻量测试生成,默认只出 1 张,确认工具和渠道都可用。要我现在开始吗?
150
191
  ```
151
192
 
152
193
  Generation preview:
153
194
 
154
195
  ```text
155
196
  生成预览:
156
- 我将使用 OpenAI 官方渠道生成 2 张发布会海报,比例 3:4,保存到本地。
197
+ 我将使用当前渠道生成 1 张轻量测试图,保存到本地。
157
198
 
158
199
  确认后开始生成。
159
200
  ```