@koda-sl/baker-cli 0.329.0 → 0.331.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/README.md CHANGED
@@ -99,8 +99,9 @@ An option a command does not declare stops the run with `UNKNOWN_ARGUMENT`, nami
99
99
  closest real one. Previously such an option was accepted and ignored, so a call that
100
100
  looked like it had filtered or narrowed a result was in fact answering the unfiltered
101
101
  question — and nothing in the output said so. Both spellings of an option work
102
- (`--run-id` and `--runId`), as they always did. Run `baker schema <command>` to see
103
- every option a command takes.
102
+ (`--run-id` and `--runId`), as they always did. The error's `fix` names the exact call
103
+ that lists every option: `baker schema <command>` (`check_schema`) when the command
104
+ registers a schema, `baker <command> --help` (`check_help`) when it does not.
104
105
 
105
106
  ### Fresh reads: `--no-cache`
106
107
 
@@ -299,7 +300,7 @@ Read it before interpreting any null. Field coverage is folded page by page on t
299
300
  }
300
301
  ```
301
302
 
302
- Never total or count from a response carrying it — re-run with `--all --limit 10000 --out data.csv` (the API accepts page sizes up to 10,000) and check the re-run came back without the warning. Warnings reach every output format, so no `--output` choice can hide one:
303
+ Never total or count from a response carrying it — re-run with `--all --limit 10000 --out data.csv` (the API accepts page sizes up to 10,000) and check the re-run came back without the warning. A read that returns one row by construction — `FROM customer` with no segment selected, such as `--preset account-summary` — has no page to fill, so it never carries `TRUNCATED_RESULTS`, nor `CONVERSION_MIX`, since one account-wide row has no campaigns to total or compare. Warnings reach every output format, so no `--output` choice can hide one:
303
304
 
304
305
  | `--output` | Where warnings land |
305
306
  |------------|---------------------|
@@ -3661,8 +3662,9 @@ Inspect command argument schemas for AI agent introspection.
3661
3662
 
3662
3663
  ```bash
3663
3664
  baker schema # List all commands
