@koda-sl/baker-cli 0.254.0 → 0.255.0-dev.8030cfcf9

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.
@@ -2,10 +2,10 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-5JHSX3ZB.js";
5
+ } from "./chunk-6F52WYB7.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-JKSMLAQN.js";
8
+ } from "./chunk-GU7IWEBC.js";
9
9
  import {
10
10
  getEnv
11
11
  } from "./chunk-DZUVUGEP.js";
@@ -108,4 +108,4 @@ export {
108
108
  csvOrJson,
109
109
  resolveEffectiveStatus
110
110
  };
111
- //# sourceMappingURL=chunk-6MAVG6LO.js.map
111
+ //# sourceMappingURL=chunk-KSOGK7SJ.js.map
package/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-SJPRTSGY.js";
61
+ } from "./chunk-HBDGUVUH.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -67,7 +67,7 @@ import {
67
67
  resolveAccountIdArg,
68
68
  resolveEffectiveStatus,
69
69
  todayIso
70
- } from "./chunk-6MAVG6LO.js";
70
+ } from "./chunk-KSOGK7SJ.js";
71
71
  import {
72
72
  buildQueryCacheKey,
73
73
  cacheGet,
@@ -83,13 +83,14 @@ import {
83
83
  writeAdsJson,
84
84
  writeAdsOutput,
85
85
  writeJsonEnvelope
86
- } from "./chunk-5JHSX3ZB.js";
86
+ } from "./chunk-6F52WYB7.js";
87
87
  import {
88
88
  ApiError,
89
89
  apiGet,
90
+ apiGetWithHeaders,
90
91
  apiPost,
91
92
  validateConvexId
92
- } from "./chunk-JKSMLAQN.js";
93
+ } from "./chunk-GU7IWEBC.js";
93
94
  import {
94
95
  installStreamTaps,
95
96
  logInvocation
@@ -2112,7 +2113,14 @@ var GOOGLE_ADS_LIMITS = {
2112
2113
  // Google's published headers (`structured-snippet-headers.ts`), the longest
2113
2114
  // of which is 29 characters. Only the values are capped.
2114
2115
  structuredSnippetValuesMin: 3,
2115
- structuredSnippetValuesMax: 10
2116
+ structuredSnippetValuesMax: 10,
2117
+ /**
2118
+ * Per VALUE, not per asset. Google rejects a 26-character value with a bare "Too long." at
2119
+ * `structured_snippet_asset.values[i]` — one red row inside an otherwise clean publish, plus
2120
+ * every link that depends on the asset. Confirmed live on v23: "Bases de datos vectoriales"
2121
+ * (26) rejected, its four neighbours accepted.
2122
+ */
2123
+ structuredSnippetValueMax: 25
2116
2124
  },
2117
2125
  conversionAction: {
2118
2126
  nameMax: 255
@@ -2272,9 +2280,24 @@ var ASSET_FIELD_TYPES = [
2272
2280
  "HEADLINE",
2273
2281
  "DESCRIPTION",
2274
2282
  "LOGO",
2283
+ /**
2284
+ * Kept because a draft staged before {@link IMAGE_ASSET_LINK_MESSAGE} shipped may still carry one,
2285
+ * and because it is the field type an asset-group image occupies. It is refused on an
2286
+ * `assetLink.attach` — see that message for what Google actually does with it.
2287
+ */
2275
2288
  "MARKETING_IMAGE",
2276
2289
  "BUSINESS_NAME"
2277
2290
  ];
2291
+ var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
2292
+ "MARKETING_IMAGE",
2293
+ "SQUARE_MARKETING_IMAGE",
2294
+ "PORTRAIT_MARKETING_IMAGE",
2295
+ "AD_IMAGE"
2296
+ ]);
2297
+ var IMAGE_ASSET_LINK_MESSAGE = "Google does not accept an image through an asset link. MARKETING_IMAGE is a Performance Max asset-group slot (use `asset-groups attach`), and AD_IMAGE \u2014 the field type Google names for a Search image extension \u2014 comes back UNSUPPORTED_FIELD_TYPE at campaign and ad group level alike. Image extensions are not reachable through the API: tell the user to add them in the Google Ads interface, and carry on with the rest of the job.";
2298
+ function isImageAssetLinkFieldType(fieldType) {
2299
+ return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
2300
+ }
2278
2301
  var ASSET_GROUP_ASSET_FIELD_TYPES = [
2279
2302
  "MARKETING_IMAGE",
2280
2303
  "SQUARE_MARKETING_IMAGE",
@@ -3122,7 +3145,7 @@ var structuredSnippetHeaderSchema = z4.string().min(1).transform((header, ctx) =
3122
3145
  var structuredSnippetAssetSchema = z4.object({
3123
3146
  type: z4.literal("structuredSnippet"),
3124
3147
  header: structuredSnippetHeaderSchema,
3125
- values: z4.array(z4.string().min(1)).min(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMin).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMax)
3148
+ values: z4.array(z4.string().min(1).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValueMax)).min(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMin).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMax)
3126
3149
  });
