@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 +7 -5
- package/dist/cli.js +185 -169
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
103
|
-
every option
|
|
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
|
|
3665
|
-
baker 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
|
|
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 =
|
|
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
|
|
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
|
|
62400
|
-
if (
|
|
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
|
|
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
|
|
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,
|
|
67055
|
+
`${JSON.stringify(unknownFlagEnvelope(unknown, path48, suggestion, findSchema(path48.split(" ")) !== void 0), null, 2)}
|
|
67040
67056
|
`
|
|
67041
67057
|
);
|
|
67042
67058
|
process.exit(1);
|