3664
- baker schema ads.google.query # Get query command schema
3665
- baker schema images.search # Get image search schema
3665
+ baker schema ads google query # Get query command schema, path as typed
3666
+ baker schema ads.google.query # Same schema, dotted name
3667
+ baker schema tag-manager read # Kebab-case paths resolve to their registered name (tagManager.read)
3666
3668
  ```
3667
3669
 
3668
3670
  Not every command registers a schema. On `NOT_FOUND` the error carries a structured `fix` pointing at `--help` and the family's agent doc (`__tooling__/docs/tools/baker/<family>.md` in sandboxes). Every top-level family's `--help` also ends with a `Full guide:` line pointing at that doc; `packages/cli/__tests__/doc-coverage.test.ts` fails CI if a mounted command is missing from its family doc.
package/dist/cli.js CHANGED
@@ -578,12 +578,24 @@ var registry = /* @__PURE__ */ new Map();
578
578
  function registerSchema(schema) {
579
579
  registry.set(schema.command, schema);
580
580
  }
581
- function getSchema(command) {
582
- return registry.get(command);
583
- }
584
581
  function listSchemas() {
585
582
  return [...registry.keys()];
586
583
  }
584
+ function spellingKey(name) {
585
+ return name.toLowerCase().replaceAll("-", "");
586
+ }
587
+ function resolveSchemaName(words2, names) {
588
+ const joined = words2.join(".");
589
+ if (names.includes(joined)) {
590
+ return joined;
591
+ }
592
+ const matches = names.filter((name) => spellingKey(name) === spellingKey(joined));
593
+ return matches.length === 1 ? matches[0] : void 0;
594
+ }
595
+ function findSchema(words2) {
596
+ const name = resolveSchemaName(words2, listSchemas());
597
+ return name === void 0 ? void 0 : registry.get(name);
598
+ }
587
599
 
588
600
  // ../api/src/actions.ts
589
601
  import { z } from "zod";
@@ -11834,160 +11846,6 @@ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync
11834
11846
  import { resolve } from "path";
11835
11847
  import { defineCommand as defineCommand28 } from "citty";
11836
11848
 
11837
- // src/commands/ads/google/conversion-mix.ts
11838
- var CONVERSION_METRICS = [
11839
- "METRICS.CONVERSIONS",
11840
- "METRICS.ALL_CONVERSIONS",
11841
- "METRICS.CONVERSIONS_VALUE",
11842
- "METRICS.ALL_CONVERSIONS_VALUE",
11843
- "METRICS.COST_PER_CONVERSION",
11844
- "METRICS.COST_PER_ALL_CONVERSIONS",
11845
- "METRICS.CONVERSIONS_FROM_INTERACTIONS_RATE",
11846
- "METRICS.ALL_CONVERSIONS_FROM_INTERACTIONS_RATE",
11847
- "METRICS.VALUE_PER_CONVERSION",
11848
- "METRICS.VALUE_PER_ALL_CONVERSIONS"
11849
- ];
11850
- var CONVERSION_SEGMENTS = ["SEGMENTS.CONVERSION_ACTION_NAME", "SEGMENTS.CONVERSION_ACTION="];
11851
- var CAMPAIGN_EQUALITY = /\bCAMPAIGN\.(ID|NAME|RESOURCE_NAME)\s*=/;
11852
- function whereClause(upperQuery) {
11853
- const whereIdx = upperQuery.indexOf(" WHERE ");
11854
- if (whereIdx === -1) return "";
11855
- let rest = upperQuery.slice(whereIdx + 7);
11856
- for (const trailing of [" ORDER BY ", " GROUP BY ", " LIMIT ", " PARAMETERS "]) {
11857
- const idx = rest.indexOf(trailing);
11858
- if (idx !== -1) rest = rest.slice(0, idx);
11859
- }
11860
- return rest;
11861
- }
11862
- function conversionMixWarnings(finalQuery) {
11863
- const upper2 = ` ${finalQuery.toUpperCase().replace(/\s+/g, " ")} `;
11864
- const readsConversions = CONVERSION_METRICS.some((metric) => upper2.includes(metric));
11865
- if (!readsConversions) return [];
11866
- if (CONVERSION_SEGMENTS.some((segment) => upper2.includes(segment))) return [];
11867
- if (CAMPAIGN_EQUALITY.test(whereClause(upper2))) return [];
11868
- return [
11869
- {
11870
- code: "CONVERSION_MIX",
11871
- message: "MIXED CONVERSION ACTIONS \u2014 this read covers more than one campaign, and a campaign's conversion figures count whatever conversion actions are primary for it. Those differ campaign by campaign (a webinar registration, an info request, a CRM-stage SQL), so a total across these rows adds different events and a conversion rate from one campaign is not comparable with another's. Before totalling or comparing, re-run segmented by segments.conversion_action_name and check the campaigns you are putting side by side count the same action. If they do not, compare only within one action \u2014 and carry that caveat into anything you write for the user, not just the analysis."
11872
- }
11873
- ];
11874
- }
11875
-
11876
- // src/commands/ads/google/keyword-serving.ts
11877
- var BELOW_FIRST_PAGE_BID = "AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID";
11878
- var ACTIONABLE_REASONS = {
11879
- [BELOW_FIRST_PAGE_BID]: {
11880
- label: "Below first page bid",
11881
- advice: "raise the max CPC (keywords update --cpc-bid) to at least the first-page estimate, or move the ad group onto a smart bidding strategy that sets the bid itself"
11882
- },
11883
- AD_GROUP_CRITERION_LOW_QUALITY: {
11884
- label: "Low quality score",
11885
- advice: "a bid raise does not lift quality \u2014 tighten ad relevance to the keyword and the landing page it points at (ad_group_criterion.quality_info.* breaks the score into its three parts)"
11886
- },
11887
- AD_GROUP_CRITERION_RARELY_SERVED: {
11888
- label: "Rarely served",
11889
- advice: "too few people search this term \u2014 broaden the match type or drop it, don't bid it up"
11890
- },
11891
- AD_GROUP_CRITERION_DISAPPROVED: {
11892
- label: "Disapproved",
11893
- advice: "select ad_group_criterion.disapproval_reasons to see which policy blocked it"
11894
- },
11895
- AD_GROUP_CRITERION_RESTRICTED: {
11896
- label: "Restricted",
11897
- advice: "policy limits where and to whom this keyword may show"
11898
- },
11899
- AD_GROUP_CRITERION_UNDER_REVIEW: {
11900
- label: "Under review",
11901
- advice: "waiting on Google's review \u2014 nothing to change yet"
11902
- },
11903
- AD_GROUP_CRITERION_PENDING_REVIEW: {
11904
- label: "Pending review",
11905
- advice: "waiting on Google's review \u2014 nothing to change yet"
11906
- },
11907
- AD_GROUP_CRITERION_PAUSED_DUE_TO_LOW_ACTIVITY: {
11908
- label: "Auto-paused for low activity",
11909
- advice: "Google paused it after prolonged low serving \u2014 resume it only alongside a change that gives it traffic"
11910
- }
11911
- };
11912
- var REASON_ORDER = Object.keys(ACTIONABLE_REASONS);
11913
- var MAX_NAMED_KEYWORDS = 5;
11914
- function toMicros2(value) {
11915
- if (typeof value === "number" && Number.isFinite(value)) return value;
11916
- if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
11917
- return null;
11918
- }
11919
- function formatMoney(micros) {
11920
- return (micros / 1e6).toFixed(2);
11921
- }
11922
- function keywordLabel(row) {
11923
- const text2 = row["ad_group_criterion.keyword.text"];
11924
- const id = row["ad_group_criterion.criterion_id"];
11925
- const name = typeof text2 === "string" && text2 !== "" ? text2 : id == null ? "(unnamed keyword)" : `criterion ${id}`;
11926
- const adGroup = row["ad_group.name"];
11927
- return typeof adGroup === "string" && adGroup !== "" ? `${name} (ad group ${adGroup})` : name;
11928
- }
11929
- function keywordKey(row) {
11930
- const adGroup = row["ad_group.id"] ?? row["ad_group.name"] ?? "";
11931
- const criterion = row["ad_group_criterion.criterion_id"] ?? row["ad_group_criterion.keyword.text"] ?? "";
11932
- return `${String(adGroup)}~${String(criterion)}` === "~" ? keywordLabel(row) : `${adGroup}~${criterion}`;
11933
- }
11934
- function reasonsOn(row) {
11935
- const raw = row["ad_group_criterion.primary_status_reasons"];
11936
- if (!Array.isArray(raw)) return null;
11937
- return raw.filter((r) => typeof r === "string");
11938
- }
11939
- function bidShortfall(row) {
11940
- const bid = toMicros2(row["ad_group_criterion.effective_cpc_bid_micros"] ?? row["ad_group_criterion.cpc_bid_micros"]);
11941
- const firstPage = toMicros2(row["ad_group_criterion.position_estimates.first_page_cpc_micros"]);
11942
- if (bid == null || firstPage == null || bid >= firstPage) return null;
11943
- return `bids ${formatMoney(bid)} against a ${formatMoney(firstPage)} first-page estimate`;
11944
- }
11945
- function limitsOn(row, options) {
11946
- const reasons = reasonsOn(row);
11947
- const shortfall = bidShortfall(row);
11948
- const label = keywordLabel(row);
11949
- const applicable = reasons ? reasons.filter((reason) => reason in ACTIONABLE_REASONS) : options.statusReasonsSelected || shortfall === null ? [] : [BELOW_FIRST_PAGE_BID];
11950
- return applicable.map((reason) => ({
11951
- reason,
11952
- detail: reason === BELOW_FIRST_PAGE_BID && shortfall ? `"${label}" ${shortfall}` : `"${label}"`
11953
- }));
11954
- }
11955
- function describeLimit(reason, details) {
11956
- const named = details.slice(0, MAX_NAMED_KEYWORDS).join("; ");
11957
- const rest = details.length > MAX_NAMED_KEYWORDS ? ` (+${details.length - MAX_NAMED_KEYWORDS} more)` : "";
11958
- const { label, advice } = ACTIONABLE_REASONS[reason];
11959
- return `${label} \u2014 ${details.length}: ${named}${rest}. Fix: ${advice}.`;
11960
- }
11961
- function collectKeywordLimits(rows, options, acc = { detailsByReason: /* @__PURE__ */ new Map(), keywords: /* @__PURE__ */ new Set() }) {
11962
- for (const row of rows) {
11963
- const limits = limitsOn(row, options);
11964
- if (limits.length === 0) continue;
11965
- acc.keywords.add(keywordKey(row));
11966
- for (const { reason, detail } of limits) {
11967
- const bucket = acc.detailsByReason.get(reason);
11968
- if (bucket) bucket.push(detail);
11969
- else acc.detailsByReason.set(reason, [detail]);
11970
- }
11971
- }
11972
- return acc;
11973
- }
11974
- function renderKeywordLimits(limits) {
11975
- if (limits.detailsByReason.size === 0) return [];
11976
- const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
11977
- (reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
11978
- );
11979
- const count2 = limits.keywords.size;
11980
- return [
11981
- {
11982
- code: "KEYWORD_SERVING_LIMITED",
11983
- message: `${count2} ${count2 === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
11984
- }
11985
- ];
11986
- }
11987
- function keywordServingWarnings(rows, options) {
11988
- return renderKeywordLimits(collectKeywordLimits(rows, options));
11989
- }
11990
-
11991
11849
  // src/commands/ads/google/presets.ts