3127
3150
  var callToActionAssetSchema = z4.object({ type: z4.literal("callToAction"), callToAction: z4.string().min(1) });
3128
3151
  var assetCreateSchema = z4.discriminatedUnion("type", [
@@ -3142,7 +3165,7 @@ var assetUpdateSchema = z4.object({
3142
3165
  finalUrls: z4.array(httpsUrlSchema2).min(1).optional(),
3143
3166
  calloutText: z4.string().min(1).max(GOOGLE_ADS_LIMITS.asset.calloutTextMax).optional(),
3144
3167
  header: structuredSnippetHeaderSchema.optional(),
3145
- values: z4.array(z4.string().min(1)).min(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMin).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMax).optional(),
3168
+ values: z4.array(z4.string().min(1).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValueMax)).min(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMin).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMax).optional(),
3146
3169
  callToAction: z4.string().min(1).optional(),
3147
3170
  text: z4.string().min(1).optional()
3148
3171
  }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
@@ -3150,7 +3173,11 @@ var assetLinkAttachSchema = z4.object({
3150
3173
  level: z4.enum(["campaign", "adGroup", "customer"]),
3151
3174
  parent: refSchema.optional(),
3152
3175
  asset: refSchema,
3153
- fieldType: z4.enum(ASSET_FIELD_TYPES)
3176
+ // A plain string, not `z.enum(ASSET_FIELD_TYPES)`, so the image refusal below can answer with
3177
+ // the reason instead of an enum listing. An agent reaching for AD_IMAGE is following Google's
3178
+ // own documentation; "Invalid option: expected one of …" sends it to guess another field type,
3179
+ // which is the failure this whole refusal exists to end.
3180
+ fieldType: z4.string().min(1)
3154
3181
  }).superRefine((value, ctx) => {
3155
3182
  if (value.level !== "customer" && !value.parent) {
3156
3183
  ctx.addIssue({
@@ -3159,6 +3186,15 @@ var assetLinkAttachSchema = z4.object({
3159
3186
  message: `parent is required for a ${value.level}-level asset link (--parent-ref)`
3160
3187
  });
3161
3188
  }
3189
+ if (isImageAssetLinkFieldType(value.fieldType)) {
3190
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
3191
+ } else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
3192
+ ctx.addIssue({
3193
+ code: z4.ZodIssueCode.custom,
3194
+ path: ["fieldType"],
3195
+ message: `expected one of ${ASSET_FIELD_TYPES.join(" | ")}`
3196
+ });
3197
+ }
3162
3198
  });
3163
3199
  var assetGroupCreateSchema = z4.object({
3164
3200
  campaign: refSchema,
@@ -6183,7 +6219,10 @@ var testimonialsListRequestSchema = z20.object({
6183
6219
  status: testimonialStatusSchema.optional(),
6184
6220
  sentiment: testimonialSentimentSchema.optional(),
6185
6221
  language: z20.string().min(2).max(5).optional(),
6186
- limit: z20.coerce.number().int().positive().max(200).optional()
6222
+ limit: z20.coerce.number().int().positive().max(200).optional(),
6223
+ // Opaque, from the previous response's `X-Baker-Next-Cursor`. Absent on the
6224
+ // first page; its absence on a response means the corpus is exhausted.
6225
+ cursor: z20.string().optional()
6187
6226
  });
6188
6227
  var testimonialsListResponseSchema = z20.array(testimonialDocSchema);
6189
6228
  var testimonialsGetRequestSchema = z20.object({ id: z20.string().min(1, "Missing id parameter") });
@@ -14780,7 +14819,7 @@ var assetsUpdateCommand = defineCommand30({
14780
14819
  "final-urls": { type: "string", description: "Comma-separated sitelink final URLs" },
14781
14820
  "callout-text": { type: "string", description: "Callout text" },
14782
14821
  header: { type: "string", description: "Structured snippet header" },
14783
- values: { type: "string", description: "Comma-separated structured snippet values" },
14822
+ values: { type: "string", description: "Comma-separated structured snippet values (3-10, max 25 characters each)" },
14784
14823
  cta: { type: "string", description: "Call-to-action text" },
14785
14824
  text: { type: "string", description: "Text asset content" }
14786
14825
  },
@@ -14814,7 +14853,9 @@ var assetsCommand = defineCommand30({
14814
14853
  "Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)"
14815
14854
  ),
14816
14855
  update: assetsUpdateCommand,
14817
- attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer"),
14856
+ attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
14857
+ "Images are not attachable here at all \u2014 Google refuses every image field type through an asset link, at every level. Image extensions have to be added in the Google Ads interface; say so and carry on. Performance Max asset-group images are a different mechanism and do work (`asset-groups attach`)."
14858
+ ]),
14818
14859
  detach: statusCommand2("google.assetLink.detach", "asset link")
14819
14860
  }
14820
14861
  });
@@ -21014,10 +21055,10 @@ function duplicateCommand2(entity, label) {
21014
21055
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
21015
21056
  },
21016
21057
  run: async ({ args }) => {
21017
- const { apiPost: apiPost2 } = await import("./client-3TNO43FO.js");
21058
+ const { apiPost: apiPost2 } = await import("./client-VDCCDEHE.js");
21018
21059
  const { requireChatId: requireChatId2 } = await import("./env-FWMZXMQK.js");
21019
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-B3OCLLS5.js");
21020
- const { handleMetaError: handleMetaError2 } = await import("./shared-J24NXBW4.js");
21060
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-JBYI5IVX.js");
21061
+ const { handleMetaError: handleMetaError2 } = await import("./shared-GWFXJXKJ.js");
21021
21062
  try {
21022
21063
  const accountId = bareAccountId2(args);
21023
21064
  const chatId = requireChatId2();
@@ -50105,14 +50146,30 @@ function emptyReasonDetail(reason, sourceCount, activeFilters) {
50105
50146
  }
50106
50147
  return `${sourceCount} review source${sourceCount === 1 ? " is" : "s are"} connected but hold no testimonials yet \u2014 the source is there, the rows are not. Ingestion may still be running.`;
50107
50148
  }
50149
+ function morePagesHint(resultCount, cursor) {
50150
+ const found = resultCount === 0 ? "This page held no matches" : `This page held ${resultCount}`;
50151
+ return `${found}, and the library continues past it \u2014 so this is one page, not the client's whole set. Do not report it as the corpus and do not conclude anything is missing from it. Read the next page with \`baker testimonials list --cursor ${cursor}\` (same filters), or go straight at the rows you want with \`baker testimonials search <query>\`.`;
50152
+ }
50153
+ function endOfWalkHint(command) {
50154
+ const noun = command === "search" ? "results" : "rows";
50155
+ return `That was the end of the library \u2014 no more ${noun} past the page before this one. Nothing here says the corpus is empty (the earlier pages held it), so answer from what those pages returned rather than re-reading.`;
50156
+ }
50108
50157
  async function measureEmptyCorpus({
50109
50158
  command,
50110
50159
  resultCount,
50111
- activeFilters
50160
+ activeFilters,
50161
+ nextCursor = null,
50162
+ continued = false
50112
50163
  }) {
50164
+ if (nextCursor) {
50165
+ return { hints: [morePagesHint(resultCount, nextCursor)] };
50166
+ }
50113
50167
  if (resultCount > 0) {
50114
50168
  return { hints: [] };
50115
50169
  }
50170
+ if (continued) {
50171
+ return { hints: [endOfWalkHint(command)] };
50172
+ }
50116
50173
  if (activeFilters.length > 0) {
50117
50174
  const reason2 = "filtered_out";
50118
50175
  return {
@@ -50191,7 +50248,12 @@ registerSchema({
50191
50248
  },
50192
50249
  language: { type: "string", description: "Filter by language code (e.g. en, es)", required: false },
50193
50250
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false },
50194
- limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
50251
+ limit: { type: "number", description: "Max results per page (default 50)", required: false, default: 50 },
50252
+ cursor: {
50253
+ type: "string",
50254
+ description: "Read the next page: pass meta.next_cursor from the previous call, with the same filters.",
50255
+ required: false
50256
+ }
50195
50257
  }
50196
50258
  });
50197
50259
  var PARAM_NAMES = {
@@ -50202,7 +50264,8 @@ var PARAM_NAMES = {
50202
50264
  sentiment: "sentiment",
50203
50265
  language: "language",
50204
50266
  tags: "tags",
50205
- limit: "limit"
50267
+ limit: "limit",
50268
+ cursor: "cursor"
50206
50269
  };
50207
50270
  function buildListParams(args) {
50208
50271
  const params = {};
@@ -50227,22 +50290,30 @@ var listCommand18 = defineCommand205({
50227
50290
  sentiment: { type: "string", description: "Filter: positive|neutral|negative", required: false },
50228
50291
  language: { type: "string", description: "Filter by language code (e.g. en, es)", required: false },
50229
50292
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false },
50230
- limit: { type: "string", description: "Max results (default 50)", required: false },
50293
+ limit: { type: "string", description: "Max results per page (default 50)", required: false },
50294
+ cursor: { type: "string", description: "meta.next_cursor from the previous page", required: false },
50231
50295
  output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
50232
50296
  fields: { type: "string", description: "Comma-separated field names to include", required: false },
50233
50297
  full: { type: "boolean", description: "Include full metadata", required: false, default: false }
50234
50298
  },
50235
50299
  run: async ({ args }) => {
50236
50300
  try {
50237
- const data = await apiGet("/api/testimonials", buildListParams(args));
50301
+ const { data, headers } = await apiGetWithHeaders(
50302
+ "/api/testimonials",
50303
+ buildListParams(args)
50304
+ );
50305
+ const nextCursor = headers.get("x-baker-next-cursor");
50238
50306
  const { hints: hints2, empty } = await measureEmptyCorpus({
50239
50307
  command: "list",
50240
50308
  resultCount: data.length,
50241
- activeFilters: activeFilterFlags(args, FILTER_FLAGS)
50309
+ activeFilters: activeFilterFlags(args, FILTER_FLAGS),
50310
+ nextCursor,
50311
+ continued: Boolean(args.cursor)
50242
50312
  });
50243
50313
  const outputFormat = args.output || "json";
50314
+ const meta = { ...empty ? { empty } : {}, ...nextCursor ? { next_cursor: nextCursor } : {} };
50244
50315
  writeOutput(
50245
- { ok: true, data, ...empty ? { meta: { empty } } : {}, ...hints2.length > 0 ? { hints: hints2 } : {} },
50316
+ { ok: true, data, ...Object.keys(meta).length > 0 ? { meta } : {}, ...hints2.length > 0 ? { hints: hints2 } : {} },
50246
50317
  outputFormat,
50247
50318
  args.fields ? args.fields.split(",") : void 0,
50248
50319
  args.full,