focalapi-cli 0.4.1 → 0.5.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli.js +148 -10
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-08-26
4
+
5
+ - Added `@file` support to `--style-references` and `--moodboards`: the JSON array can now be read from a file instead of inline argv. Windows `cmd` mangles embedded double quotes in JSON arguments (matrix-run evidence 2026-08-25), and the file source sidesteps shell quoting entirely.
6
+
7
+ ## 0.5.0 - 2026-08-25
8
+
9
+ - Fixed `krea-2-medium/-turbo/large` reference-image validation: the local table said 0 while the platform contract accepts up to 10 reference images (each maps to a Krea style reference), so every `gen image --image` edit was rejected locally without reaching the server (2026-08-25 live audit P01).
10
+ - Added local constraints for the August image wave so invalid parameters are intercepted before submission: `topaz-image-reimagine`/`-bloom-2`/`-wonder-3-5`, `wavespeed-seedvr2-upscale`/`-ultimate-upscale`, `quiver-text-to-svg`/`-image-to-svg`, `hitpaw-image-enhance`/`-portrait-enhance`, `beeble-switchx-image-720p`/`-1080p`, `recraft-v4`/`-v4-pro`.
11
+ - Enforce the exact-one-input-image requirement locally for the enhance/upscale/vectorize/switch families instead of surfacing the server 400.
12
+ - Map `--resolution` to the `target_resolution` wire field for WaveSpeed upscales (the generic `resolution` field is silently ignored by that family).
13
+ - Send Topaz `--creativity` as an integer 1-9 (the platform contract takes a number; the Krea string modes are unchanged).
14
+ - Validate Recraft sizes against the official V4 tables (non-pro caps at ~1.5K, pro at ~3K) and reject reference images locally (the channel is text-to-image only).
15
+ - Intercept `gen image` on Gemini image models with a direct pointer to `gen gemini-image` (native generateContent endpoint) instead of an opaque upstream failure (audit P07).
16
+
3
17
  ## 0.4.1 - 2026-08-18
4
18
 
5
19
  - Added `@file` syntax for local reference media on all gen paths (`--image @C:/path/ref.jpg`): the file is read and inlined as a data URI with per-file 8MB and per-command 12MB guards — local files no longer require manual hosting.
package/dist/cli.js CHANGED
@@ -176,7 +176,7 @@ function displayWidth(s) {
176
176
  }
177
177
 
178
178
  // src/lib/version.ts
179
- var VERSION = true ? "0.4.1" : "0.0.0-dev";
179
+ var VERSION = true ? "0.5.1" : "0.0.0-dev";
180
180
 
181
181
  // src/commands/auth.ts
182
182
  import { createInterface } from "readline/promises";
@@ -1050,7 +1050,7 @@ var IMAGE_CONSTRAINTS = {
1050
1050
  },
