@koda-sl/baker-cli 0.149.0-dev.7b64ca6b5 → 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-ZBRVPPJP.js";
39
+ } from "./chunk-OO5BDH3J.js";
38
40
  import {
39
41
  csvOrJson,
40
42
  daysAgoIso,
@@ -2544,13 +2546,6 @@ var imageDocSchema = z8.object({
2544
2546
  width: z8.number().optional(),
2545
2547
  height: z8.number().optional(),
2546
2548
  aspectRatio: z8.number().optional(),
2547
- /** Any non-opaque pixel in the decoded image. */
2548
- hasAlpha: z8.boolean().optional(),
2549
- /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2550
- * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2551
- * before placing it — the library keeps assets forever, so a bad ingest is
2552
- * otherwise indistinguishable from a good one months later. */
2553
- solidity: z8.number().optional(),
2554
2549
  dominantColor: z8.string().optional(),
2555
2550
  imagePalette: z8.array(z8.string()).optional(),
2556
2551
  thumbhashDataUri: z8.string().optional(),
@@ -2633,13 +2628,6 @@ var imageSearchResultSchema = z8.object({
2633
2628
  width: z8.number().optional(),
2634
2629
  height: z8.number().optional(),
2635
2630
  aspectRatio: z8.number().optional(),
2636
- /** Any non-opaque pixel in the decoded image. */
2637
- hasAlpha: z8.boolean().optional(),
2638
- /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2639
- * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2640
- * before placing it — the library keeps assets forever, so a bad ingest is
2641
- * otherwise indistinguishable from a good one months later. */
2642
- solidity: z8.number().optional(),
2643
2631
  dominantColor: z8.string().optional(),
2644
2632
  imagePalette: z8.array(z8.string()).optional(),
2645
2633
  source: z8.string(),
@@ -2721,8 +2709,9 @@ var rgbTriple = z8.tuple([
2721
2709
  z8.number().int().min(0).max(255)
2722
2710
  ]);
2723
2711
  var imageGenerateModelSchema = z8.enum([
2712
+ "openai/gpt-image-2",
2713
+ // Legacy — see the registry entry; kept so pre-switch canvases still run.
2724
2714
  "openai/gpt-5.4-image-2",
2725
- "google/gemini-3.5-flash",
2726
2715
  "google/gemini-3.1-flash-image-preview",
2727
2716
  "google/gemini-3-pro-image-preview",
2728
2717
  "recraft/recraft-v4.1-pro-vector"
@@ -2763,13 +2752,10 @@ var imagesLogoRequestSchema = z8.object({
2763
2752
  descriptionContext: z8.string().optional()
2764
2753
  });
2765
2754
  var imagesLogoResponseSchema = providerHitsResponseSchema();
2766
- var hexColorSchema = z8.string().transform((value) => value.trim().replace(/^%23/i, "#")).refine((value) => /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value), {
2767
- message: "Expected a hex color like #D7FFA4"
2768
- }).transform((value) => value.startsWith("#") ? value : `#${value}`);
2769
2755
  var imagesIconRequestSchema = z8.object({
2770
2756
  name: z8.string().min(1),
2771
2757
  set: z8.string().optional(),
2772
- color: hexColorSchema.optional(),
2758
+ color: z8.string().optional(),
2773
2759
  width: z8.coerce.number().int().positive().optional(),
2774
2760
  autoIngest: z8.coerce.number().int().min(0).max(20).optional(),
2775
2761
  descriptionContext: z8.string().optional()
@@ -2819,19 +2805,6 @@ var imagesIngestResponseSchema = z8.object({
2819
2805
  contentHash: z8.string()
2820
2806
  });
2821
2807
 
2822
- // ../api/src/logoShape.ts
2823
- var PLATE_SOLIDITY_THRESHOLD = 0.6;
2824
- var WORDMARK_MIN_ASPECT = 1.8;
2825
- function classifyLogoShape({
2826
- aspectRatio: aspectRatio2,
2827
- hasAlpha,
2828
- solidity
2829
- }) {
2830
- if (solidity === void 0 || aspectRatio2 === void 0) return "unknown";
2831
- if (solidity >= PLATE_SOLIDITY_THRESHOLD || hasAlpha === false) return "plated-icon";
2832
- return aspectRatio2 >= WORDMARK_MIN_ASPECT ? "wordmark" : "symbol";
2833
- }
2834
-
2835
2808
  // ../api/src/tags.ts
2836
2809
  import { z as z9 } from "zod";
2837
2810
  var TAG_TYPES = [
@@ -5425,17 +5398,27 @@ var googleDraftStageRequestSchema = z13.object({
5425
5398
  chatId: z13.string(),
5426
5399
  op: googleDraftOpInputSchema
5427
5400
  });
5428
- var googleDraftStageResponseSchema = z13.object({
5429
- staged: z13.literal(true),
5430
- ref: z13.string(),
5431
- kind: googleDraftOpKindSchema,
5432
- mode: googleWriteModeSchema,
5433
- dependsOn: z13.array(z13.string()),
5434
- summary: z13.string(),
5435
- warnings: z13.array(z13.string()),
5436
- /** True when the op amended an already-staged op in place instead of appending a new one. */
5437
- amended: z13.boolean().optional()
5438
- });
5401
+ var googleDraftStageResponseSchema = z13.discriminatedUnion("staged", [
5402
+ z13.object({
5403
+ staged: z13.literal(true),
5404
+ ref: z13.string(),
5405
+ kind: googleDraftOpKindSchema,
5406
+ mode: googleWriteModeSchema,
5407
+ dependsOn: z13.array(z13.string()),
5408
+ summary: z13.string(),
5409
+ warnings: z13.array(z13.string()),
5410
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
5411
+ amended: z13.boolean().optional()
5412
+ }),
5413
+ z13.object({
5414
+ staged: z13.literal(false),
5415
+ noop: z13.literal(true),
5416
+ kind: googleDraftOpKindSchema,
5417
+ mode: googleWriteModeSchema,
5418
+ summary: z13.string(),
5419
+ reason: z13.string()
5420
+ })
5421
+ ]);
5439
5422
  var googleDraftAmendRequestSchema = z13.object({
5440
5423
  chatId: z13.string(),
5441
5424
  ref: z13.string(),
@@ -5462,7 +5445,8 @@ var googleDraftStageBatchResponseSchema = z13.object({
5462
5445
  summary: z13.string(),
5463
5446
  warnings: z13.array(z13.string())
5464
5447
  })
5465
- )
5448
+ ),
5449
+ skipped: z13.array(z13.object({ kind: googleDraftOpKindSchema, summary: z13.string(), reason: z13.string() })).optional()
5466
5450
  });
5467
5451
  var googleDraftOpViewSchema = z13.object({
5468
5452
  ref: z13.string(),
@@ -5721,7 +5705,7 @@ function keywordEntries(args) {
5721
5705
  const seen = /* @__PURE__ */ new Set();
5722
5706
  for (const item of raw) {
5723
5707
  const entry = parseKeywordEntry(item, defaultMatch);
5724
- const key = `${entry.text}\0${entry.matchType}`;
5708
+ const key = JSON.stringify([entry.text, entry.matchType]);
5725
5709
  if (!seen.has(key)) {
5726
5710
  seen.add(key);
5727
5711
  entries.push(entry);
@@ -5788,6 +5772,9 @@ function handleGoogleError(err) {
5788
5772
  });
5789
5773
  process.exit(1);
5790
5774
  }
5775
+ function noopHints(data) {
5776
+ return data.staged ? [] : [`NOT STAGED \u2014 ${data.reason}. Nothing to publish for this change; say so rather than reporting it as done.`];
5777
+ }
5791
5778
  async function stageGoogleOp(raw, hints) {
5792
5779
  const preflight = googleDraftOpInputSchema.safeParse(raw);
5793
5780
  if (!preflight.success) {
@@ -5805,7 +5792,8 @@ async function stageGoogleOp(raw, hints) {
5805
5792
  try {
5806
5793
  const chatId = requireChatId();
5807
5794
  const response = await apiPost("/api/ads/google/draft/stage", { chatId, op: preflight.data });
5808
- writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5795
+ const allHints = [...noopHints(response.data), ...hints ?? []];
5796
+ writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5809
5797
  } catch (err) {
5810
5798
  handleGoogleError(err);
5811
5799
  }
@@ -5841,7 +5829,14 @@ async function stageGoogleOps(rawOps, hints) {
5841
5829
  try {
5842
5830
  const chatId = requireChatId();
5843
5831
  const response = await apiPost("/api/ads/google/draft/stage-batch", { chatId, ops });
5844
- writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5832
+ const skipped = response.data.skipped ?? [];
5833
+ const allHints = [
5834
+ ...skipped.length > 0 ? [
5835
+ `${skipped.length} of ${ops.length} op(s) NOT STAGED \u2014 already in the requested state: ${skipped.map((s) => s.reason).join("; ")}`
5836
+ ] : [],
5837
+ ...hints ?? []
5838
+ ];
5839
+ writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5845
5840
  } catch (err) {
5846
5841
  handleGoogleError(err);
5847
5842
  }
@@ -16661,7 +16656,9 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16661
16656
  const imageProfile = imageProfileFor(ctx.imageModel);
16662
16657
  const genParams = {
16663
16658
  model: ctx.imageModel,
16664
- 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" } : {},
16665
16662
  // Per-model image defaults (gpt-image: quality=high — OpenRouter forwards it; we do
16666
16663
  // NOT send input_fidelity, which gpt-image-2 forces high automatically).
16667
16664
  ...imageProfile?.paramDefaults ?? {},
@@ -16676,7 +16673,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16676
16673
  ctx.imageModel
16677
16674
  )
16678
16675
  };
16679
- if (ctx.genAr) genParams.aspect_ratio = ctx.genAr;
16676
+ if (ctx.genAr) genParams.aspect_ratio = nearestSupportedAspectRatio("image_generate", ctx.imageModel, ctx.genAr);
16680
16677
  const genId = `s${ctx.sceneIndex}${tag}_${edge}`;
16681
16678
  nodes.push({
16682
16679
  id: genId,
@@ -20403,8 +20400,13 @@ function scaffoldStaticAd(input, elementsInput, opts) {
20403
20400
  inputs: { reference, target_blueprint: "$ref:prompt.asset" },
20404
20401
  params: {
20405
20402
  model: opts.genModel,
20406
- aspect_ratio: baseAspectRatio(blueprint, opts),
20407
- 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 ?? {},
20408
20410
  prompt
20409
20411
  }
20410
20412
  });
@@ -20771,7 +20773,7 @@ function resolveModels(args) {
20771
20773
  describeModel: pick("describe-model", "image_describe", "~google/gemini-pro-latest"),
20772
20774
  selectModel: pick("select-model", "text_generate", "~google/gemini-flash-latest"),
20773
20775
  layoutModel: pick("layout-model", "text_generate", "~google/gemini-flash-latest"),
20774
- genModel: pick("gen-model", "image_generate", "openai/gpt-5.4-image-2")
20776
+ genModel: pick("gen-model", "image_generate", "openai/gpt-image-2")
20775
20777
  };
20776
20778
  }
20777
20779
  var PLATFORM_PLACEMENTS = {
@@ -21413,7 +21415,7 @@ function resolveModels2(args) {
21413
21415
  // Default to the strongest image model (matches the static-ad scaffold); the
21414
21416
  // frame generators need the most faithful text/identity reproduction. Override
21415
21417
  // with --image-model for a cheaper/faster pass.
21416
- imageModel: pick("image-model", "image_generate", "openai/gpt-5.4-image-2")
21418
+ imageModel: pick("image-model", "image_generate", "openai/gpt-image-2")
21417
21419
  };
21418
21420
  }
21419
21421
  function hasPhotorealCast(elements) {
@@ -24123,7 +24125,7 @@ async function resolveReferences(spec) {
24123
24125
  var generateCommand = defineCommand114({
24124
24126
  meta: {
24125
24127
  name: "generate",
24126
- 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]]'"
24127
24129
  },
24128
24130
  args: {
24129
24131
  prompt: { type: "positional", description: "What to generate", required: false },
@@ -24296,6 +24298,34 @@ var gifCommand = defineCommand116({
24296
24298
 
24297
24299
  // src/commands/images/google.ts
24298
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
+ };
24299
24329
  registerSchema({
24300
24330
  command: "images.google",
24301
24331
  description: "Google Images search via the official Custom Search JSON API. Unverified source \u2014 inspect before placing.",
@@ -24366,10 +24396,18 @@ var googleCommand2 = defineCommand117({
24366
24396
  if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
24367
24397
  if (args.context) body.descriptionContext = args.context;
24368
24398
  const data = await apiPost("/api/images/google", body);
24369
- 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 } : {} });
24370
24405
  } catch (err) {
24371
24406
  if (err instanceof ApiError) {
24372
- 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
+ });
24373
24411
  process.exit(1);
24374
24412
  }
24375
24413
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -24500,31 +24538,6 @@ var ingestCommand = defineCommand119({
24500
24538
 
24501
24539
  // src/commands/images/library.ts
24502
24540
  import { defineCommand as defineCommand120 } from "citty";
24503
-
24504
- // src/commands/images/logoHints.ts
24505
- var LOGO_QUERY_RE = /\blogos?\b|\bwordmark\b|\bbrand mark\b/i;
24506
- function withLogoShape(row) {
24507
- return {
24508
- ...row,
24509
- logoShape: classifyLogoShape({
24510
- aspectRatio: row.aspectRatio,
24511
- hasAlpha: row.hasAlpha,
24512
- solidity: row.solidity
24513
- })
24514
- };
24515
- }
24516
- function buildLogoLibraryHints(query, rows) {
24517
- if (!LOGO_QUERY_RE.test(query)) return [];
24518
- const plates = rows.filter((row) => row.logoShape === "plated-icon");
24519
- if (plates.length === 0) return [];
24520
- const names = plates.map((row) => row.name).filter((name) => Boolean(name)).slice(0, 4);
24521
- const subject = names.length > 0 ? names.join(", ") : `${plates.length} result(s)`;
24522
- return [
24523
- `PLATED ICON: ${subject} \u2014 solid tile with the mark knocked out, not a wordmark. In a logo strip it renders as a filled box and verify blocks it (logo-plate). Re-source with 'baker images logo <domain> --variant logo', or strip the plate with 'baker images normalize <file> --remove-bg --shrink-to-content'. Only use a plate in slots under ~32px.`
24524
- ];
24525
- }
24526
-
24527
- // src/commands/images/library.ts
24528
24541
  registerSchema({
24529
24542
  command: "images.library",
24530
24543
  description: "Search the company image library. Returns only ready images.",
@@ -24589,10 +24602,8 @@ var libraryCommand = defineCommand120({
24589
24602
  if (minScore !== void 0) {
24590
24603
  data = data.filter((r) => typeof r.score === "number" && r.score >= minScore);
24591
24604
  }
24592
- const shaped = data.map(withLogoShape);
24593
- const hints = buildLogoLibraryHints(query, shaped);
24594
24605
  writeOutput(
24595
- { ok: true, data: shaped, ...hints.length > 0 ? { hints } : {} },
24606
+ { ok: true, data },
24596
24607
  args.output || "json",
24597
24608
  args.fields ? args.fields.split(",") : void 0,
24598
24609
  args.full
@@ -24610,16 +24621,11 @@ var libraryCommand = defineCommand120({
24610
24621
 
24611
24622
  // src/commands/images/logo.ts
24612
24623
  import { defineCommand as defineCommand121 } from "citty";
24613
- var MAX_DOMAINS = 20;
24614
24624
  registerSchema({
24615
24625
  command: "images.logo",
24616
24626
  description: "Brand logo lookup via Brandfetch CDN (fallback/404). Auto-ingests by default.",
24617
24627
  args: {
24618
- domain: {
24619
- type: "string",
24620
- description: "Brand domain, or a comma-separated list for a whole strip (e.g. stripe.com,intercom.com)",
24621
- required: true
24622
- },
24628
+ domain: { type: "string", description: "Brand domain (e.g. stripe.com)", required: true },
24623
24629
  variant: { type: "string", description: "icon | logo | symbol", required: false },
24624
24630
  "auto-ingest": {
24625
24631
  type: "number",
@@ -24642,10 +24648,10 @@ registerSchema({
24642
24648
  var logoCommand = defineCommand121({
24643
24649
  meta: {
24644
24650
  name: "logo",
24645
- description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nSource a whole strip in one call by passing a comma-separated list \u2014 a text-rendered brand name is never the cheaper option.\n\nExamples:\n baker images logo stripe.com --variant logo\n baker images logo stripe.com,intercom.com,notion.so --variant logo"
24651
+ description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nExample: baker images logo stripe.com --variant logo"
24646
24652
  },
24647
24653
  args: {
24648
- domain: { type: "positional", description: "Brand domain, or a comma-separated list", required: false },
24654
+ domain: { type: "positional", description: "Brand domain", required: false },
24649
24655
  variant: { type: "string", description: "icon|logo|symbol", required: false },
24650
24656
  "auto-ingest": { type: "string", description: "Ingest top N (0-20, default 1)", required: false },
24651
24657
  "no-auto-ingest": { type: "boolean", description: "Skip auto-ingest", required: false },
@@ -24653,50 +24659,18 @@ var logoCommand = defineCommand121({
24653
24659
  },
24654
24660
  run: async ({ args }) => {
24655
24661
  try {
24656
- const raw = args.domain;
24657
- const domains = [
24658
- ...new Set(
24659
- (raw ?? "").split(",").map((d) => d.trim()).filter(Boolean)
24660
- )
24661
- ];
24662
- if (domains.length === 0) {
24662
+ const domain = args.domain;
24663
+ if (!domain) {
24663
24664
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Domain is required" } });
24664
24665
  process.exit(1);
24665
24666
  }
24666
- if (domains.length > MAX_DOMAINS) {
24667
- writeJson({
24668
- ok: false,
24669
- error: {
24670
- code: "VALIDATION_ERROR",
24671
- message: `Too many domains (${domains.length}); pass at most ${MAX_DOMAINS} per call`
24672
- }
24673
- });
24674
- process.exit(1);
24675
- }
24676
- const base = {};
24677
- if (args.variant) base.variant = args.variant;
24678
- if (args["auto-ingest"] !== void 0) base.autoIngest = Number(args["auto-ingest"]);
24679
- else if (args["no-auto-ingest"]) base.autoIngest = 0;
24680
- if (args.context) base.descriptionContext = args.context;
24681
- const fetchOne = (domain) => apiPost("/api/images/logo", { ...base, domain });
24682
- if (domains.length === 1) {
24683
- writeJson({ ok: true, data: await fetchOne(domains[0]) });
24684
- return;
24685
- }
24686
- const settled = await Promise.all(
24687
- domains.map(async (domain) => {
24688
- try {
24689
- return { domain, ...await fetchOne(domain) };
24690
- } catch (err) {
24691
- return { domain, hits: [], error: err instanceof ApiError ? err.message : "Lookup failed" };
24692
- }
24693
- })
24694
- );
24695
- const empty = settled.filter((r) => r.hits.length === 0).map((r) => r.domain);
24696
- const hints = empty.length > 0 ? [
24697
- `NO LOGO: ${empty.join(", ")} \u2014 Brandfetch doesn't know ${empty.length === 1 ? "this brand" : "these brands"}. Fall back per domain: 'baker images extract <domain> --auto-ingest 5', then 'baker images icon <brand> --set simple-icons'. Still nothing \u2192 'baker actions create' to acquire the file. Do not render the name as text.`
24698
- ] : [];
24699
- writeJson({ ok: true, data: { results: settled }, ...hints.length > 0 ? { hints } : {} });
24667
+ const body = { domain };
24668
+ if (args.variant) body.variant = args.variant;
24669
+ if (args["auto-ingest"] !== void 0) body.autoIngest = Number(args["auto-ingest"]);
24670
+ else if (args["no-auto-ingest"]) body.autoIngest = 0;
24671
+ if (args.context) body.descriptionContext = args.context;
24672
+ const data = await apiPost("/api/images/logo", body);
24673
+ writeJson({ ok: true, data });
24700
24674
  } catch (err) {
24701
24675
  if (err instanceof ApiError) {
24702
24676
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
@@ -24740,7 +24714,6 @@ function getDominantEdgeColor(data, width, height) {
24740
24714
  const colorCount = {};
24741
24715
  function accumulateColor(i, j) {
24742
24716
  const idx = (i * width + j) * 4;
24743
- if ((data[idx + 3] ?? 0) < 10) return;
24744
24717
  const colorKey = `${data[idx]},${data[idx + 1]},${data[idx + 2]}`;
24745
24718
  colorCount[colorKey] = (colorCount[colorKey] ?? 0) + 1;
24746
24719
  }
@@ -24753,7 +24726,7 @@ function getDominantEdgeColor(data, width, height) {
24753
24726
  accumulateColor(i, width - 1);
24754
24727
  }
24755
24728
  let maxCount = 0;
24756
- let dominantColor = null;
24729
+ let dominantColor = { r: 0, g: 0, b: 0 };
24757
24730
  for (const key in colorCount) {
24758
24731
  const count = colorCount[key];
24759
24732
  if (count > maxCount) {
@@ -24764,27 +24737,6 @@ function getDominantEdgeColor(data, width, height) {
24764
24737
  }
24765
24738
  return dominantColor;
24766
24739
  }
24767
- function opaqueSolidity(data, width, height) {
24768
- let minX = width;
24769
- let minY = height;
24770
- let maxX = -1;
24771
- let maxY = -1;
24772
- let opaque = 0;
24773
- for (let y = 0; y < height; y++) {
24774
- for (let x = 0; x < width; x++) {
24775
- if ((data[(y * width + x) * 4 + 3] ?? 0) <= 200) continue;
24776
- opaque++;
24777
- if (x < minX) minX = x;
24778
- if (x > maxX) maxX = x;
24779
- if (y < minY) minY = y;
24780
- if (y > maxY) maxY = y;
24781
- }
24782
- }
24783
- if (maxX < 0) return 0;
24784
- const boxArea = (maxX - minX + 1) * (maxY - minY + 1);
24785
- return boxArea === 0 ? 0 : opaque / boxArea;
24786
- }
24787
- var PLATE_SOLIDITY_THRESHOLD2 = 0.6;
24788
24740
  function hasTransparency(data, threshold = 0.02) {
24789
24741
  let transparentPixels = 0;
24790
24742
  let totalPixels = 0;
@@ -24849,9 +24801,6 @@ function removeBackground(data, width, height, colorRangeThreshold = COLOR_RANGE
24849
24801
  return data;
24850
24802
  }
24851
24803
  const dominantEdgeColor = getDominantEdgeColor(data, width, height);
24852
- if (!dominantEdgeColor) {
24853
- return data;
24854
- }
24855
24804
  const isGradient = hasGradientColors(data);
24856
24805
  const result = Buffer.from(data);
24857
24806
  if (isGradient) {
@@ -25129,8 +25078,8 @@ async function processInternal(inputBuffer, isSVG, options) {
25129
25078
  const metadata = await sharp3(inputBuffer).metadata();
25130
25079
  let alreadyTransparent = false;
25131
25080
  if (metadata.hasAlpha) {
25132
- const { data: alphaData, info: info2 } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25133
- alreadyTransparent = opaqueSolidity(alphaData, info2.width, info2.height) < PLATE_SOLIDITY_THRESHOLD2;
25081
+ const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25082
+ alreadyTransparent = hasTransparency(alphaData, 0.05);
25134
25083
  }
25135
25084
  let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
25136
25085
  if (options.color) {
@@ -25816,6 +25765,10 @@ var stickerCommand = defineCommand126({
25816
25765
 
25817
25766
  // src/commands/images/stock.ts
25818
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
+ };
25819
25772
  registerSchema({
25820
25773
  command: "images.stock",
25821
25774
  description: "Stock photo, vector illustration, icon-set, and PSD search via Magnific (Freepik's developer API).",
@@ -25873,6 +25826,21 @@ registerSchema({
25873
25826
  }
25874
25827
  }
25875
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
+ }
25876
25844
  var stockCommand = defineCommand127({
25877
25845
  meta: {
25878
25846
  name: "stock",
@@ -25903,23 +25871,23 @@ var stockCommand = defineCommand127({
25903
25871
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Query is required" } });
25904
25872
  process.exit(1);
25905
25873
  }
25906
- const body = { query };
25907
- if (args.type) body.contentType = args.type;
25908
- if (args.orientation) body.orientation = args.orientation;
25909
- if (args.license) body.license = args.license;
25910
- if (args.color) body.color = args.color;
25911
- if (args.ai) body.aiGenerated = args.ai;
25912
- if (args.people) body.people = args.people;
25913
- if (args.order) body.order = args.order;
25914
- if (args.limit) body.limit = Number(args.limit);
25915
- if (args.page) body.page = Number(args.page);
25916
- if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
25917
- if (args.context) body.descriptionContext = args.context;
25918
- const data = await apiPost("/api/images/stock", body);
25919
- 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 } : {} });
25920
25881
  } catch (err) {
25921
25882
  if (err instanceof ApiError) {
25922
- 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
+ });
25923
25891
  process.exit(1);
25924
25892
  }
25925
25893
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });