@koda-sl/baker-cli 0.257.0 → 0.258.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 +4 -2
- package/dist/cli.js +66 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -658,17 +658,19 @@ baker tag-manager draft show --chat <chat-id> gtm_temp_ab12
|
|
|
658
658
|
baker actions draft --chat <chat-id>
|
|
659
659
|
```
|
|
660
660
|
|
|
661
|
-
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file
|
|
661
|
+
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file` payload — a path to a JSON file, or the JSON object written inline (flags override its keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
|
|
662
662
|
|
|
663
663
|
**A device criterion can be added, but never removed** — `campaign-criteria add` with a device criterion works, but Google then rejects `campaign-criteria remove` on it forever, including on a criterion you added yourself (and Baker refuses the removal before publish). Adjust it instead with `campaign-criteria update customers/<cid>/campaignCriteria/<campaignId>~<criterionId> --bid-modifier <n>`: `1` for no adjustment, `1.25` to bid 25% more, `0.9` to bid 10% less, `0` to stop serving on that device. Locations, languages and ad schedules remove normally.
|
|
664
664
|
|
|
665
|
+
**Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach` rejects it at stage time and names what to do instead. Staging an image therefore also stages a **Task** — "Add N images to <campaign> in Google Ads" — which applies with the rest of the chat, so the one step Google keeps inside its own interface is tracked rather than mentioned. Attach the image to a Performance Max asset group (`asset-groups attach`) and the Task withdraws itself. The one image a Search campaign does take through a link is the advertiser's logo: `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
|
|
666
|
+
|
|
665
667
|
**Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
|
|
666
668
|
|
|
667
669
|
**Account-level conversion goals** — `conversions goal --customer-id … --category PURCHASE --origin WEBSITE --biddable` sets Google's `CustomerConversionGoal` for one (category, origin) pair; `--no-biddable` (or `--biddable=false`) excludes the category from bidding while it keeps being measured. Neither flag defaults, and the spaced form `--biddable false` is rejected. A conversion drives automated bidding only when both halves are on — the action is primary AND its category's account-level goal is biddable — so this is where "my conversion is Primary but bidding ignores it" is fixed. One staged op per pair.
|
|
668
670
|
|
|
669
671
|
**Retiring a conversion action** — `status` accepts `ENABLED` and `REMOVED` only. Google denies `HIDDEN` on every conversion-action write, so to stop bidding toward an action while it keeps measuring use `--no-primary`, and to stop it counting at all use `"status": "REMOVED"`. Conversion actions **Google owns** cannot be changed at all — Smart campaign call/directions actions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions. Staging any edit to one, a rename included, is refused up front; there is no manual route either, since the Google Ads interface will not change them.
|
|
670
672
|
|
|
671
|
-
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
|
|
673
|
+
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Once the chat has published it reports the outcome instead — `published · N applied, M failed`, with a `✗` line naming each op that did not land and why. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
|
|
672
674
|
|
|
673
675
|
**Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
|
|
674
676
|
|
package/dist/cli.js
CHANGED
|
@@ -2309,15 +2309,24 @@ var ASSET_FIELD_TYPES = [
|
|
|
2309
2309
|
* `assetLink.attach` — see that message for what Google actually does with it.
|
|
2310
2310
|
*/
|
|
2311
2311
|
"MARKETING_IMAGE",
|
|
2312
|
-
"BUSINESS_NAME"
|
|
2312
|
+
"BUSINESS_NAME",
|
|
2313
|
+
/**
|
|
2314
|
+
* The one image link a Search campaign takes. It carries the advertiser's logo beside the ad and
|
|
2315
|
+
* is the only image field type that survives Google's field-type check at all — swept live on v23
|
|
2316
|
+
* and v25, campaign and customer level both answer `CUSTOMER_NOT_VERIFIED` (an account fact,
|
|
2317
|
+
* fixed by advertiser verification) rather than a field-type refusal. Ad group level is not a
|
|
2318
|
+
* level Google links it at — see {@link BUSINESS_LOGO_LEVEL_MESSAGE}.
|
|
2319
|
+
*/
|
|
2320
|
+
"BUSINESS_LOGO"
|
|
2313
2321
|
];
|
|
2322
|
+
var BUSINESS_LOGO_LEVEL_MESSAGE = "Google links a BUSINESS_LOGO at campaign or customer level only \u2014 an ad group link comes back UNSUPPORTED_FIELD_TYPE. Attach it to the campaign instead.";
|
|
2314
2323
|
var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
2315
2324
|
"MARKETING_IMAGE",
|
|
2316
2325
|
"SQUARE_MARKETING_IMAGE",
|
|
2317
2326
|
"PORTRAIT_MARKETING_IMAGE",
|
|
2318
2327
|
"AD_IMAGE"
|
|
2319
2328
|
]);
|
|
2320
|
-
var IMAGE_ASSET_LINK_MESSAGE = "Google
|
|
2329
|
+
var IMAGE_ASSET_LINK_MESSAGE = "Google refuses the LINK, not the upload \u2014 so do not stop at this refusal, finish the handover. `assets create` with an image DOES publish: the image lands in the advertiser's Google Ads asset library and is theirs to use. Only this link is refused (MARKETING_IMAGE is a Performance Max asset-group slot \u2014 use `asset-groups attach`; AD_IMAGE comes back UNSUPPORTED_FIELD_TYPE at every level, on every API version). So: stage the image assets anyway, drop this link, and tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets \u2014 name them. Baker stages the Task for that step itself, alongside the image, so it is already in this Session's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload the images; they are uploaded. The one image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO at campaign or customer level, on a verified account.";
|
|
2321
2330
|
function isImageAssetLinkFieldType(fieldType) {
|
|
2322
2331
|
return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
|
|
2323
2332
|
}
|
|
@@ -3214,7 +3223,9 @@ var assetLinkAttachSchema = z4.object({
|
|
|
3214
3223
|
message: `parent is required for a ${value.level}-level asset link (--parent-ref)`
|
|
3215
3224
|
});
|
|
3216
3225
|
}
|
|
3217
|
-
if (
|
|
3226
|
+
if (value.fieldType === "BUSINESS_LOGO" && value.level === "adGroup") {
|
|
3227
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: BUSINESS_LOGO_LEVEL_MESSAGE });
|
|
3228
|
+
} else if (isImageAssetLinkFieldType(value.fieldType)) {
|
|
3218
3229
|
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
|
|
3219
3230
|
} else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
|
|
3220
3231
|
ctx.addIssue({
|
|
@@ -9909,7 +9920,7 @@ function renderSection(title, nodes, lines, showAccount) {
|
|
|
9909
9920
|
}
|
|
9910
9921
|
function renderAdvisories(data, lines) {
|
|
9911
9922
|
const advisories = data.advisories ?? [];
|
|
9912
|
-
if (advisories.length === 0) {
|
|
9923
|
+
if (advisories.length === 0 || data.status === "applied") {
|
|
9913
9924
|
return;
|
|
9914
9925
|
}
|
|
9915
9926
|
lines.push("");
|
|
@@ -9918,15 +9929,37 @@ function renderAdvisories(data, lines) {
|
|
|
9918
9929
|
lines.push(` \u2022 ${advisory.message}`);
|
|
9919
9930
|
}
|
|
9920
9931
|
}
|
|
9932
|
+
function renderOutcome(data, lines) {
|
|
9933
|
+
const counts = /* @__PURE__ */ new Map();
|
|
9934
|
+
for (const op of data.ops) {
|
|
9935
|
+
const status = op.result?.status ?? "pending";
|
|
9936
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
9937
|
+
}
|
|
9938
|
+
const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
|
|
9939
|
+
lines.push(`Google Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
|
|
9940
|
+
for (const op of data.ops) {
|
|
9941
|
+
const status = op.result?.status;
|
|
9942
|
+
if (status !== "failed" && status !== "skipped") {
|
|
9943
|
+
continue;
|
|
9944
|
+
}
|
|
9945
|
+
const reason = op.result?.error ?? op.result?.skippedBecause ?? "no reason recorded";
|
|
9946
|
+
lines.push(` \u2717 ${op.summary} \u2014 ${status}: ${reason}`);
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9921
9949
|
function renderDraftStatus(data) {
|
|
9922
9950
|
if (data.status === "none" || data.count === 0) {
|
|
9923
9951
|
return "No staged Google Ads changes on this chat.";
|
|
9924
9952
|
}
|
|
9925
9953
|
const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to Google Ads." : "Simulated \u2014 publishing completes the whole draft without calling Google Ads.";
|
|
9926
|
-
const lines = [
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9954
|
+
const lines = [];
|
|
9955
|
+
if (data.status === "applied") {
|
|
9956
|
+
renderOutcome(data, lines);
|
|
9957
|
+
} else if (data.status === "publishing") {
|
|
9958
|
+
lines.push(`Google Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
9959
|
+
} else {
|
|
9960
|
+
lines.push(`Google Ads \u2014 ${data.count} staged change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
9961
|
+
}
|
|
9962
|
+
lines.push(modeNote);
|
|
9930
9963
|
const tree = data.tree ?? [];
|
|
9931
9964
|
const showAccount = accountsInTree(tree).size > 1;
|
|
9932
9965
|
renderSection(
|
|
@@ -10043,15 +10076,17 @@ function loadJsonFileArg(path38) {
|
|
|
10043
10076
|
if (typeof path38 !== "string" || path38.length === 0) {
|
|
10044
10077
|
return {};
|
|
10045
10078
|
}
|
|
10079
|
+
const inline = path38.trimStart().startsWith("{");
|
|
10080
|
+
const source = inline ? "inline JSON" : path38;
|
|
10046
10081
|
try {
|
|
10047
|
-
const parsed = JSON.parse(readFileSync2(path38, "utf8"));
|
|
10082
|
+
const parsed = JSON.parse(inline ? path38 : readFileSync2(path38, "utf8"));
|
|
10048
10083
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10049
|
-
failWriteValidation(`${
|
|
10084
|
+
failWriteValidation(`${source} must contain a JSON object`);
|
|
10050
10085
|
}
|
|
10051
10086
|
return parsed;
|
|
10052
10087
|
} catch (err) {
|
|
10053
10088
|
if (err instanceof SyntaxError) {
|
|
10054
|
-
failWriteValidation(`${
|
|
10089
|
+
failWriteValidation(`${source} is not valid JSON: ${err.message}`);
|
|
10055
10090
|
}
|
|
10056
10091
|
throw err;
|
|
10057
10092
|
}
|
|
@@ -13877,7 +13912,10 @@ Examples:
|
|
|
13877
13912
|
import { defineCommand as defineCommand30 } from "citty";
|
|
13878
13913
|
var customerIdArg = { "customer-id": { type: "string", description: "10-digit Google Ads customer id" } };
|
|
13879
13914
|
var fileArg = {
|
|
13880
|
-
file: {
|
|
13915
|
+
file: {
|
|
13916
|
+
type: "string",
|
|
13917
|
+
description: "The op payload: a JSON file path, or the JSON object inline (flags override it)"
|
|
13918
|
+
}
|
|
13881
13919
|
};
|
|
13882
13920
|
var BUDGET_DEMAND_HINTS = [
|
|
13883
13921
|
"Check this daily budget against the search volume you measured for these keywords, not against the plan you had before you measured it. A budget many times larger than the traffic the target pool can absorb never spends, and reads to the client as a number nobody costed.",
|
|
@@ -14877,10 +14915,19 @@ function fileCreateCommand(name, kind, description, hints2) {
|
|
|
14877
14915
|
if (Object.keys(payload).length === 0) {
|
|
14878
14916
|
failWriteValidation(`${kind} needs --file with the op payload`);
|
|
14879
14917
|
}
|
|
14880
|
-
await stageGoogleOp({ kind, customerId, payload }, hints2);
|
|
14918
|
+
await stageGoogleOp({ kind, customerId, payload }, typeof hints2 === "function" ? hints2(payload) : hints2);
|
|
14881
14919
|
}
|
|
14882
14920
|
});
|
|
14883
14921
|
}
|
|
14922
|
+
function imageAssetHints(payload) {
|
|
14923
|
+
if (payload.type !== "image") {
|
|
14924
|
+
return [];
|
|
14925
|
+
}
|
|
14926
|
+
return [
|
|
14927
|
+
"This image publishes into the advertiser's Google Ads asset library \u2014 that half works. What Google refuses is the link that puts it on a Search campaign, so do not stage an `assets attach` for it.",
|
|
14928
|
+
"Finish the handover rather than reporting a limitation: tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets, and name the ones you staged. Baker stages the Task for that step itself the moment you stage the image, so it is already in this chat's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload images they already have. (A Performance Max asset group is different \u2014 images do link there, with `asset-groups attach`.)"
|
|
14929
|
+
];
|
|
14930
|
+
}
|
|
14884
14931
|
var assetsUpdateCommand = defineCommand30({
|
|
14885
14932
|
meta: {
|
|
14886
14933
|
name: "update",
|
|
@@ -14921,17 +14968,18 @@ var assetsUpdateCommand = defineCommand30({
|
|
|
14921
14968
|
var assetsCommand = defineCommand30({
|
|
14922
14969
|
meta: {
|
|
14923
14970
|
name: "assets",
|
|
14924
|
-
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign.
|
|
14971
|
+
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Image extensions are the one thing you cannot finish here \u2014 `create` uploads the image into the advertiser's account, but Google refuses the link that puts it on a Search campaign, so hand that one step over rather than telling them to upload anything. The image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO, at campaign or customer level, on a verified account."
|
|
14925
14972
|
},
|
|
14926
14973
|
subCommands: {
|
|
14927
14974
|
create: fileCreateCommand(
|
|
14928
14975
|
"create",
|
|
14929
14976
|
"google.asset.create",
|
|
14930
|
-
"Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)"
|
|
14977
|
+
"Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)",
|
|
14978
|
+
imageAssetHints
|
|
14931
14979
|
),
|
|
14932
14980
|
update: assetsUpdateCommand,
|
|
14933
14981
|
attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
|
|
14934
|
-
"
|
|
14982
|
+
"Image extensions are not attachable here \u2014 Google refuses AD_IMAGE and every marketing-image field type at every level, on every API version. The upload is not refused, though: stage the images with `assets create`, then tell the user they are already in their Google Ads account and only need adding to the campaign under Assets. Baker stages that Task by itself when the image is staged. Two images that DO work: the advertiser's logo through this command (`BUSINESS_LOGO`, campaign or customer level, on a verified account), and Performance Max asset-group images, which are a different mechanism (`asset-groups attach`)."
|
|
14935
14983
|
]),
|
|
14936
14984
|
detach: statusCommand2("google.assetLink.detach", "asset link")
|
|
14937
14985
|
}
|
|
@@ -18714,7 +18762,7 @@ function renderSection2(title, nodes, lines, showAccount) {
|
|
|
18714
18762
|
}
|
|
18715
18763
|
}
|
|
18716
18764
|
var CAMPAIGN_TREE_ENTITIES = /* @__PURE__ */ new Set(["campaignGroup", "campaign", "creative"]);
|
|
18717
|
-
function
|
|
18765
|
+
function renderOutcome2(data, lines) {
|
|
18718
18766
|
const counts = /* @__PURE__ */ new Map();
|
|
18719
18767
|
for (const op of data.ops ?? []) {
|
|
18720
18768
|
const status = op.result?.status ?? "pending";
|
|
@@ -18738,7 +18786,7 @@ function renderDraftStatus2(data) {
|
|
|
18738
18786
|
const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to LinkedIn." : "Simulated \u2014 publishing completes the whole draft without calling LinkedIn.";
|
|
18739
18787
|
const lines = [];
|
|
18740
18788
|
if (data.status === "applied") {
|
|
18741
|
-
|
|
18789
|
+
renderOutcome2(data, lines);
|
|
18742
18790
|
} else if (data.status === "publishing") {
|
|
18743
18791
|
lines.push(`LinkedIn Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
18744
18792
|
} else {
|