1051
1051
  "krea-2-medium": {
1052
1052
  maxN: 1,
1053
- maxReferenceImages: 0,
1053
+ maxReferenceImages: 10,
1054
1054
  aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
1055
1055
  resolutions: ["1k"],
1056
1056
  supportsSeed: true,
@@ -1061,7 +1061,7 @@ var IMAGE_CONSTRAINTS = {
1061
1061
  },
1062
1062
  "krea-2-medium-turbo": {
1063
1063
  maxN: 1,
1064
- maxReferenceImages: 0,
1064
+ maxReferenceImages: 10,
1065
1065
  aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
1066
1066
  resolutions: ["1k"],
1067
1067
  supportsSeed: true,
@@ -1072,7 +1072,7 @@ var IMAGE_CONSTRAINTS = {
1072
1072
  },
1073
1073
  "krea-2-large": {
1074
1074
  maxN: 1,
1075
- maxReferenceImages: 0,
1075
+ maxReferenceImages: 10,
1076
1076
  aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
1077
1077
  resolutions: ["1k"],
1078
1078
  supportsSeed: true,
@@ -1106,8 +1106,112 @@ var IMAGE_CONSTRAINTS = {
1106
1106
  supportsNegativePrompt: true,
1107
1107
  supportsPromptExtend: true,
1108
1108
  supportsWatermark: true
1109
+ },
1110
+ "topaz-image-reimagine": {
1111
+ maxN: 1,
1112
+ minImageCount: 1,
1113
+ maxReferenceImages: 1,
1114
+ integerCreativity: [1, 9]
1115
+ },
1116
+ "topaz-image-bloom-2": {
1117
+ maxN: 1,
1118
+ minImageCount: 1,
1119
+ maxReferenceImages: 1
1120
+ },
1121
+ "topaz-image-wonder-3-5": {
1122
+ maxN: 1,
1123
+ minImageCount: 1,
1124
+ maxReferenceImages: 1
1125
+ },
1126
+ "wavespeed-seedvr2-upscale": {
1127
+ maxN: 1,
1128
+ minImageCount: 1,
1129
+ maxReferenceImages: 1,
1130
+ resolutions: ["2k", "4k", "8k"],
1131
+ resolutionParam: "target_resolution"
1132
+ },
1133
+ "wavespeed-ultimate-upscale": {
1134
+ maxN: 1,
1135
+ minImageCount: 1,
1136
+ maxReferenceImages: 1,
1137
+ resolutions: ["2k", "4k", "8k"],
1138
+ resolutionParam: "target_resolution"
1139
+ },
1140
+ "quiver-text-to-svg": {
1141
+ maxN: 1,
1142
+ maxReferenceImages: 4
1143
+ },
1144
+ "quiver-image-to-svg": {
1145
+ maxN: 1,
1146
+ minImageCount: 1,
1147
+ maxReferenceImages: 1
1148
+ },
1149
+ "hitpaw-image-enhance": {
1150
+ maxN: 1,
1151
+ minImageCount: 1,
1152
+ maxReferenceImages: 1
1153
+ },
1154
+ "hitpaw-image-portrait-enhance": {
1155
+ maxN: 1,
1156
+ minImageCount: 1,
1157
+ maxReferenceImages: 1
1158
+ },
1159
+ "beeble-switchx-image-720p": {
1160
+ maxN: 1,
1161
+ minImageCount: 1,
1162
+ maxReferenceImages: 2
1163
+ },
1164
+ "beeble-switchx-image-1080p": {
1165
+ maxN: 1,
1166
+ minImageCount: 1,
1167
+ maxReferenceImages: 2
1168
+ },
1169
+ "recraft-v4": {
1170
+ defaultSize: "1024x1024",
1171
+ maxN: 6,
1172
+ maxReferenceImages: 0,
1173
+ allowedSizes: [
1174
+ "1024x1024",
1175
+ "1280x832",
1176
+ "832x1280",
1177
+ "1216x896",
1178
+ "896x1216",
1179
+ "1152x896",
1180
+ "896x1152",
1181
+ "1536x768",
1182
+ "768x1536",
1183
+ "1344x768",
1184
+ "768x1344",
1185
+ "832x1344",
1186
+ "1280x896",
1187
+ "896x1280"
1188
+ ]
1189
+ },
1190
+ "recraft-v4-pro": {
1191
+ defaultSize: "2048x2048",
1192
+ maxN: 6,
1193
+ maxReferenceImages: 0,
1194
+ allowedSizes: [
1195
+ "2048x2048",
1196
+ "2560x1664",
1197
+ "1664x2560",
1198
+ "2432x1792",
1199
+ "1792x2432",
1200
+ "2304x1792",
1201
+ "1792x2304",
1202
+ "3072x1536",
1203
+ "1536x3072",
1204
+ "2688x1536",
1205
+ "1536x2688",
1206
+ "1664x2688",
1207
+ "2560x1792",
1208
+ "1792x2560"
1209
+ ]
1109
1210
  }
1110
1211
  };
1212
+ function getImageConstraint(model) {
1213
+ return IMAGE_CONSTRAINTS[model.trim()];
1214
+ }
1111
1215
  var SEEDANCE_RATIOS = ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
1112
1216
  var GROK_VIDEO_ASPECT_RATIOS = ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"];
1113
1217
  var KLING_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "1:1"];
@@ -1285,11 +1389,18 @@ function validateImageGeneration(model, input) {
1285
1389
  if (input.responseFormat && input.responseFormat !== "url" && input.responseFormat !== "b64_json") {
1286
1390
  throw new ApiError("invalid_request", `response_format must be url or b64_json (received: ${input.responseFormat})`);
1287
1391
  }
1288
- const constraint = IMAGE_CONSTRAINTS[model.trim()];
1392
+ const trimmedModel = model.trim();
1393
+ if (GEMINI_IMAGE_CONSTRAINTS[trimmedModel]) {
1394
+ throw new ApiError("invalid_request", `${model} is a Gemini image model; use \`focalapi gen gemini-image\` (native generateContent endpoint) instead of \`gen image\``);
1395
+ }
1396
+ const constraint = IMAGE_CONSTRAINTS[trimmedModel];
1289
1397
  if (!constraint) return;
1290
1398
  if (input.n < 1 || input.n > constraint.maxN) {
1291
1399
  throw new ApiError("invalid_request", `${model} n must be 1-${constraint.maxN} (received: ${input.n})`);
1292
1400
  }
1401
+ if (input.imageCount !== void 0 && constraint.minImageCount !== void 0 && input.imageCount < constraint.minImageCount) {
1402
+ throw new ApiError("invalid_request", `${model} requires at least ${constraint.minImageCount} input image${constraint.minImageCount === 1 ? "" : "s"}`);
1403
+ }
1293
1404
  if (input.imageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.imageCount > constraint.maxReferenceImages) {
1294
1405
  throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference images`);
1295
1406
  }
@@ -1305,7 +1416,16 @@ function validateImageGeneration(model, input) {
1305
1416
  validateOptionalChoice(model, "resolution", input.resolution, constraint.resolutions);
1306
1417
  validateOptionalChoice(model, "output_format", input.outputFormat, constraint.outputFormats);
1307
1418
  validateOptionalChoice(model, "optimize_prompt", input.optimizePrompt, constraint.optimizePromptModes);
1308
- validateOptionalChoice(model, "creativity", input.creativity, constraint.creativityModes);
1419
+ if (!constraint.integerCreativity) {
1420
+ validateOptionalChoice(model, "creativity", input.creativity, constraint.creativityModes);
1421
+ }
1422
+ if (input.creativity !== void 0 && constraint.integerCreativity) {
1423
+ const [minimum, maximum] = constraint.integerCreativity;
1424
+ const creativity = Number(input.creativity);
1425
+ if (!Number.isInteger(creativity) || creativity < minimum || creativity > maximum) {
1426
+ throw new ApiError("invalid_request", `${model} creativity must be an integer ${minimum}-${maximum} (received: ${input.creativity})`);
1427
+ }
1428
+ }
1309
1429
  if (input.negativePrompt !== void 0 && !constraint.supportsNegativePrompt) {
1310
1430
  throw new ApiError("invalid_request", `${model} does not support negative_prompt`);
1311
1431
  }
@@ -1340,6 +1460,12 @@ function validateImageGeneration(model, input) {
1340
1460
  return;
1341
1461
  }
1342
1462
  const suppliedSize = input.size ?? constraint.defaultSize;
1463
+ if (constraint.allowedSizes) {
1464
+ if (!constraint.allowedSizes.includes(suppliedSize.toLowerCase())) {
1465
+ throw new ApiError("invalid_request", `${model} size must be one of ${constraint.allowedSizes.join(", ")} (received: ${suppliedSize})`);
1466
+ }
1467
+ return;
1468
+ }
1343
1469
  if (constraint.sizeTiers?.includes(suppliedSize.toLowerCase())) return;
1344
1470
  if (constraint.sizeTiers && !/^\d+x\d+$/i.test(suppliedSize)) {
1345
1471
  throw new ApiError("invalid_request", `${model} size must be one of ${constraint.sizeTiers.join(", ")} or WIDTHxHEIGHT (received: ${suppliedSize})`);
@@ -1817,6 +1943,15 @@ function parseJsonArray(raw, option) {
1817
1943
  throw new ApiError("invalid_request", `--${option} must be a JSON array`);
1818
1944
  }
1819
1945
  }
1946
+ async function readJsonArrayArgument(raw, option) {
1947
+ if (!raw.startsWith("@")) return parseJsonArray(raw, option);
1948
+ try {
1949
+ return parseJsonArray((await readFile(normalizeHomePath(raw.slice(1)), "utf8")).trim(), option);
1950
+ } catch (error) {
1951
+ if (error instanceof ApiError) throw error;
1952
+ throw new ApiError("invalid_request", `--${option} @file \u65E0\u6CD5\u8BFB\u53D6\uFF1A${raw.slice(1, 120)}`);
1953
+ }
1954
+ }
1820
1955
  var MAX_CONTENT_FILE_BYTES = 64 * 1024 * 1024;
1821
1956
  async function readContentArgument(raw) {
1822
1957
  if (raw === "-") {
@@ -1953,8 +2088,8 @@ function registerGen(program) {
1953
2088
  const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
1954
2089
  if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u56FE\u50CF\u6A21\u578B\uFF1A${model}`);
1955
2090
  const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
1956
- const styleReferences = opts.styleReferences ? parseJsonArray(opts.styleReferences, "style-references") : void 0;
1957
- const moodboards = opts.moodboards ? parseJsonArray(opts.moodboards, "moodboards") : void 0;
2091
+ const styleReferences = opts.styleReferences ? await readJsonArrayArgument(opts.styleReferences, "style-references") : void 0;
2092
+ const moodboards = opts.moodboards ? await readJsonArrayArgument(opts.moodboards, "moodboards") : void 0;
1958
2093
  const referenceImages = await resolveMediaInputs(opts.image ?? []);
1959
2094
  const maskImage = (await resolveMediaInputs(opts.mask ? [opts.mask] : []))[0];
1960
2095
  validateImageGeneration(model, {
@@ -1981,14 +2116,17 @@ function registerGen(program) {
1981
2116
  throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
1982
2117
  }
1983
2118
  const body = { model, prompt: promptParts.join(" "), n };
2119
+ const constraint = getImageConstraint(model);
1984
2120
  if (opts.size) body.size = opts.size;
1985
2121
  if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
1986
- if (opts.resolution) body.resolution = opts.resolution.toLowerCase();
2122
+ if (opts.resolution) body[constraint?.resolutionParam ?? "resolution"] = opts.resolution.toLowerCase();
1987
2123
  if (opts.seed !== void 0) body.seed = opts.seed;
1988
2124
  if (opts.quality) body.quality = opts.quality;
1989
2125
  if (opts.background) body.background = opts.background;
1990
2126
  if (opts.negativePrompt) body.negative_prompt = opts.negativePrompt;
1991
- if (opts.creativity) body.creativity = opts.creativity.toLowerCase();
2127
+ if (opts.creativity) {
2128
+ body.creativity = constraint?.integerCreativity ? Number(opts.creativity) : opts.creativity.toLowerCase();
2129
+ }
1992
2130
  if (opts.promptExtend !== void 0) body.prompt_extend = opts.promptExtend;
1993
2131
  if (styleReferences) body.image_style_references = styleReferences;
1994
2132
  if (moodboards) body.moodboards = moodboards;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focalapi-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "让任意 AI Agent 直接调用 focalapi 创作模型的命令行工具",
5
5
  "type": "module",
6
6
  "bin": {