@koda-sl/baker-cli 0.177.0 → 0.179.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
@@ -528,6 +528,9 @@ baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<ad
528
528
  --text "brand name, brand shop:PHRASE, brand store" --match-type EXACT
529
529
  baker ads google keyword-lists add --customer-id 1234567890 --list-ref <list-id> --file negatives.txt
530
530
  # negatives.txt: one keyword[:MATCH_TYPE] per line; blank lines and "# comments" are skipped
531
+ # Send one keyword to its own page, then put it back on the ad's — never remove + re-add for this
532
+ baker ads google keywords update <criterion-id> --customer-id 1234567890 --final-url https://example.com/solar
533
+ baker ads google keywords update <criterion-id> --customer-id 1234567890 --clear-final-url
531
534
  # Responsive search ad
532
535
  baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
533
536
  --headlines "Fast Widgets,Buy Online,Free Shipping" --descriptions "Best widgets around.,Ships tomorrow." \
@@ -5011,6 +5014,10 @@ This CLI is designed for AI agent consumption. Key patterns:
5011
5014
 
5012
5015
  - **0.177.0**: `baker ga4` can change what a property measures, not only report on it. Five staged write families — `key-event` (create|update|delete), `custom-dimension` and `custom-metric` (create|update|archive), `custom-event` (create|update|delete), `data-retention set` — plus `baker ga4 config` to read the property's current configuration and `baker ga4 draft` (list|show|amend|remove|clear) to review the staged set. `custom-event` is GA4's "Create event" rule: build a new event out of one the site already sends (a `form_submit` on `/quote` becomes `quote_request`), then mark it a key event so Google Ads imports it — the loop that previously ended with "do this by hand in Analytics". Staging follows Tag Manager: nothing is sent during the chat, the user reviews the whole set in the dashboard's **Analytics** tab, and it applies when they complete the chat. Unlike Tag Manager there is no version left over — an applied change is in effect at once, and there is no undo, so removals ask for confirmation first. Everything Google would reject is refused at stage time against a fresh read of the property rather than discovered at publish: a duplicate `eventName` or `parameterName` (including against another change staged on the same chat), an immutable field in an update, a currency metric with no restricted type, a retention window past 14 months on a standard property, a target the property does not have. Marking an event Google fires by itself (`page_view`, `scroll`, …) as a key event is a **warning**, not a refusal — it is legal, it just inflates conversions and poisons Smart Bidding. The one thing that is *not* a duplicate is the replacement of something the same chat has already staged the removal of: ops apply in stage order, so `custom-dimension archive 12` followed by a create on the same `parameterName` works, which is the only way to change a field GA4 marks immutable. Every `create` also takes a JSON array and stages the list in one request, because each stage costs a full read of the property. One chat stages changes for one property. **The OAuth scope changed:** Baker now requests `analytics.edit`, so every Google Analytics connection made before this is read-only until it is reconnected; the write commands refuse up front with that as the fix, and `baker ga4 config` reports `canWrite`. Reporting is unaffected. Not covered, and reported as such by `baker capabilities`: property create/rename/merge, data streams, the Google Ads link, and audiences.
5013
5016
  - **0.169.0**: `baker capabilities` — one call, before any platform work, that answers what Baker **cannot** do. Everything else (`baker schema`, the family docs, `--help`) describes what it can, so the only way to learn a capability was absent was to try it, usually after telling the user it would happen. Per surface (`google-ads`, `meta-ads`, `linkedin-ads`, `x-ads`, `tag-manager`, `ga4`, `gsc`) it reports: what is connected and which accounts/properties/containers were picked; whether writes are **live** or **simulated** for this company, and that Tag Manager has neither mode — completing the chat changes the real container; and a `limits` list separating *not built yet* from *the platform forbids it* from *we hold no permission for it*, because each leads to something different to tell the user. Naming one surface (`baker capabilities google-ads`) also returns `ops`: every change with its required and optional fields and the exact values each accepts — so "is there a field to demote a conversion action, and how is it spelled?" is one call instead of four guesses. Almost everything is derived rather than written down: the accounts and permissions come from the connection, the write mode from the company's gate, the writable entities and field contracts from the same schemas the backend parses with, and read-only vs read-write from the OAuth scopes the app actually requests (which is why "GA4 is read-only" is a fact, not a sentence). The handful of hand-written entries each carry a machine-checkable claim about the code behind them, and a coverage test fails when one stops holding. A rejected staged write on Google, Meta or LinkedIn now points at this command instead of leaving the agent to guess another field name.
5017
+ - **0.178.0**: `baker ads google keywords add --final-url` is now described and nudged as the exception it is. A keyword-level final URL overrides the ad's landing page for every click on that keyword, and the flag applies to every keyword in the call — so one URL pasted across a batch restates the ad's own destination on each row while making the change review read like a per-keyword routing decision. The flag's description says to leave it unset unless the destination is keyword-specific, and staging with it returns a hint: on a batch, that the shared page belongs on the ad (`ads create --final-url`) and the keywords should be re-staged without it; on a single keyword, to keep it only where that keyword's destination differs. The review surface changed to match — a staged keyword's final URL is now shown in full on its row in the change card (it used to be visible only on a keyword *update*), so a URL that adds nothing is visible to the user rather than buried.
5018
+ - **0.179.0**: `baker ads google keywords update --final-url` / `--clear-final-url` — changing where one keyword sends its clicks no longer costs the keyword its history. The write contract has always accepted `finalUrls` on a keyword update (`baker capabilities google-ads` reported it as a field of `google.keyword.update`), but no CLI flag reached it, so an agent that followed capabilities got "update needs at least one field" and the only route left was remove + re-add — a new criterion to Google, learning from zero, for a change that was only ever about the destination. `--clear-final-url` stages the empty set Google reads as "inherit the ad's landing page", which is the undo for the misuse `--final-url` already warns about. Same nudge as `add` on the way in.
5019
+
5020
+ A keyword's final URL now also shows on its row when the keyword is being **removed**. 0.178.0 read it from the stage-time snapshot, but the snapshot never asked Google for it, so the removal half of that change silently did nothing — the card said which keyword was going away but not which landing page it had been sending its clicks to. The batch hint says outright that `--final-url` has no per-keyword form rather than implying destinations could differ within one call, and the tool doc now lists each `keywords` subcommand's own flags instead of one shared set (`--ad-group-ref`, `--text` and `--match-type` are `add`-only; `remove` takes a target and nothing else).
5014
5021
 
5015
5022
  ## Publishing
5016
5023
 
package/dist/cli.js CHANGED
@@ -2892,6 +2892,7 @@ var historyCategorySchema = z9.enum([
2892
2892
  "creative",
2893
2893
  "report",
2894
2894
  "domain",
2895
+ "hosting",
2895
2896
  "integration",
2896
2897
  "tag_manager"
2897
2898
  ]);
@@ -9962,6 +9963,29 @@ function keywordKillHints(status) {
9962
9963
  const kills = typeof status === "string" && ["PAUSED", "REMOVED"].includes(status.toUpperCase());
9963
9964
  return kills ? KEYWORD_KILL_HINTS : [];
9964
9965
  }
9966
+ function keywordFinalUrlHints(finalUrl, count) {
9967
+ if (typeof finalUrl !== "string" || finalUrl.length === 0) {
9968
+ return [];
9969
+ }
9970
+ return count > 1 ? [
9971
+ `--final-url applied the same landing page to all ${count} keywords \u2014 the flag has no per-keyword form. A keyword-level URL only earns its place when that keyword must land somewhere the ad doesn't, so one URL shared by every keyword belongs on the ad instead: set the ad's final URL to this page and re-stage the keywords without --final-url. Keep it only if this page is genuinely each keyword's own destination and not the ad's.`
9972
+ ] : [
9973
+ "Keep --final-url only if this keyword's destination differs from the ad's final URL \u2014 otherwise drop it and let the keyword inherit the ad's landing page."
9974
+ ];
9975
+ }
9976
+ function keywordUpdateFinalUrls(args) {
9977
+ const finalUrl = args["final-url"];
9978
+ const clear = args["clear-final-url"] === true;
9979
+ if (typeof finalUrl !== "string" || finalUrl.length === 0) {
9980
+ return clear ? [] : void 0;
9981
+ }
9982
+ if (clear) {
9983
+ failWriteValidation(
9984
+ "--clear-final-url sends the keyword back to the ad's landing page \u2014 pass it OR --final-url, not both"
9985
+ );
9986
+ }
9987
+ return [finalUrl];
9988
+ }
9965
9989
  var keywordWriteSubcommands = {
9966
9990
  add: defineCommand30({
9967
9991
  meta: { name: "add", description: "Add keyword(s) to an ad group \u2014 one --text or a whole batch" },
@@ -9975,33 +9999,51 @@ var keywordWriteSubcommands = {
9975
9999
  file: { type: "string", description: "Text file with one keyword[:MATCH_TYPE] per line" },
9976
10000
  "match-type": { type: "string", description: "Default match type: EXACT | PHRASE | BROAD" },
9977
10001
  "cpc-bid": { type: "string", description: "Keyword CPC bid in major units (applies to all)" },
9978
- "final-url": { type: "string", description: "Keyword-level final URL (applies to all)" }
10002
+ "final-url": {
10003
+ type: "string",
10004
+ description: "Rarely needed: send THIS keyword to a page the ad doesn't go to. Applies to every keyword in the call, so leave it unset unless the destination is keyword-specific \u2014 otherwise set the ad's final URL"
10005
+ }
9979
10006
  },
9980
10007
  run: async ({ args }) => {
9981
10008
  const customerId = requireCustomerId(args);
9982
10009
  const adGroup = requireStringFlag(args["ad-group-ref"], "--ad-group-ref");
9983
10010
  const cpcBidMicros = microsFlag(args["cpc-bid"], "--cpc-bid");
9984
10011
  const finalUrls = args["final-url"] ? [args["final-url"]] : void 0;
10012
+ const entries = keywordEntries(args);
9985
10013
  await stageGoogleOps(
9986
- keywordEntries(args).map((entry) => ({
10014
+ entries.map((entry) => ({
9987
10015
  kind: "google.keyword.add",
9988
10016
  customerId,
9989
10017
  payload: { adGroup, text: entry.text, matchType: entry.matchType, cpcBidMicros, finalUrls }
9990
- }))
10018
+ })),
10019
+ keywordFinalUrlHints(args["final-url"], entries.length)
9991
10020
  );
9992
10021
  }
9993
10022
  }),
9994
10023
  update: defineCommand30({
9995
10024
  meta: { name: "update", description: "Update a keyword" },
9996
- args: { ...customerIdArg, "cpc-bid": { type: "string" }, status: { type: "string" } },
10025
+ args: {
10026
+ ...customerIdArg,
10027
+ "cpc-bid": { type: "string" },
10028
+ status: { type: "string" },
10029
+ "final-url": {
10030
+ type: "string",
10031
+ description: "Rarely needed: send THIS keyword to a page the ad doesn't go to. Leave unset unless the destination is keyword-specific \u2014 otherwise set the ad's final URL"
10032
+ },
10033
+ "clear-final-url": {
10034
+ type: "boolean",
10035
+ description: "Drop this keyword's own landing page so it inherits the ad's again"
10036
+ }
10037
+ },
9997
10038
  run: async ({ args }) => {
9998
10039
  const customerId = requireCustomerId(args);
10040
+ const finalUrls = keywordUpdateFinalUrls(args);
9999
10041
  await stageUpdate(
10000
10042
  "google.keyword.update",
10001
10043
  customerId,
10002
10044
  requireTarget(args, "keyword"),
10003
- { cpcBidMicros: microsFlag(args["cpc-bid"], "--cpc-bid"), status: args.status },
10004
- keywordKillHints(args.status)
10045
+ { cpcBidMicros: microsFlag(args["cpc-bid"], "--cpc-bid"), status: args.status, finalUrls },
10046
+ [...keywordKillHints(args.status), ...keywordFinalUrlHints(args["final-url"], 1)]
10005
10047
  );
10006
10048
  }
10007
10049
  }),