11992
11850
  var GAQL_DATE_RANGE_RE = /^(?:TODAY|YESTERDAY|LAST_7_DAYS|LAST_14_DAYS|LAST_30_DAYS|LAST_90_DAYS|THIS_MONTH|LAST_MONTH|THIS_QUARTER|LAST_QUARTER|THIS_YEAR|LAST_YEAR|ALL_TIME|BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}')$/i;
11993
11851
  function isValidDateRange(value) {
@@ -12155,7 +12013,7 @@ var RUNG_PIN_FIELDS = {
12155
12013
  "campaign_asset.status": ["campaign_asset.resource_name"],
12156
12014
  "ad_group_asset.status": ["ad_group_asset.resource_name"]
12157
12015
  };
12158
- function whereClause2(query) {
12016
+ function whereClause(query) {
12159
12017
  const match = query.match(/\bWHERE\b([\s\S]*)$/i);
12160
12018
  if (!match?.[1]) return "";
12161
12019
  let rest = match[1];
@@ -12175,7 +12033,7 @@ function analyzeServingChain(query) {
12175
12033
  const resource = fromResource(query);
12176
12034
  const chain = resource ? SERVING_CHAINS[resource] : void 0;
12177
12035
  if (!chain) return null;
12178
- const where = whereClause2(query);
12036
+ const where = whereClause(query);
12179
12037
  if (/ad_group_criterion\.negative\s*=\s*TRUE/i.test(where)) return null;
12180
12038
  const unscoped = chain.filter((rung) => {
12181
12039
  if (mentions(where, rung)) return false;
@@ -12227,7 +12085,7 @@ function fromResource(query) {
12227
12085
  function isSingleRowRead(query) {
12228
12086
  const resource = fromResource(query);
12229
12087
  if (!resource || !SINGLE_ROW_RESOURCES.includes(resource)) return false;
12230
- return !/\bsegments\./i.test(query);
12088
+ return !/\bsegments\./i.test(selectClause(query));
12231
12089
  }
12232
12090
  var SEGMENTING_ANCESTORS = {
12233
12091
  campaign_budget: ["campaign"],
@@ -12402,6 +12260,161 @@ function validatePreflight(query, customerId, limit) {
12402
12260
  return { valid: true, correctedQuery: annotated.query, warnings };
12403
12261
  }
12404
12262
 
12263
+ // src/commands/ads/google/conversion-mix.ts
12264
+ var CONVERSION_METRICS = [
12265
+ "METRICS.CONVERSIONS",
12266
+ "METRICS.ALL_CONVERSIONS",
12267
+ "METRICS.CONVERSIONS_VALUE",
12268
+ "METRICS.ALL_CONVERSIONS_VALUE",
12269
+ "METRICS.COST_PER_CONVERSION",
12270
+ "METRICS.COST_PER_ALL_CONVERSIONS",
12271
+ "METRICS.CONVERSIONS_FROM_INTERACTIONS_RATE",
12272
+ "METRICS.ALL_CONVERSIONS_FROM_INTERACTIONS_RATE",
12273
+ "METRICS.VALUE_PER_CONVERSION",
12274
+ "METRICS.VALUE_PER_ALL_CONVERSIONS"
12275
+ ];
12276
+ var CONVERSION_SEGMENTS = ["SEGMENTS.CONVERSION_ACTION_NAME", "SEGMENTS.CONVERSION_ACTION="];
12277
+ var CAMPAIGN_EQUALITY = /\bCAMPAIGN\.(ID|NAME|RESOURCE_NAME)\s*=/;
12278
+ function whereClause2(upperQuery) {
12279
+ const whereIdx = upperQuery.indexOf(" WHERE ");
12280
+ if (whereIdx === -1) return "";
12281
+ let rest = upperQuery.slice(whereIdx + 7);
12282
+ for (const trailing of [" ORDER BY ", " GROUP BY ", " LIMIT ", " PARAMETERS "]) {
12283
+ const idx = rest.indexOf(trailing);
12284
+ if (idx !== -1) rest = rest.slice(0, idx);
12285
+ }
12286
+ return rest;
12287
+ }
12288
+ function conversionMixWarnings(finalQuery) {
12289
+ const upper2 = ` ${finalQuery.toUpperCase().replace(/\s+/g, " ")} `;
12290
+ const readsConversions = CONVERSION_METRICS.some((metric) => upper2.includes(metric));
12291
+ if (!readsConversions) return [];
12292
+ if (CONVERSION_SEGMENTS.some((segment) => upper2.includes(segment))) return [];
12293
+ if (CAMPAIGN_EQUALITY.test(whereClause2(upper2))) return [];
12294
+ if (isSingleRowRead(finalQuery)) return [];
12295
+ return [
12296
+ {
12297
+ code: "CONVERSION_MIX",
12298
+ message: "MIXED CONVERSION ACTIONS \u2014 this read covers more than one campaign, and a campaign's conversion figures count whatever conversion actions are primary for it. Those differ campaign by campaign (a webinar registration, an info request, a CRM-stage SQL), so a total across these rows adds different events and a conversion rate from one campaign is not comparable with another's. Before totalling or comparing, re-run segmented by segments.conversion_action_name and check the campaigns you are putting side by side count the same action. If they do not, compare only within one action \u2014 and carry that caveat into anything you write for the user, not just the analysis."
12299
+ }
12300
+ ];
12301
+ }
12302
+
12303
+ // src/commands/ads/google/keyword-serving.ts
12304
+ var BELOW_FIRST_PAGE_BID = "AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID";
12305
+ var ACTIONABLE_REASONS = {
12306
+ [BELOW_FIRST_PAGE_BID]: {
12307
+ label: "Below first page bid",
12308
+ advice: "raise the max CPC (keywords update --cpc-bid) to at least the first-page estimate, or move the ad group onto a smart bidding strategy that sets the bid itself"
12309
+ },
12310
+ AD_GROUP_CRITERION_LOW_QUALITY: {
12311
+ label: "Low quality score",
12312
+ advice: "a bid raise does not lift quality \u2014 tighten ad relevance to the keyword and the landing page it points at (ad_group_criterion.quality_info.* breaks the score into its three parts)"
12313
+ },
12314
+ AD_GROUP_CRITERION_RARELY_SERVED: {
12315
+ label: "Rarely served",
12316
+ advice: "too few people search this term \u2014 broaden the match type or drop it, don't bid it up"
12317
+ },
12318
+ AD_GROUP_CRITERION_DISAPPROVED: {
12319
+ label: "Disapproved",
12320
+ advice: "select ad_group_criterion.disapproval_reasons to see which policy blocked it"
12321
+ },
12322
+ AD_GROUP_CRITERION_RESTRICTED: {
12323
+ label: "Restricted",
12324
+ advice: "policy limits where and to whom this keyword may show"
12325
+ },
12326
+ AD_GROUP_CRITERION_UNDER_REVIEW: {
12327
+ label: "Under review",
12328
+ advice: "waiting on Google's review \u2014 nothing to change yet"
12329
+ },
12330
+ AD_GROUP_CRITERION_PENDING_REVIEW: {
12331
+ label: "Pending review",
12332
+ advice: "waiting on Google's review \u2014 nothing to change yet"
12333
+ },
12334
+ AD_GROUP_CRITERION_PAUSED_DUE_TO_LOW_ACTIVITY: {
12335
+ label: "Auto-paused for low activity",
12336
+ advice: "Google paused it after prolonged low serving \u2014 resume it only alongside a change that gives it traffic"
12337
+ }
12338
+ };
12339
+ var REASON_ORDER = Object.keys(ACTIONABLE_REASONS);
12340
+ var MAX_NAMED_KEYWORDS = 5;
12341
+ function toMicros2(value) {
12342
+ if (typeof value === "number" && Number.isFinite(value)) return value;
12343
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
12344
+ return null;
12345
+ }
12346
+ function formatMoney(micros) {
12347
+ return (micros / 1e6).toFixed(2);
12348
+ }
12349
+ function keywordLabel(row) {
12350
+ const text2 = row["ad_group_criterion.keyword.text"];
12351
+ const id = row["ad_group_criterion.criterion_id"];
12352
+ const name = typeof text2 === "string" && text2 !== "" ? text2 : id == null ? "(unnamed keyword)" : `criterion ${id}`;
12353
+ const adGroup = row["ad_group.name"];
12354
+ return typeof adGroup === "string" && adGroup !== "" ? `${name} (ad group ${adGroup})` : name;
12355
+ }
12356
+ function keywordKey(row) {
12357
+ const adGroup = row["ad_group.id"] ?? row["ad_group.name"] ?? "";
12358
+ const criterion = row["ad_group_criterion.criterion_id"] ?? row["ad_group_criterion.keyword.text"] ?? "";
12359
+ return `${String(adGroup)}~${String(criterion)}` === "~" ? keywordLabel(row) : `${adGroup}~${criterion}`;
12360
+ }
12361
+ function reasonsOn(row) {
12362
+ const raw = row["ad_group_criterion.primary_status_reasons"];
12363
+ if (!Array.isArray(raw)) return null;
12364
+ return raw.filter((r) => typeof r === "string");
12365
+ }
12366
+ function bidShortfall(row) {
12367
+ const bid = toMicros2(row["ad_group_criterion.effective_cpc_bid_micros"] ?? row["ad_group_criterion.cpc_bid_micros"]);
12368
+ const firstPage = toMicros2(row["ad_group_criterion.position_estimates.first_page_cpc_micros"]);
12369
+ if (bid == null || firstPage == null || bid >= firstPage) return null;
12370
+ return `bids ${formatMoney(bid)} against a ${formatMoney(firstPage)} first-page estimate`;
12371
+ }
12372
+ function limitsOn(row, options) {
12373
+ const reasons = reasonsOn(row);
12374
+ const shortfall = bidShortfall(row);
12375
+ const label = keywordLabel(row);
12376
+ const applicable = reasons ? reasons.filter((reason) => reason in ACTIONABLE_REASONS) : options.statusReasonsSelected || shortfall === null ? [] : [BELOW_FIRST_PAGE_BID];
12377
+ return applicable.map((reason) => ({
12378
+ reason,
12379
+ detail: reason === BELOW_FIRST_PAGE_BID && shortfall ? `"${label}" ${shortfall}` : `"${label}"`
12380
+ }));
12381
+ }
12382
+ function describeLimit(reason, details) {
12383
+ const named = details.slice(0, MAX_NAMED_KEYWORDS).join("; ");
12384
+ const rest = details.length > MAX_NAMED_KEYWORDS ? ` (+${details.length - MAX_NAMED_KEYWORDS} more)` : "";
12385
+ const { label, advice } = ACTIONABLE_REASONS[reason];
12386
+ return `${label} \u2014 ${details.length}: ${named}${rest}. Fix: ${advice}.`;
12387
+ }
12388
+ function collectKeywordLimits(rows, options, acc = { detailsByReason: /* @__PURE__ */ new Map(), keywords: /* @__PURE__ */ new Set() }) {
12389
+ for (const row of rows) {
12390
+ const limits = limitsOn(row, options);
12391
+ if (limits.length === 0) continue;
12392
+ acc.keywords.add(keywordKey(row));
12393
+ for (const { reason, detail } of limits) {
12394
+ const bucket = acc.detailsByReason.get(reason);
12395
+ if (bucket) bucket.push(detail);
12396
+ else acc.detailsByReason.set(reason, [detail]);
12397
+ }
12398
+ }
12399
+ return acc;
12400
+ }
12401
+ function renderKeywordLimits(limits) {
12402
+ if (limits.detailsByReason.size === 0) return [];
12403
+ const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
12404
+ (reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
12405
+ );
12406
+ const count2 = limits.keywords.size;
12407
+ return [
12408
+ {
12409
+ code: "KEYWORD_SERVING_LIMITED",
12410
+ message: `${count2} ${count2 === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
12411
+ }
12412
+ ];
12413
+ }
12414
+ function keywordServingWarnings(rows, options) {
12415
+ return renderKeywordLimits(collectKeywordLimits(rows, options));
12416
+ }
12417
+
12405
12418
  // src/commands/ads/google/query.ts
12406
12419
  registerSchema({
12407
12420
  command: "ads.google.query",
@@ -12506,6 +12519,7 @@ function writeRowsToFile(filePath, rows, fields, append) {
12506
12519
  }
12507
12520
  }
12508
12521
  function effectiveRowLimit(finalQuery, limit, all = false) {
12522
+ if (isSingleRowRead(finalQuery)) return void 0;
12509
12523
  const match = finalQuery.match(/\bLIMIT\s+(\d+)/i);
12510
12524
  const statementLimit = match?.[1] ? Number(match[1]) : void 0;
12511
12525
  if (all) return statementLimit;
@@ -62386,23 +62400,24 @@ function narrowToFamily(commandName, available) {
62386
62400
  var schemaCommand2 = defineCommand204({
62387
62401
  meta: {
62388
62402
  name: "schema",
62389
- description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
62403
+ description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images search (or images.search)"
62390
62404
  },
62391
62405
  args: {
62392
62406
  command: {
62393
62407
  type: "positional",
62394
- description: "Command name (e.g. images.list). Omit to list all.",
62408
+ description: "Command path, as typed (images list) or dotted (images.list). Omit to list all.",
62395
62409
  required: false
62396
62410
  }
62397
62411
  },
62398
62412
  run: ({ args: args4 }) => {
62399
- const commandName = args4.command;
62400
- if (!commandName) {
62413
+ const words2 = args4._.filter((word) => typeof word === "string" && word.length > 0);
62414
+ if (words2.length === 0) {
62401
62415
  const schemas = listSchemas();
62402
62416
  writeJson({ ok: true, data: { commands: schemas } });
62403
62417
  return;
62404
62418
  }
62405
- const schema = getSchema(commandName);
62419
+ const commandName = words2.join(".");
62420
+ const schema = findSchema(words2);
62406
62421
  if (!schema) {
62407
62422
  const all = listSchemas();
62408
62423
  const available = narrowToFamily(commandName, all);
@@ -66995,7 +67010,7 @@ function commandFor(root, argv) {
66995
67010
  const { command, resolved } = resolveCommand2(root, argv);
66996
67011
  return resolved ? command : null;
66997
67012
  }
66998
- function unknownFlagEnvelope(unknown, commandPath, suggestion) {
67013
+ function unknownFlagEnvelope(unknown, commandPath, suggestion, hasSchema) {
66999
67014
  const list = unknown.join(", ");
67000
67015
  return {
67001
67016
  ok: false,
@@ -67003,8 +67018,8 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
67003
67018
  code: "UNKNOWN_ARGUMENT",
67004
67019
  message: `${list} ${unknown.length > 1 ? "are not options" : "is not an option"} of \`baker ${commandPath}\`.`,
67005
67020
  fix: {
67006
- action: "check_schema",
67007
- explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`baker schema ${commandPath}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
67021
+ action: hasSchema ? "check_schema" : "check_help",
67022
+ explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`${hasSchema ? `baker schema ${commandPath}` : `baker ${commandPath} --help`}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
67008
67023
  },
67009
67024
  retryable: false
67010
67025
  }
@@ -67035,8 +67050,9 @@ function refuseUnknownFlags(root, argv) {
67035
67050
  const command = commandFor(root, argv);
67036
67051
  const first = unknown[0];
67037
67052
  const suggestion = command && first ? closestFlag(first, command) : null;
67053
+ const path48 = commandPathOf(root, argv);
67038
67054
  process.stdout.write(
67039
- `${JSON.stringify(unknownFlagEnvelope(unknown, commandPathOf(root, argv), suggestion), null, 2)}
67055
+ `${JSON.stringify(unknownFlagEnvelope(unknown, path48, suggestion, findSchema(path48.split(" ")) !== void 0), null, 2)}
67040
67056
  `
67041
67057
  );
67042
67058
  process.exit(1);