@koda-sl/baker-cli 0.148.0 → 0.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  imageProfileFor,
25
25
  isPersistedAssetRef,
26
26
  looksLikeHttpUrl,
27
+ nearestSupportedAspectRatio,
27
28
  parseRefExpr,
28
29
  platformFormats,
29
30
  requireCredentialsFromEnv,
@@ -31,10 +32,11 @@ import {
31
32
  sha256Hex,
32
33
  spineInputFlags,
33
34
  spineInputOps,
35
+ supportsParam,
34
36
  toModelSafeImage,
35
37
  ulid,
36
38
  validateCanvasDeep
37
- } from "./chunk-2F5SCTNS.js";
39
+ } from "./chunk-OO5BDH3J.js";
38
40
  import {
39
41
  csvOrJson,
40
42
  daysAgoIso,
@@ -2707,8 +2709,9 @@ var rgbTriple = z8.tuple([
2707
2709
  z8.number().int().min(0).max(255)
2708
2710
  ]);
2709
2711
  var imageGenerateModelSchema = z8.enum([
2712
+ "openai/gpt-image-2",
2713
+ // Legacy — see the registry entry; kept so pre-switch canvases still run.
2710
2714
  "openai/gpt-5.4-image-2",
2711
- "google/gemini-3.5-flash",
2712
2715
  "google/gemini-3.1-flash-image-preview",
2713
2716
  "google/gemini-3-pro-image-preview",
2714
2717
  "recraft/recraft-v4.1-pro-vector"
@@ -16653,7 +16656,9 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16653
16656
  const imageProfile = imageProfileFor(ctx.imageModel);
16654
16657
  const genParams = {
16655
16658
  model: ctx.imageModel,
16656
- image_size: "2K",
16659
+ // gpt-image-2 derives pixel dimensions from the ratio and has no size knob,
16660
+ // so asking for 2K there is an `unknown_param` at validate.
16661
+ ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: "2K" } : {},
16657
16662
  // Per-model image defaults (gpt-image: quality=high — OpenRouter forwards it; we do
16658
16663
  // NOT send input_fidelity, which gpt-image-2 forces high automatically).
16659
16664
  ...imageProfile?.paramDefaults ?? {},
@@ -16668,7 +16673,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16668
16673
  ctx.imageModel
16669
16674
  )
16670
16675
  };
16671
- if (ctx.genAr) genParams.aspect_ratio = ctx.genAr;
16676
+ if (ctx.genAr) genParams.aspect_ratio = nearestSupportedAspectRatio("image_generate", ctx.imageModel, ctx.genAr);
16672
16677
  const genId = `s${ctx.sceneIndex}${tag}_${edge}`;
16673
16678
  nodes.push({
16674
16679
  id: genId,
@@ -20395,8 +20400,13 @@ function scaffoldStaticAd(input, elementsInput, opts) {
20395
20400
  inputs: { reference, target_blueprint: "$ref:prompt.asset" },
20396
20401
  params: {
20397
20402
  model: opts.genModel,
20398
- aspect_ratio: baseAspectRatio(blueprint, opts),
20399
- image_size: "2K",
20403
+ // The hero renders at the closest ratio its model actually accepts; the
20404
+ // placement fan-out below adapts it to the exact platform formats, which
20405
+ // is also how a 4:5 Meta feed ad gets made on a model that has no 4:5.
20406
+ aspect_ratio: nearestSupportedAspectRatio("image_generate", opts.genModel, baseAspectRatio(blueprint, opts)),
20407
+ ...supportsParam("image_generate", opts.genModel, "image_size") ? { image_size: "2K" } : {},
20408
+ // Per-model image defaults (gpt-image: quality=high).
20409
+ ...imageProfileFor(opts.genModel)?.paramDefaults ?? {},
20400
20410
  prompt
20401
20411
  }
20402
20412
  });
@@ -20763,7 +20773,7 @@ function resolveModels(args) {
20763
20773
  describeModel: pick("describe-model", "image_describe", "~google/gemini-pro-latest"),
20764
20774
  selectModel: pick("select-model", "text_generate", "~google/gemini-flash-latest"),
20765
20775
  layoutModel: pick("layout-model", "text_generate", "~google/gemini-flash-latest"),
20766
- genModel: pick("gen-model", "image_generate", "openai/gpt-5.4-image-2")
20776
+ genModel: pick("gen-model", "image_generate", "openai/gpt-image-2")
20767
20777
  };
20768
20778
  }
20769
20779
  var PLATFORM_PLACEMENTS = {
@@ -21405,7 +21415,7 @@ function resolveModels2(args) {
21405
21415
  // Default to the strongest image model (matches the static-ad scaffold); the
21406
21416
  // frame generators need the most faithful text/identity reproduction. Override
21407
21417
  // with --image-model for a cheaper/faster pass.
21408
- imageModel: pick("image-model", "image_generate", "openai/gpt-5.4-image-2")
21418
+ imageModel: pick("image-model", "image_generate", "openai/gpt-image-2")
21409
21419
  };
21410
21420
  }
21411
21421
  function hasPhotorealCast(elements) {
@@ -24115,7 +24125,7 @@ async function resolveReferences(spec) {
24115
24125
  var generateCommand = defineCommand114({
24116
24126
  meta: {
24117
24127
  name: "generate",
24118
- description: "Generate an image with AI and store it in the library (cost-tracked per request via OpenRouter usage). Models mirror the canvas: google/gemini-3.1-flash-image-preview (Nano Banana flash \u2014 default, fast, extreme aspect ratios) & google/gemini-3.5-flash (fast), google/gemini-3-pro-image-preview (Nano Banana Pro \u2014 highest fidelity), openai/gpt-5.4-image-2 (photoreal, cleanest in-image text, best for ad/landing reproduction), recraft/recraft-v4.1-pro-vector (vector/SVG-style with palette control). The result is auto-ingested (describe + embed), so the next `baker images library` query finds it. Pass --reference with image URLs and/or local file paths (Pinterest, stock, brand assets, sandbox files) to ground generation in reality.\n\nExamples:\n baker images generate 'a friendly golden retriever sitting in a bright modern living room' --aspect-ratio 16:9\n baker images generate 'hero shot of a matte black water bottle on marble' --model openai/gpt-5.4-image-2 --image-size 2K\n baker images generate 'lifestyle photo matching this mood' --reference 'https://\u2026/ref1.jpg,https://\u2026/ref2.jpg'\n baker images generate 'put this product on a marble countertop, soft daylight' --reference './src/brand/logos/product.png,./refs/kitchen-mood.jpg'\n baker images generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
24128
+ description: "Generate an image with AI and store it in the library (cost-tracked per request via OpenRouter usage). Models mirror the canvas: google/gemini-3.1-flash-image-preview (Nano Banana flash \u2014 default, fast, extreme aspect ratios), google/gemini-3-pro-image-preview (Nano Banana Pro \u2014 highest fidelity), openai/gpt-image-2 (photoreal, cleanest in-image text, best for ad/landing reproduction \u2014 no --image-size, and no 4:5 / 5:4), recraft/recraft-v4.1-pro-vector (vector/SVG-style with palette control). The result is auto-ingested (describe + embed), so the next `baker images library` query finds it. Pass --reference with image URLs and/or local file paths (Pinterest, stock, brand assets, sandbox files) to ground generation in reality.\n\nExamples:\n baker images generate 'a friendly golden retriever sitting in a bright modern living room' --aspect-ratio 16:9\n baker images generate 'hero shot of a matte black water bottle on marble' --model openai/gpt-image-2 --aspect-ratio 3:2\n baker images generate 'lifestyle photo matching this mood' --reference 'https://\u2026/ref1.jpg,https://\u2026/ref2.jpg'\n baker images generate 'put this product on a marble countertop, soft daylight' --reference './src/brand/logos/product.png,./refs/kitchen-mood.jpg'\n baker images generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
24119
24129
  },
24120
24130
  args: {
24121
24131
  prompt: { type: "positional", description: "What to generate", required: false },
@@ -24288,6 +24298,34 @@ var gifCommand = defineCommand116({
24288
24298
 
24289
24299
  // src/commands/images/google.ts
24290
24300
  import { defineCommand as defineCommand117 } from "citty";
24301
+
24302
+ // src/commands/images/searchHints.ts
24303
+ var FALLBACK = {
24304
+ stock: "Still empty \u2192 `baker images find <query> --sources library,pinterest,google` (one call across the providers stock does not cover \u2014 `--sources` is required, `find` alone searches the library only) or `baker images generate` to make the asset. Do not re-run this search with reshuffled flags.",
24305
+ google: "Still empty \u2192 `baker images generate` to make the asset. Google is the last-resort provider; there is nothing below it to retry."
24306
+ };
24307
+ function emptyResultHints({ provider, hitCount, activeFilters }) {
24308
+ if (hitCount > 0) {
24309
+ return [];
24310
+ }
24311
+ const hints = [];
24312
+ if (activeFilters.length > 0) {
24313
+ hints.push(
24314
+ `No hits. Drop the filters before touching the query \u2014 ${activeFilters.join(", ")} narrowed this search and filter combinations are the usual cause of an empty result.`
24315
+ );
24316
+ }
24317
+ hints.push(FALLBACK[provider]);
24318
+ return hints;
24319
+ }
24320
+ function activeFilterFlags(args, candidates) {
24321
+ return candidates.filter((flag) => args[flag] !== void 0 && args[flag] !== "").map((flag) => `--${flag}`);
24322
+ }
24323
+
24324
+ // src/commands/images/google.ts
24325
+ var GOOGLE_ERROR_FIX = {
24326
+ action: "use_different_resource",
24327
+ explanation: "Generate the asset instead of retrying Google. Google is the last-resort image provider. Run `baker images generate` to make the asset. Never sleep-and-retry this command \u2014 the CLI already backs off on rate limits. If no image can be sourced, continue the rest of the task with a placeholder rather than aborting it."
24328
+ };
24291
24329
  registerSchema({
24292
24330
  command: "images.google",
24293
24331
  description: "Google Images search via the official Custom Search JSON API. Unverified source \u2014 inspect before placing.",
@@ -24358,10 +24396,18 @@ var googleCommand2 = defineCommand117({
24358
24396
  if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
24359
24397
  if (args.context) body.descriptionContext = args.context;
24360
24398
  const data = await apiPost("/api/images/google", body);
24361
- writeJson({ ok: true, data });
24399
+ const hints = emptyResultHints({
24400
+ provider: "google",
24401
+ hitCount: data.hits.length,
24402
+ activeFilters: activeFilterFlags(args, ["type", "size", "color", "safe"])
24403
+ });
24404
+ writeJson({ ok: true, data, ...hints.length ? { hints } : {} });
24362
24405
  } catch (err) {
24363
24406
  if (err instanceof ApiError) {
24364
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
24407
+ writeJson({
24408
+ ok: false,
24409
+ error: { code: err.code, message: err.message, fix: GOOGLE_ERROR_FIX }
24410
+ });
24365
24411
  process.exit(1);
24366
24412
  }
24367
24413
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -25719,6 +25765,10 @@ var stickerCommand = defineCommand126({
25719
25765
 
25720
25766
  // src/commands/images/stock.ts
25721
25767
  import { defineCommand as defineCommand127 } from "citty";
25768
+ var STOCK_ERROR_FIX = {
25769
+ action: "use_different_resource",
25770
+ explanation: "Switch provider instead of retrying stock search. Stock search is one of several image sources. Run `baker images find <query> --sources library,pinterest,google` (`--sources` is required \u2014 `find` alone searches the library only) or `baker images generate` to make the asset. Never sleep-and-retry this command \u2014 the CLI already backs off on rate limits. If no image can be sourced, continue the rest of the task with a placeholder rather than aborting it."
25771
+ };
25722
25772
  registerSchema({
25723
25773
  command: "images.stock",
25724
25774
  description: "Stock photo, vector illustration, icon-set, and PSD search via Magnific (Freepik's developer API).",
@@ -25776,6 +25826,21 @@ registerSchema({
25776
25826
  }
25777
25827
  }
25778
25828
  });
25829
+ function buildStockRequest(query, args) {
25830
+ const body = { query };
25831
+ if (args.type) body.contentType = args.type;
25832
+ if (args.orientation) body.orientation = args.orientation;
25833
+ if (args.license) body.license = args.license;
25834
+ if (args.color) body.color = args.color;
25835
+ if (args.ai) body.aiGenerated = args.ai;
25836
+ if (args.people) body.people = args.people;
25837
+ if (args.order) body.order = args.order;
25838
+ if (args.limit) body.limit = Number(args.limit);
25839
+ if (args.page) body.page = Number(args.page);
25840
+ if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
25841
+ if (args.context) body.descriptionContext = args.context;
25842
+ return body;
25843
+ }
25779
25844
  var stockCommand = defineCommand127({
25780
25845
  meta: {
25781
25846
  name: "stock",
@@ -25806,23 +25871,23 @@ var stockCommand = defineCommand127({
25806
25871
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Query is required" } });
25807
25872
  process.exit(1);
25808
25873
  }
25809
- const body = { query };
25810
- if (args.type) body.contentType = args.type;
25811
- if (args.orientation) body.orientation = args.orientation;
25812
- if (args.license) body.license = args.license;
25813
- if (args.color) body.color = args.color;
25814
- if (args.ai) body.aiGenerated = args.ai;
25815
- if (args.people) body.people = args.people;
25816
- if (args.order) body.order = args.order;
25817
- if (args.limit) body.limit = Number(args.limit);
25818
- if (args.page) body.page = Number(args.page);
25819
- if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
25820
- if (args.context) body.descriptionContext = args.context;
25821
- const data = await apiPost("/api/images/stock", body);
25822
- writeJson({ ok: true, data });
25874
+ const data = await apiPost("/api/images/stock", buildStockRequest(query, args));
25875
+ const hints = emptyResultHints({
25876
+ provider: "stock",
25877
+ hitCount: data.hits.length,
25878
+ activeFilters: activeFilterFlags(args, ["type", "orientation", "license", "color", "ai", "people"])
25879
+ });
25880
+ writeJson({ ok: true, data, ...hints.length ? { hints } : {} });
25823
25881
  } catch (err) {
25824
25882
  if (err instanceof ApiError) {
25825
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
25883
+ writeJson({
25884
+ ok: false,
25885
+ error: {
25886
+ code: err.code,
25887
+ message: err.message,
25888
+ fix: STOCK_ERROR_FIX
25889
+ }
25890
+ });
25826
25891
  process.exit(1);
25827
25892
  }
25828
25893
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });