@ai-agent-tools/picgen 0.1.0-alpha.4 → 0.1.0-alpha.8
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 +15 -1
- package/dist/cli.js +315 -67
- package/docs/release-alpha.md +17 -1
- package/package.json +1 -1
- package/skills/picgen/SKILL.md +66 -3
package/README.md
CHANGED
|
@@ -26,15 +26,21 @@ npm run build
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
node -v
|
|
30
|
+
npm -v
|
|
29
31
|
npm install -g @ai-agent-tools/picgen
|
|
32
|
+
npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy
|
|
33
|
+
picgen skill install codex
|
|
30
34
|
picgen --help
|
|
31
35
|
picgen quickstart
|
|
32
36
|
```
|
|
33
37
|
|
|
38
|
+
Use `npx -y skills add ...` for cross-agent skill installation when supported. `picgen skill install codex` is a Codex-only fallback that copies the bundled skill into `~/.codex/skills/picgen`.
|
|
39
|
+
|
|
34
40
|
Agent trial prompt:
|
|
35
41
|
|
|
36
42
|
```text
|
|
37
|
-
|
|
43
|
+
请帮我安装并配置 PicGen 生图工具。安装前请先检查这台电脑是否已经安装 Node.js 和 npm。如果没有安装,请先指导我安装 Node.js LTS 版本,并验证 node -v 和 npm -v 是否能正常显示版本号。然后执行 npm install -g @ai-agent-tools/picgen@latest 安装 CLI,再执行 npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy 安装 PicGen skill。如果 skills 安装器不可用且当前是 Codex,请改用 picgen skill install codex。然后引导我配置 provider 和 API key,先预览生成方案,等我确认后再生成测试图。
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
For first-user rollout, see [docs/release-alpha.md](./docs/release-alpha.md).
|
|
@@ -44,6 +50,8 @@ For first-user rollout, see [docs/release-alpha.md](./docs/release-alpha.md).
|
|
|
44
50
|
```bash
|
|
45
51
|
picgen setup
|
|
46
52
|
picgen quickstart
|
|
53
|
+
npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy
|
|
54
|
+
picgen skill install codex
|
|
47
55
|
picgen update check
|
|
48
56
|
picgen doctor --json
|
|
49
57
|
picgen create --dry-run "一张产品发布会主视觉"
|
|
@@ -52,6 +60,10 @@ picgen create --dry-run --provider gemini_official --reference ./reference.png "
|
|
|
52
60
|
picgen create --yes --provider gemini_official --reference ./reference.png "基于参考图生成一张品牌海报"
|
|
53
61
|
picgen provider list
|
|
54
62
|
picgen provider add
|
|
63
|
+
picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer
|
|
64
|
+
picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard
|
|
65
|
+
picgen key set PICGEN_GEMINI_PROXY_KEY --stdin
|
|
66
|
+
picgen key list --json
|
|
55
67
|
picgen provider test openai_official --json
|
|
56
68
|
picgen provider prefer gemini_official
|
|
57
69
|
picgen provider disable gemini_proxy
|
|
@@ -99,6 +111,8 @@ For non-technical users, `picgen setup` can save API keys for you in:
|
|
|
99
111
|
|
|
100
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.
|
|
101
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
|
+
|
|
102
116
|
You can start from the included example:
|
|
103
117
|
|
|
104
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,
|
|
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(
|
|
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(
|
|
235
|
-
return
|
|
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.
|
|
1057
|
+
var VERSION = "0.1.0-alpha.8";
|
|
1058
1058
|
|
|
1059
1059
|
// src/commands/update.ts
|
|
1060
1060
|
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1225,6 +1225,182 @@ async function runDoctor(options) {
|
|
|
1225
1225
|
await maybePrintUpdateHint();
|
|
1226
1226
|
}
|
|
1227
1227
|
|
|
1228
|
+
// src/commands/key.ts
|
|
1229
|
+
import { execFile } from "child_process";
|
|
1230
|
+
import { promisify } from "util";
|
|
1231
|
+
import { password } from "@inquirer/prompts";
|
|
1232
|
+
|
|
1233
|
+
// src/config/env.ts
|
|
1234
|
+
import { chmod, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1235
|
+
import { existsSync } from "fs";
|
|
1236
|
+
import { createHash } from "crypto";
|
|
1237
|
+
import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
|
|
1238
|
+
import { homedir as homedir3 } from "os";
|
|
1239
|
+
import { parse } from "dotenv";
|
|
1240
|
+
var loadedEnvSources = /* @__PURE__ */ new Map();
|
|
1241
|
+
function getManagedEnvPath() {
|
|
1242
|
+
return process.env.PICGEN_ENV_PATH ?? join5(homedir3(), ".picgen", ".env");
|
|
1243
|
+
}
|
|
1244
|
+
async function loadPicgenEnv() {
|
|
1245
|
+
const shellEnv = new Set(Object.keys(process.env));
|
|
1246
|
+
await loadEnvFile(getManagedEnvPath(), shellEnv, false, "managed");
|
|
1247
|
+
await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true, "project");
|
|
1248
|
+
}
|
|
1249
|
+
async function saveManagedEnvVar(name, value) {
|
|
1250
|
+
const path = getManagedEnvPath();
|
|
1251
|
+
const current = await readManagedEnvFile(path);
|
|
1252
|
+
const next = {
|
|
1253
|
+
...current,
|
|
1254
|
+
[name]: value
|
|
1255
|
+
};
|
|
1256
|
+
await mkdir4(dirname3(path), { recursive: true });
|
|
1257
|
+
await writeFile4(path, stringifyEnv(next), "utf8");
|
|
1258
|
+
await chmod(path, 384);
|
|
1259
|
+
process.env[name] = value;
|
|
1260
|
+
loadedEnvSources.set(name, { source: "managed", path });
|
|
1261
|
+
return path;
|
|
1262
|
+
}
|
|
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) {
|
|
1294
|
+
if (!existsSync(path)) return;
|
|
1295
|
+
const parsed = parse(await readFile4(path, "utf8"));
|
|
1296
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
1297
|
+
if (shellEnv.has(name)) continue;
|
|
1298
|
+
if (!overrideManagedValues && process.env[name] !== void 0) continue;
|
|
1299
|
+
process.env[name] = value;
|
|
1300
|
+
loadedEnvSources.set(name, { source, path });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async function readManagedEnvFile(path) {
|
|
1304
|
+
return readEnvFile(path);
|
|
1305
|
+
}
|
|
1306
|
+
async function readEnvFile(path) {
|
|
1307
|
+
try {
|
|
1308
|
+
return parse(await readFile4(path, "utf8"));
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (error.code === "ENOENT") return {};
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function stringifyEnv(values) {
|
|
1315
|
+
return `${Object.entries(values).map(([name, value]) => `${name}=${quoteEnvValue(value)}`).join("\n")}
|
|
1316
|
+
`;
|
|
1317
|
+
}
|
|
1318
|
+
function quoteEnvValue(value) {
|
|
1319
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
|
|
1320
|
+
return JSON.stringify(value);
|
|
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 <= 8) return "*".repeat(value.length);
|
|
1335
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/commands/key.ts
|
|
1339
|
+
var execFileAsync = promisify(execFile);
|
|
1340
|
+
async function setApiKey(name, options) {
|
|
1341
|
+
validateEnvName(name);
|
|
1342
|
+
const value = options.clipboard ? await readClipboard() : options.stdin ? await readStdin() : options.value ? options.value : await password({
|
|
1343
|
+
message: `Paste API key for ${name}`,
|
|
1344
|
+
mask: "*"
|
|
1345
|
+
});
|
|
1346
|
+
if (!value.trim()) {
|
|
1347
|
+
throw new Error("API key is empty.");
|
|
1348
|
+
}
|
|
1349
|
+
const path = await saveManagedEnvVar(name, value.trim());
|
|
1350
|
+
console.log(`Saved ${name} to ${path}`);
|
|
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
|
+
}
|
|
1373
|
+
function validateEnvName(name) {
|
|
1374
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
1375
|
+
throw new Error(`Invalid environment variable name: ${name}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async function readStdin() {
|
|
1379
|
+
const chunks = [];
|
|
1380
|
+
for await (const chunk of process.stdin) {
|
|
1381
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1382
|
+
}
|
|
1383
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
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
|
+
}
|
|
1403
|
+
|
|
1228
1404
|
// src/commands/provider.ts
|
|
1229
1405
|
import { input, select } from "@inquirer/prompts";
|
|
1230
1406
|
|
|
@@ -1370,6 +1546,31 @@ async function addProvider() {
|
|
|
1370
1546
|
await saveConfig(config);
|
|
1371
1547
|
console.log(`Added provider: ${provider2.name}`);
|
|
1372
1548
|
}
|
|
1549
|
+
async function quickAddProvider(templateName, options) {
|
|
1550
|
+
const config = await loadConfig();
|
|
1551
|
+
const template = quickProviderTemplate(templateName);
|
|
1552
|
+
const name = options.name ?? nextAvailableProviderName(config, template.name);
|
|
1553
|
+
const provider2 = {
|
|
1554
|
+
enabled: true,
|
|
1555
|
+
protocol: template.protocol,
|
|
1556
|
+
channel: template.channel,
|
|
1557
|
+
base_url: normalizeProviderBaseUrl(options.host ?? template.base_url),
|
|
1558
|
+
api_key_env: options.keyEnv ?? template.api_key_env,
|
|
1559
|
+
models: parseModels(options.models ?? template.models.join(",")),
|
|
1560
|
+
capabilities: defaultCapabilitiesForProtocol2(template.protocol)
|
|
1561
|
+
};
|
|
1562
|
+
addProviderToConfig(config, name, provider2);
|
|
1563
|
+
if (options.prefer) {
|
|
1564
|
+
setPreferredProvider(config, name);
|
|
1565
|
+
}
|
|
1566
|
+
await saveConfig(config);
|
|
1567
|
+
console.log(`Added provider: ${name}`);
|
|
1568
|
+
console.log(`Protocol: ${provider2.protocol}`);
|
|
1569
|
+
console.log(`Host: ${provider2.base_url}`);
|
|
1570
|
+
console.log(`API key env: ${provider2.api_key_env}`);
|
|
1571
|
+
console.log(`Models: ${provider2.models.join(",")}`);
|
|
1572
|
+
if (options.prefer) console.log(`Preferred provider: ${name}`);
|
|
1573
|
+
}
|
|
1373
1574
|
function addProviderToConfig(config, name, provider2) {
|
|
1374
1575
|
config.providers[name] = provider2;
|
|
1375
1576
|
const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
|
|
@@ -1496,6 +1697,53 @@ function nextAvailableProviderName(config, baseName, existingName) {
|
|
|
1496
1697
|
function defaultCapabilitiesForProtocol2(protocol) {
|
|
1497
1698
|
return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
|
|
1498
1699
|
}
|
|
1700
|
+
function quickProviderTemplate(templateName) {
|
|
1701
|
+
switch (templateName.replaceAll("_", "-")) {
|
|
1702
|
+
case "openai-proxy":
|
|
1703
|
+
return {
|
|
1704
|
+
name: "openai_proxy",
|
|
1705
|
+
protocol: "openai-images",
|
|
1706
|
+
channel: "third_party",
|
|
1707
|
+
base_url: "https://www.pandai.vip",
|
|
1708
|
+
api_key_env: "PICGEN_OPENAI_PROXY_KEY",
|
|
1709
|
+
models: ["gpt-image-2"]
|
|
1710
|
+
};
|
|
1711
|
+
case "gemini-proxy":
|
|
1712
|
+
return {
|
|
1713
|
+
name: "gemini_proxy",
|
|
1714
|
+
protocol: "gemini",
|
|
1715
|
+
channel: "third_party",
|
|
1716
|
+
base_url: "https://www.pandai.vip",
|
|
1717
|
+
api_key_env: "PICGEN_GEMINI_PROXY_KEY",
|
|
1718
|
+
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1719
|
+
};
|
|
1720
|
+
case "openai-official":
|
|
1721
|
+
return {
|
|
1722
|
+
name: "openai_official",
|
|
1723
|
+
protocol: "openai-images",
|
|
1724
|
+
channel: "official",
|
|
1725
|
+
base_url: defaultProviderBaseUrl("openai-images"),
|
|
1726
|
+
api_key_env: "OPENAI_API_KEY",
|
|
1727
|
+
models: ["gpt-image-2"]
|
|
1728
|
+
};
|
|
1729
|
+
case "gemini-official":
|
|
1730
|
+
return {
|
|
1731
|
+
name: "gemini_official",
|
|
1732
|
+
protocol: "gemini",
|
|
1733
|
+
channel: "official",
|
|
1734
|
+
base_url: defaultProviderBaseUrl("gemini"),
|
|
1735
|
+
api_key_env: "GEMINI_API_KEY",
|
|
1736
|
+
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1737
|
+
};
|
|
1738
|
+
default:
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
`Unknown provider template: ${templateName}. Use openai-proxy, gemini-proxy, openai-official, or gemini-official.`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function parseModels(raw) {
|
|
1745
|
+
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
1746
|
+
}
|
|
1499
1747
|
|
|
1500
1748
|
// src/commands/preferences.ts
|
|
1501
1749
|
async function preferMode(name) {
|
|
@@ -1521,10 +1769,18 @@ function formatQuickstart() {
|
|
|
1521
1769
|
"PicGen quickstart",
|
|
1522
1770
|
"",
|
|
1523
1771
|
"Install:",
|
|
1772
|
+
" node -v",
|
|
1773
|
+
" npm -v",
|
|
1524
1774
|
" npm install -g @ai-agent-tools/picgen",
|
|
1775
|
+
" npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy",
|
|
1776
|
+
" picgen skill install codex",
|
|
1525
1777
|
"",
|
|
1526
1778
|
"Configure:",
|
|
1527
1779
|
" picgen setup # can save provider API keys for you",
|
|
1780
|
+
" picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer",
|
|
1781
|
+
" picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard",
|
|
1782
|
+
" picgen key set PICGEN_GEMINI_PROXY_KEY --stdin",
|
|
1783
|
+
" picgen key list --json",
|
|
1528
1784
|
" picgen doctor --json",
|
|
1529
1785
|
"",
|
|
1530
1786
|
"Preview before spending quota:",
|
|
@@ -1537,7 +1793,7 @@ function formatQuickstart() {
|
|
|
1537
1793
|
' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
|
|
1538
1794
|
"",
|
|
1539
1795
|
"Agent prompt:",
|
|
1540
|
-
" \u8BF7\u5B89\u88C5\u5E76\
|
|
1796
|
+
" \u8BF7\u5E2E\u6211\u5B89\u88C5\u5E76\u914D\u7F6E PicGen \u751F\u56FE\u5DE5\u5177\u3002\u5B89\u88C5\u524D\u8BF7\u5148\u68C0\u67E5\u8FD9\u53F0\u7535\u8111\u662F\u5426\u5DF2\u7ECF\u5B89\u88C5 Node.js \u548C npm\u3002\u5982\u679C\u6CA1\u6709\u5B89\u88C5\uFF0C\u8BF7\u5148\u6307\u5BFC\u6211\u5B89\u88C5 Node.js LTS \u7248\u672C\uFF0C\u5E76\u9A8C\u8BC1 node -v \u548C npm -v \u662F\u5426\u80FD\u6B63\u5E38\u663E\u793A\u7248\u672C\u53F7\u3002\u7136\u540E\u6267\u884C npm install -g @ai-agent-tools/picgen@latest \u5B89\u88C5 CLI\uFF0C\u518D\u6267\u884C npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy \u5B89\u88C5 PicGen skill\u3002\u5982\u679C skills \u5B89\u88C5\u5668\u4E0D\u53EF\u7528\u4E14\u5F53\u524D\u662F Codex\uFF0C\u8BF7\u6539\u7528 picgen skill install codex\u3002\u7136\u540E\u5F15\u5BFC\u6211\u914D\u7F6E provider \u548C API key\uFF0C\u5148\u9884\u89C8\u751F\u6210\u65B9\u6848\uFF0C\u7B49\u6211\u786E\u8BA4\u540E\u518D\u751F\u6210\u6D4B\u8BD5\u56FE\u3002",
|
|
1541
1797
|
"",
|
|
1542
1798
|
"Notes:",
|
|
1543
1799
|
" - Provider host URLs should not include /v1 or /v1beta.",
|
|
@@ -1549,62 +1805,7 @@ function formatQuickstart() {
|
|
|
1549
1805
|
}
|
|
1550
1806
|
|
|
1551
1807
|
// src/commands/setup.ts
|
|
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
|
|
1808
|
+
import { confirm as confirm2, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
1608
1809
|
async function runSetup() {
|
|
1609
1810
|
await ensureConfig();
|
|
1610
1811
|
console.log(`PicGen config: ${getConfigPath()}`);
|
|
@@ -1626,7 +1827,7 @@ async function runSetup() {
|
|
|
1626
1827
|
]
|
|
1627
1828
|
});
|
|
1628
1829
|
if (action === "quick-add") {
|
|
1629
|
-
await
|
|
1830
|
+
await quickAddProvider2();
|
|
1630
1831
|
} else if (action === "provider") {
|
|
1631
1832
|
await chooseDefaultProvider();
|
|
1632
1833
|
} else if (action === "mode") {
|
|
@@ -1711,7 +1912,7 @@ async function chooseProviderKeyToConfigure() {
|
|
|
1711
1912
|
});
|
|
1712
1913
|
await configureProviderApiKey(config.providers[name]);
|
|
1713
1914
|
}
|
|
1714
|
-
async function
|
|
1915
|
+
async function quickAddProvider2() {
|
|
1715
1916
|
const config = await loadConfig();
|
|
1716
1917
|
const template = await select2({
|
|
1717
1918
|
message: "Choose the provider/channel you want to add",
|
|
@@ -1757,7 +1958,7 @@ async function quickAddProvider() {
|
|
|
1757
1958
|
channel: defaults.channel,
|
|
1758
1959
|
base_url: normalizeProviderBaseUrl(baseUrl),
|
|
1759
1960
|
api_key_env: apiKeyEnv,
|
|
1760
|
-
models:
|
|
1961
|
+
models: parseModels2(modelsRaw),
|
|
1761
1962
|
capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
|
|
1762
1963
|
};
|
|
1763
1964
|
addProviderToConfig(config, name, provider2);
|
|
@@ -1780,7 +1981,7 @@ async function configureProviderApiKey(provider2) {
|
|
|
1780
1981
|
});
|
|
1781
1982
|
if (!replace) return;
|
|
1782
1983
|
}
|
|
1783
|
-
const value = await
|
|
1984
|
+
const value = await password2({
|
|
1784
1985
|
message: `Paste API key for ${provider2.api_key_env} (leave empty to skip)`,
|
|
1785
1986
|
mask: "*"
|
|
1786
1987
|
});
|
|
@@ -1832,7 +2033,7 @@ function quickProviderDefaults(template) {
|
|
|
1832
2033
|
};
|
|
1833
2034
|
}
|
|
1834
2035
|
}
|
|
1835
|
-
function
|
|
2036
|
+
function parseModels2(raw) {
|
|
1836
2037
|
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
1837
2038
|
}
|
|
1838
2039
|
function providerLabel(provider2) {
|
|
@@ -1854,6 +2055,47 @@ function modeLabel(modeName) {
|
|
|
1854
2055
|
}
|
|
1855
2056
|
}
|
|
1856
2057
|
|
|
2058
|
+
// src/commands/skill.ts
|
|
2059
|
+
import { access, cp, mkdir as mkdir5 } from "fs/promises";
|
|
2060
|
+
import { constants } from "fs";
|
|
2061
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
2062
|
+
import { homedir as homedir4 } from "os";
|
|
2063
|
+
import { fileURLToPath } from "url";
|
|
2064
|
+
async function installSkill(target, options) {
|
|
2065
|
+
if (target !== "codex") {
|
|
2066
|
+
throw new Error(`Unsupported skill target: ${target}. Supported target: codex.`);
|
|
2067
|
+
}
|
|
2068
|
+
const source = await findBundledPicgenSkill();
|
|
2069
|
+
const destination = join6(getCodexHome(), "skills", "picgen");
|
|
2070
|
+
await mkdir5(dirname4(destination), { recursive: true });
|
|
2071
|
+
await cp(source, destination, {
|
|
2072
|
+
recursive: true,
|
|
2073
|
+
force: options.force ?? false,
|
|
2074
|
+
errorOnExist: !(options.force ?? false)
|
|
2075
|
+
});
|
|
2076
|
+
console.log(`Installed PicGen skill for Codex: ${destination}`);
|
|
2077
|
+
console.log("Restart Codex or start a new Codex session if the skill is not visible yet.");
|
|
2078
|
+
}
|
|
2079
|
+
function getCodexHome() {
|
|
2080
|
+
return process.env.CODEX_HOME ?? join6(homedir4(), ".codex");
|
|
2081
|
+
}
|
|
2082
|
+
async function findBundledPicgenSkill() {
|
|
2083
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
2084
|
+
const candidates = [
|
|
2085
|
+
resolve3(here, "../skills/picgen"),
|
|
2086
|
+
resolve3(here, "../../skills/picgen"),
|
|
2087
|
+
resolve3(process.cwd(), "skills/picgen")
|
|
2088
|
+
];
|
|
2089
|
+
for (const candidate of candidates) {
|
|
2090
|
+
try {
|
|
2091
|
+
await access(join6(candidate, "SKILL.md"), constants.R_OK);
|
|
2092
|
+
return candidate;
|
|
2093
|
+
} catch {
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
throw new Error("Bundled PicGen skill not found. Reinstall @ai-agent-tools/picgen and try again.");
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1857
2099
|
// src/cli.ts
|
|
1858
2100
|
await loadPicgenEnv();
|
|
1859
2101
|
var program = new Command();
|
|
@@ -1870,12 +2112,18 @@ program.command("create").description("Create an image generation plan or genera
|
|
|
1870
2112
|
var provider = program.command("provider").description("Manage providers/channels.");
|
|
1871
2113
|
provider.command("list").description("List providers.").action(listProviders);
|
|
1872
2114
|
provider.command("add").description("Add a provider.").action(addProvider);
|
|
2115
|
+
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);
|
|
1873
2116
|
provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
|
|
1874
2117
|
provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
|
|
1875
2118
|
provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
|
|
1876
2119
|
provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
|
|
1877
2120
|
provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
|
|
1878
2121
|
provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
|
|
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);
|
|
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);
|
|
1879
2127
|
program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
|
|
1880
2128
|
program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
|
|
1881
2129
|
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);
|
package/docs/release-alpha.md
CHANGED
|
@@ -5,19 +5,25 @@ This checklist is for the first internal or friend-and-colleague trial of PicGen
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
node -v
|
|
9
|
+
npm -v
|
|
8
10
|
npm install -g @ai-agent-tools/picgen
|
|
11
|
+
npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy
|
|
12
|
+
picgen skill install codex
|
|
9
13
|
picgen --help
|
|
10
14
|
picgen quickstart
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
Node.js 20 or newer is required.
|
|
14
18
|
|
|
19
|
+
`npx -y skills add ...` is the preferred cross-agent skill installation path when supported. `picgen skill install codex` installs the bundled PicGen skill into `~/.codex/skills/picgen` as a Codex-only fallback. Restart the agent or start a new session if the skill is not visible yet.
|
|
20
|
+
|
|
15
21
|
## Agent Prompt
|
|
16
22
|
|
|
17
23
|
Send this to Codex, Trae, Claude Code, or a similar coding agent:
|
|
18
24
|
|
|
19
25
|
```text
|
|
20
|
-
|
|
26
|
+
请帮我安装并配置 PicGen 生图工具。安装前请先检查这台电脑是否已经安装 Node.js 和 npm。如果没有安装,请先指导我安装 Node.js LTS 版本,并验证 node -v 和 npm -v 是否能正常显示版本号。然后执行 npm install -g @ai-agent-tools/picgen@latest 安装 CLI,再执行 npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy 安装 PicGen skill。如果 skills 安装器不可用且当前是 Codex,请改用 picgen skill install codex。然后引导我配置 provider 和 API key,先预览生成方案,等我确认后再生成测试图。
|
|
21
27
|
```
|
|
22
28
|
|
|
23
29
|
## First Run
|
|
@@ -50,6 +56,16 @@ For non-technical users, prefer `picgen setup`. It can save provider API keys in
|
|
|
50
56
|
|
|
51
57
|
PicGen loads this file automatically.
|
|
52
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
|
+
|
|
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:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer
|
|
65
|
+
picgen key set PICGEN_GEMINI_PROXY_KEY --stdin
|
|
66
|
+
picgen provider test gemini_proxy --json
|
|
67
|
+
```
|
|
68
|
+
|
|
53
69
|
Advanced users can still use shell environment variables or a local project `.env`:
|
|
54
70
|
|
|
55
71
|
```bash
|
package/package.json
CHANGED
package/skills/picgen/SKILL.md
CHANGED
|
@@ -17,10 +17,32 @@ Only suggest PicGen when the user discusses visual direction, mood, brand style,
|
|
|
17
17
|
|
|
18
18
|
Never silently spend user quota. Do not send full conversation context to providers by default; summarize only the visual details needed for the final prompt.
|
|
19
19
|
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
If `picgen` is not available, install the CLI first:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @ai-agent-tools/picgen@latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To install or update this skill for supported agents, prefer the standard Skills installer:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For Codex only, the CLI also provides a direct fallback:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
picgen skill install codex --force
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
After installing or updating a skill, the user may need to restart the agent or open a new session before the skill is visible.
|
|
41
|
+
|
|
20
42
|
## Workflow
|
|
21
43
|
|
|
22
44
|
1. Run `picgen doctor --json` to check configuration.
|
|
23
|
-
2. If no usable provider is configured,
|
|
45
|
+
2. If no usable provider is configured, configure one before generation.
|
|
24
46
|
3. Choose a preset from the user's intent, such as `poster`, `product-shot`, or `social-cover`.
|
|
25
47
|
4. Run `picgen create --dry-run --preset <preset> "<prompt>"`.
|
|
26
48
|
5. Present the dry-run as a user-facing generation preview. Do not expose `dry-run` as a technical term unless useful.
|
|
@@ -29,6 +51,47 @@ Never silently spend user quota. Do not send full conversation context to provid
|
|
|
29
51
|
|
|
30
52
|
If the user explicitly says to generate directly or not ask for confirmation, you may skip the user-facing confirmation step. Still form a generation plan internally.
|
|
31
53
|
|
|
54
|
+
## Provider Setup
|
|
55
|
+
|
|
56
|
+
When terminal prompts are visible to the user, `picgen setup` is acceptable.
|
|
57
|
+
|
|
58
|
+
When running inside an agent environment where interactive terminal prompts are not visible, do not run `picgen setup` as a blocking wizard. Ask the user for:
|
|
59
|
+
|
|
60
|
+
- Provider type: Gemini or OpenAI-compatible.
|
|
61
|
+
- Provider host: host only, such as `https://www.pandai.vip`; do not include `/v1` or `/v1beta`.
|
|
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
|
+
|
|
64
|
+
Then use non-interactive commands.
|
|
65
|
+
|
|
66
|
+
Gemini-compatible third-party channel:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer
|
|
70
|
+
picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard
|
|
71
|
+
picgen provider test gemini_proxy --json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
OpenAI-compatible third-party channel:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
picgen provider quick-add openai-proxy --host https://www.pandai.vip --prefer
|
|
78
|
+
picgen key set PICGEN_OPENAI_PROXY_KEY --clipboard
|
|
79
|
+
picgen provider test openai_proxy --json
|
|
80
|
+
```
|
|
81
|
+
|
|
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."
|
|
94
|
+
|
|
32
95
|
For reference-image generation, pass local images with repeated `--reference <path>` flags:
|
|
33
96
|
|
|
34
97
|
```bash
|
|
@@ -73,9 +136,9 @@ PicGen redacts generated image payloads and Gemini thought signatures from metad
|
|
|
73
136
|
|
|
74
137
|
## Error Handling
|
|
75
138
|
|
|
76
|
-
If `doctor` reports no usable provider,
|
|
139
|
+
If `doctor` reports no usable provider, configure a provider. Prefer non-interactive setup in agent environments.
|
|
77
140
|
|
|
78
|
-
If an API key is missing, guide the user to run `picgen setup`
|
|
141
|
+
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.
|
|
79
142
|
|
|
80
143
|
If a provider is disabled, suggest enabling it or using a one-off provider override.
|
|
81
144
|
|