@adkit/cli 1.13.25 → 1.13.26

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.
Files changed (2) hide show
  1. package/dist/cli.js +215 -191
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -25,14 +25,6 @@ function readStringProperty(value, key) {
25
25
  var GOOGLE_LIMITED_AD_APPROVAL_STATUSES = ["APPROVED_LIMITED", "AREA_OF_INTEREST_ONLY"];
26
26
  var GOOGLE_PROBLEM_AD_APPROVAL_STATUSES = ["DISAPPROVED", ...GOOGLE_LIMITED_AD_APPROVAL_STATUSES];
27
27
 
28
- // ../shared/dist/manage/google/campaign-validation.js
29
- var VALID_GOOGLE_BID_STRATEGIES = ["manual_cpc", "manual_cpm", "maximize_clicks", "maximize_conversions", "maximize_conversion_value", "target_spend", "target_impression_share"];
30
- var SUPPORTED_GOOGLE_CAMPAIGN_TYPES = ["search", "display"];
31
- var validGoogleBidStrategySet = new Set(VALID_GOOGLE_BID_STRATEGIES);
32
- var validGoogleBidStrategyList = VALID_GOOGLE_BID_STRATEGIES.join(", ");
33
- var supportedGoogleCampaignTypeSet = new Set(SUPPORTED_GOOGLE_CAMPAIGN_TYPES);
34
- var supportedGoogleCampaignTypeList = SUPPORTED_GOOGLE_CAMPAIGN_TYPES.join(", ");
35
-
36
28
  // ../shared/dist/manage/google/validators/conversion-goals.js
37
29
  var GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES = [
38
30
  "add_to_cart",
@@ -60,6 +52,188 @@ var GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES = [
60
52
  ];
61
53
  var googleCampaignConversionGoalCategorySet = new Set(GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES);
62
54
 
55
+ // ../shared/dist/manage/google/validators/campaigns.js
56
+ var VALID_GOOGLE_BID_STRATEGIES = ["manual_cpc", "manual_cpm", "maximize_clicks", "maximize_conversions", "maximize_conversion_value", "target_spend", "target_impression_share"];
57
+ var SUPPORTED_GOOGLE_CAMPAIGN_TYPES = ["search", "display", "demand_gen"];
58
+ var GOOGLE_DEMAND_GEN_BID_STRATEGIES = ["maximize_clicks", "maximize_conversions", "maximize_conversion_value"];
59
+ var GOOGLE_DEMAND_GEN_BID_STRATEGY_LIST = GOOGLE_DEMAND_GEN_BID_STRATEGIES.join(", ");
60
+ var validGoogleBidStrategySet = new Set(VALID_GOOGLE_BID_STRATEGIES);
61
+ var validGoogleBidStrategyList = VALID_GOOGLE_BID_STRATEGIES.join(", ");
62
+ var supportedGoogleCampaignTypeSet = new Set(SUPPORTED_GOOGLE_CAMPAIGN_TYPES);
63
+ var supportedGoogleCampaignTypeList = SUPPORTED_GOOGLE_CAMPAIGN_TYPES.join(", ");
64
+
65
+ // ../shared/dist/manage/google/validators/ad-groups.js
66
+ var GOOGLE_DEMAND_GEN_NETWORKS = ["youtube_in_feed", "youtube_in_stream", "youtube_shorts"];
67
+ var GOOGLE_DEMAND_GEN_NETWORK_LIST = GOOGLE_DEMAND_GEN_NETWORKS.join(", ");
68
+
69
+ // ../shared/dist/manage/google/asset-utils.js
70
+ var SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS = ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
71
+ var STRUCTURED_SNIPPET_HEADER_LOOKUP = new Map(SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS.map((header) => [header.toLowerCase(), header]));
72
+ var DAY_TO_GOOGLE_MAP = {
73
+ monday: "MONDAY",
74
+ tuesday: "TUESDAY",
75
+ wednesday: "WEDNESDAY",
76
+ thursday: "THURSDAY",
77
+ friday: "FRIDAY",
78
+ saturday: "SATURDAY",
79
+ sunday: "SUNDAY"
80
+ };
81
+ var MINUTE_TO_GOOGLE_MAP = {
82
+ 0: "ZERO",
83
+ 15: "FIFTEEN",
84
+ 30: "THIRTY",
85
+ 45: "FORTY_FIVE"
86
+ };
87
+ function normalizeWhitespace(value) {
88
+ return value.trim().replace(/\s+/g, " ");
89
+ }
90
+ function normalizeOptionalText(value) {
91
+ if (typeof value !== "string")
92
+ return void 0;
93
+ const normalized = normalizeWhitespace(value);
94
+ return normalized.length > 0 ? normalized : void 0;
95
+ }
96
+ function normalizeDate(value) {
97
+ const normalized = normalizeOptionalText(value);
98
+ if (!normalized)
99
+ return void 0;
100
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
101
+ throw new Error(`Invalid date "${normalized}". Use YYYY-MM-DD.`);
102
+ const [yearRaw, monthRaw, dayRaw] = normalized.split("-");
103
+ const year = Number.parseInt(yearRaw ?? "", 10);
104
+ const month = Number.parseInt(monthRaw ?? "", 10);
105
+ const day = Number.parseInt(dayRaw ?? "", 10);
106
+ const parsed = new Date(Date.UTC(year, month - 1, day));
107
+ if (Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day)
108
+ throw new Error(`Invalid calendar date "${normalized}". Use a real YYYY-MM-DD date.`);
109
+ return normalized;
110
+ }
111
+ function validateDateRange(startDate, endDate) {
112
+ if (!startDate || !endDate)
113
+ return;
114
+ if (Date.parse(`${startDate}T00:00:00Z`) > Date.parse(`${endDate}T00:00:00Z`))
115
+ throw new Error("startDate must be on or before endDate");
116
+ }
117
+ function normalizeUrl(value) {
118
+ const raw = value.trim();
119
+ if (!raw)
120
+ throw new Error("URL cannot be empty");
121
+ try {
122
+ const url = new URL(raw);
123
+ if (url.protocol !== "http:" && url.protocol !== "https:")
124
+ throw new Error("Only http:// and https:// URLs are allowed");
125
+ return url.toString();
126
+ } catch (error) {
127
+ if (error instanceof Error && error.message === "Only http:// and https:// URLs are allowed")
128
+ throw error;
129
+ throw new Error(`Invalid URL "${value}"`);
130
+ }
131
+ }
132
+ function normalizeScheduleEntry(entry) {
133
+ const { dayOfWeek, startHour, startMinute, endHour, endMinute } = entry;
134
+ if (!(dayOfWeek in DAY_TO_GOOGLE_MAP))
135
+ throw new Error(`Invalid schedule day "${String(dayOfWeek)}"`);
136
+ if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23)
137
+ throw new Error("Schedule startHour must be an integer between 0 and 23");
138
+ if (!Number.isInteger(endHour) || endHour < 0 || endHour > 24)
139
+ throw new Error("Schedule endHour must be an integer between 0 and 24");
140
+ if (!(startMinute in MINUTE_TO_GOOGLE_MAP))
141
+ throw new Error("Schedule startMinute must be one of 0, 15, 30, 45");
142
+ if (!(endMinute in MINUTE_TO_GOOGLE_MAP))
143
+ throw new Error("Schedule endMinute must be one of 0, 15, 30, 45");
144
+ if (endHour === 24 && endMinute !== 0)
145
+ throw new Error("Schedule endMinute must be 0 when endHour is 24");
146
+ const startTotalMinutes = startHour * 60 + startMinute;
147
+ const endTotalMinutes = endHour * 60 + endMinute;
148
+ if (endTotalMinutes <= startTotalMinutes)
149
+ throw new Error("Schedule end must be after start");
150
+ return { dayOfWeek, startHour, startMinute, endHour, endMinute };
151
+ }
152
+ function normalizeSchedule(schedule) {
153
+ if (!schedule)
154
+ return void 0;
155
+ if (!Array.isArray(schedule))
156
+ throw new Error("schedule must be an array");
157
+ if (schedule.length === 0)
158
+ return void 0;
159
+ const normalized = schedule.map(normalizeScheduleEntry).sort((left, right) => {
160
+ const leftKey = `${left.dayOfWeek}:${String(left.startHour).padStart(2, "0")}:${left.startMinute}:${String(left.endHour).padStart(2, "0")}:${left.endMinute}`;
161
+ const rightKey = `${right.dayOfWeek}:${String(right.startHour).padStart(2, "0")}:${right.startMinute}:${String(right.endHour).padStart(2, "0")}:${right.endMinute}`;
162
+ return leftKey.localeCompare(rightKey);
163
+ });
164
+ const perDay = /* @__PURE__ */ new Map();
165
+ for (const entry of normalized) {
166
+ const current = perDay.get(entry.dayOfWeek) ?? 0;
167
+ const next = current + 1;
168
+ if (next > 6)
169
+ throw new Error(`Schedule exceeds Google limit of 6 entries on ${entry.dayOfWeek}`);
170
+ perDay.set(entry.dayOfWeek, next);
171
+ }
172
+ if (normalized.length > 42)
173
+ throw new Error("Schedule exceeds Google limit of 42 total entries");
174
+ return normalized;
175
+ }
176
+ function normalizeSitelinkAsset(asset) {
177
+ const linkText = normalizeOptionalText(asset.linkText);
178
+ if (!linkText)
179
+ throw new Error("Sitelink linkText is required");
180
+ if (linkText.length > 25)
181
+ throw new Error("Sitelink linkText must be between 1 and 25 characters");
182
+ if (!Array.isArray(asset.finalUrls) || asset.finalUrls.length === 0)
183
+ throw new Error("Sitelink finalUrls must contain at least one URL");
184
+ const finalUrls = asset.finalUrls.map(normalizeUrl);
185
+ const description1 = normalizeOptionalText(asset.description1);
186
+ const description2 = normalizeOptionalText(asset.description2);
187
+ if (description1 && !description2 || !description1 && description2)
188
+ throw new Error("Sitelink description1 and description2 must either both be set or both be omitted");
189
+ if (description1 && description1.length > 35)
190
+ throw new Error("Sitelink description1 must be between 1 and 35 characters");
191
+ if (description2 && description2.length > 35)
192
+ throw new Error("Sitelink description2 must be between 1 and 35 characters");
193
+ const startDate = normalizeDate(asset.startDate);
194
+ const endDate = normalizeDate(asset.endDate);
195
+ validateDateRange(startDate, endDate);
196
+ const urlCustomParameters = asset.urlCustomParameters?.map((parameter) => {
197
+ const key = normalizeOptionalText(parameter.key);
198
+ const value = normalizeOptionalText(parameter.value);
199
+ if (!key || !value)
200
+ throw new Error("urlCustomParameters require non-empty key and value");
201
+ return { key, value };
202
+ }) ?? void 0;
203
+ return {
204
+ type: "sitelink",
205
+ linkText,
206
+ finalUrls,
207
+ description1,
208
+ description2,
209
+ startDate,
210
+ endDate,
211
+ schedule: normalizeSchedule(asset.schedule),
212
+ trackingUrlTemplate: normalizeOptionalText(asset.trackingUrlTemplate),
213
+ finalUrlSuffix: normalizeOptionalText(asset.finalUrlSuffix),
214
+ urlCustomParameters: urlCustomParameters && urlCustomParameters.length > 0 ? urlCustomParameters : void 0
215
+ };
216
+ }
217
+ function parseGoogleAssetReference(value) {
218
+ if (!value.startsWith("asset:"))
219
+ return null;
220
+ const assetId = value.slice("asset:".length).trim();
221
+ if (!/^\d+$/.test(assetId))
222
+ throw new Error(`Invalid asset reference "${value}". Expected asset:<numeric-id>.`);
223
+ return assetId;
224
+ }
225
+ function parseGoogleSitelinkShorthand(value) {
226
+ const separatorCount = value.split("|").length - 1;
227
+ if (separatorCount !== 1)
228
+ throw new Error('Invalid sitelink shorthand. Expected exactly one "|" in "text|url".');
229
+ const [rawText, rawUrl] = value.split("|");
230
+ const linkText = normalizeOptionalText(rawText);
231
+ if (!linkText)
232
+ throw new Error("Invalid sitelink shorthand. Text cannot be empty.");
233
+ const finalUrl = normalizeUrl(rawUrl ?? "");
234
+ return normalizeSitelinkAsset({ type: "sitelink", linkText, finalUrls: [finalUrl] });
235
+ }
236
+
63
237
  // ../shared/dist/manage/google/types/google-adkit.js
64
238
  var GOOGLE_GEO_LOCATION_SEARCH_TYPES = ["country", "region", "city", "postal_code", "metro"];
65
239
 
@@ -1820,174 +1994,6 @@ function getAd(client, args, flags) {
1820
1994
  return getMetaEntity(client, args, flags, { path: "ads", usageHint: "Run: adkit manage meta ads <ad-id>" });
1821
1995
  }
1822
1996
 
1823
- // ../shared/dist/manage/google/asset-utils.js
1824
- var SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS = ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
1825
- var STRUCTURED_SNIPPET_HEADER_LOOKUP = new Map(SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS.map((header) => [header.toLowerCase(), header]));
1826
- var DAY_TO_GOOGLE_MAP = {
1827
- monday: "MONDAY",
1828
- tuesday: "TUESDAY",
1829
- wednesday: "WEDNESDAY",
1830
- thursday: "THURSDAY",
1831
- friday: "FRIDAY",
1832
- saturday: "SATURDAY",
1833
- sunday: "SUNDAY"
1834
- };
1835
- var MINUTE_TO_GOOGLE_MAP = {
1836
- 0: "ZERO",
1837
- 15: "FIFTEEN",
1838
- 30: "THIRTY",
1839
- 45: "FORTY_FIVE"
1840
- };
1841
- function normalizeWhitespace(value) {
1842
- return value.trim().replace(/\s+/g, " ");
1843
- }
1844
- function normalizeOptionalText(value) {
1845
- if (typeof value !== "string")
1846
- return void 0;
1847
- const normalized = normalizeWhitespace(value);
1848
- return normalized.length > 0 ? normalized : void 0;
1849
- }
1850
- function normalizeDate(value) {
1851
- const normalized = normalizeOptionalText(value);
1852
- if (!normalized)
1853
- return void 0;
1854
- if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
1855
- throw new Error(`Invalid date "${normalized}". Use YYYY-MM-DD.`);
1856
- const [yearRaw, monthRaw, dayRaw] = normalized.split("-");
1857
- const year = Number.parseInt(yearRaw ?? "", 10);
1858
- const month = Number.parseInt(monthRaw ?? "", 10);
1859
- const day = Number.parseInt(dayRaw ?? "", 10);
1860
- const parsed = new Date(Date.UTC(year, month - 1, day));
1861
- if (Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day)
1862
- throw new Error(`Invalid calendar date "${normalized}". Use a real YYYY-MM-DD date.`);
1863
- return normalized;
1864
- }
1865
- function validateDateRange(startDate, endDate) {
1866
- if (!startDate || !endDate)
1867
- return;
1868
- if (Date.parse(`${startDate}T00:00:00Z`) > Date.parse(`${endDate}T00:00:00Z`))
1869
- throw new Error("startDate must be on or before endDate");
1870
- }
1871
- function normalizeUrl(value) {
1872
- const raw = value.trim();
1873
- if (!raw)
1874
- throw new Error("URL cannot be empty");
1875
- try {
1876
- const url = new URL(raw);
1877
- if (url.protocol !== "http:" && url.protocol !== "https:")
1878
- throw new Error("Only http:// and https:// URLs are allowed");
1879
- return url.toString();
1880
- } catch (error) {
1881
- if (error instanceof Error && error.message === "Only http:// and https:// URLs are allowed")
1882
- throw error;
1883
- throw new Error(`Invalid URL "${value}"`);
1884
- }
1885
- }
1886
- function normalizeScheduleEntry(entry) {
1887
- const { dayOfWeek, startHour, startMinute, endHour, endMinute } = entry;
1888
- if (!(dayOfWeek in DAY_TO_GOOGLE_MAP))
1889
- throw new Error(`Invalid schedule day "${String(dayOfWeek)}"`);
1890
- if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23)
1891
- throw new Error("Schedule startHour must be an integer between 0 and 23");
1892
- if (!Number.isInteger(endHour) || endHour < 0 || endHour > 24)
1893
- throw new Error("Schedule endHour must be an integer between 0 and 24");
1894
- if (!(startMinute in MINUTE_TO_GOOGLE_MAP))
1895
- throw new Error("Schedule startMinute must be one of 0, 15, 30, 45");
1896
- if (!(endMinute in MINUTE_TO_GOOGLE_MAP))
1897
- throw new Error("Schedule endMinute must be one of 0, 15, 30, 45");
1898
- if (endHour === 24 && endMinute !== 0)
1899
- throw new Error("Schedule endMinute must be 0 when endHour is 24");
1900
- const startTotalMinutes = startHour * 60 + startMinute;
1901
- const endTotalMinutes = endHour * 60 + endMinute;
1902
- if (endTotalMinutes <= startTotalMinutes)
1903
- throw new Error("Schedule end must be after start");
1904
- return { dayOfWeek, startHour, startMinute, endHour, endMinute };
1905
- }
1906
- function normalizeSchedule(schedule) {
1907
- if (!schedule)
1908
- return void 0;
1909
- if (!Array.isArray(schedule))
1910
- throw new Error("schedule must be an array");
1911
- if (schedule.length === 0)
1912
- return void 0;
1913
- const normalized = schedule.map(normalizeScheduleEntry).sort((left, right) => {
1914
- const leftKey = `${left.dayOfWeek}:${String(left.startHour).padStart(2, "0")}:${left.startMinute}:${String(left.endHour).padStart(2, "0")}:${left.endMinute}`;
1915
- const rightKey = `${right.dayOfWeek}:${String(right.startHour).padStart(2, "0")}:${right.startMinute}:${String(right.endHour).padStart(2, "0")}:${right.endMinute}`;
1916
- return leftKey.localeCompare(rightKey);
1917
- });
1918
- const perDay = /* @__PURE__ */ new Map();
1919
- for (const entry of normalized) {
1920
- const current = perDay.get(entry.dayOfWeek) ?? 0;
1921
- const next = current + 1;
1922
- if (next > 6)
1923
- throw new Error(`Schedule exceeds Google limit of 6 entries on ${entry.dayOfWeek}`);
1924
- perDay.set(entry.dayOfWeek, next);
1925
- }
1926
- if (normalized.length > 42)
1927
- throw new Error("Schedule exceeds Google limit of 42 total entries");
1928
- return normalized;
1929
- }
1930
- function normalizeSitelinkAsset(asset) {
1931
- const linkText = normalizeOptionalText(asset.linkText);
1932
- if (!linkText)
1933
- throw new Error("Sitelink linkText is required");
1934
- if (linkText.length > 25)
1935
- throw new Error("Sitelink linkText must be between 1 and 25 characters");
1936
- if (!Array.isArray(asset.finalUrls) || asset.finalUrls.length === 0)
1937
- throw new Error("Sitelink finalUrls must contain at least one URL");
1938
- const finalUrls = asset.finalUrls.map(normalizeUrl);
1939
- const description1 = normalizeOptionalText(asset.description1);
1940
- const description2 = normalizeOptionalText(asset.description2);
1941
- if (description1 && !description2 || !description1 && description2)
1942
- throw new Error("Sitelink description1 and description2 must either both be set or both be omitted");
1943
- if (description1 && description1.length > 35)
1944
- throw new Error("Sitelink description1 must be between 1 and 35 characters");
1945
- if (description2 && description2.length > 35)
1946
- throw new Error("Sitelink description2 must be between 1 and 35 characters");
1947
- const startDate = normalizeDate(asset.startDate);
1948
- const endDate = normalizeDate(asset.endDate);
1949
- validateDateRange(startDate, endDate);
1950
- const urlCustomParameters = asset.urlCustomParameters?.map((parameter) => {
1951
- const key = normalizeOptionalText(parameter.key);
1952
- const value = normalizeOptionalText(parameter.value);
1953
- if (!key || !value)
1954
- throw new Error("urlCustomParameters require non-empty key and value");
1955
- return { key, value };
1956
- }) ?? void 0;
1957
- return {
1958
- type: "sitelink",
1959
- linkText,
1960
- finalUrls,
1961
- description1,
1962
- description2,
1963
- startDate,
1964
- endDate,
1965
- schedule: normalizeSchedule(asset.schedule),
1966
- trackingUrlTemplate: normalizeOptionalText(asset.trackingUrlTemplate),
1967
- finalUrlSuffix: normalizeOptionalText(asset.finalUrlSuffix),
1968
- urlCustomParameters: urlCustomParameters && urlCustomParameters.length > 0 ? urlCustomParameters : void 0
1969
- };
1970
- }
1971
- function parseGoogleAssetReference(value) {
1972
- if (!value.startsWith("asset:"))
1973
- return null;
1974
- const assetId = value.slice("asset:".length).trim();
1975
- if (!/^\d+$/.test(assetId))
1976
- throw new Error(`Invalid asset reference "${value}". Expected asset:<numeric-id>.`);
1977
- return assetId;
1978
- }
1979
- function parseGoogleSitelinkShorthand(value) {
1980
- const separatorCount = value.split("|").length - 1;
1981
- if (separatorCount !== 1)
1982
- throw new Error('Invalid sitelink shorthand. Expected exactly one "|" in "text|url".');
1983
- const [rawText, rawUrl] = value.split("|");
1984
- const linkText = normalizeOptionalText(rawText);
1985
- if (!linkText)
1986
- throw new Error("Invalid sitelink shorthand. Text cannot be empty.");
1987
- const finalUrl = normalizeUrl(rawUrl ?? "");
1988
- return normalizeSitelinkAsset({ type: "sitelink", linkText, finalUrls: [finalUrl] });
1989
- }
1990
-
1991
1997
  // src/commands/google.ts
1992
1998
  import { readFileSync as readFileSync2 } from "node:fs";
1993
1999
  import { basename, extname, resolve } from "node:path";
@@ -2769,14 +2775,16 @@ async function getAd2(client, args, flags) {
2769
2775
  async function createAd2(client, _args, flags) {
2770
2776
  validateFlags(flags, AD_FLAGS2, "manage google ads create");
2771
2777
  if (typeof flags.data === "string") {
2772
- const body2 = parseDataFlag(flags, "Check JSON syntax in --data");
2773
- const qs2 = buildGoogleMutationQuery(flags, body2);
2778
+ const rawBody2 = parseDataFlag(flags, "Check JSON syntax in --data");
2779
+ const qs2 = buildGoogleMutationQuery(flags, rawBody2);
2780
+ const body2 = removeQueryOnlyBodyKeys(rawBody2, ["accountId"]);
2774
2781
  const path4 = `/manage/google/ads${qs2}`;
2775
2782
  return client.post(path4, body2);
2776
2783
  }
2777
2784
  const payload = buildAdPayload(flags);
2778
- const body = { ads: [payload] };
2779
- const qs = buildGoogleMutationQuery(flags, body);
2785
+ const rawBody = { ads: [payload] };
2786
+ const qs = buildGoogleMutationQuery(flags, rawBody);
2787
+ const body = removeQueryOnlyBodyKeys(rawBody, ["accountId"]);
2780
2788
  const path3 = `/manage/google/ads${qs}`;
2781
2789
  return client.post(path3, body);
2782
2790
  }
@@ -2784,19 +2792,21 @@ async function updateAd2(client, args, flags) {
2784
2792
  const id = requireArg(args, 0, "ad-id", "Run: adkit manage google ads update <ad-id> --status paused --ad-group 123456");
2785
2793
  validateFlags(flags, AD_FLAGS2, "manage google ads update");
2786
2794
  if (typeof flags.data === "string") {
2787
- const body = parseDataFlag(flags, "Check JSON syntax in --data");
2788
- const adGroupId2 = resolveGoogleAdGroupId(body, flags);
2795
+ const rawBody = parseDataFlag(flags, "Check JSON syntax in --data");
2796
+ const adGroupId2 = resolveGoogleAdGroupId(rawBody, flags);
2789
2797
  if (!adGroupId2) throw new CliError("MISSING_FLAG", "Missing required flag: `--ad-group`", "Provide --ad-group <id> or include adGroupId in --data");
2790
- const qs2 = buildGoogleMutationQuery(flags, body, { adGroupId: adGroupId2 });
2798
+ const qs2 = buildGoogleMutationQuery(flags, rawBody, { adGroupId: adGroupId2 });
2799
+ const body2 = removeQueryOnlyBodyKeys(rawBody, ["accountId", "adGroupId"]);
2791
2800
  const path4 = `/manage/google/ads/${id}${qs2}`;
2792
- return client.patch(path4, body);
2801
+ return client.patch(path4, body2);
2793
2802
  }
2794
2803
  const payload = buildAdPayload(flags);
2795
2804
  const adGroupId = resolveGoogleAdGroupId(payload, flags);
2796
2805
  if (!adGroupId) throw new CliError("MISSING_FLAG", "Missing required flag: `--ad-group`", "Run: adkit manage google ads update <ad-id> --ad-group <ad-group-id> ...");
2797
2806
  const qs = buildGoogleMutationQuery(flags, payload, { adGroupId });
2807
+ const body = removeQueryOnlyBodyKeys(payload, ["accountId", "adGroupId"]);
2798
2808
  const path3 = `/manage/google/ads/${id}${qs}`;
2799
- return client.patch(path3, payload);
2809
+ return client.patch(path3, body);
2800
2810
  }
2801
2811
  async function deleteAd2(client, args, flags) {
2802
2812
  validateFlags(flags, ["ad-group"], "manage google ads delete");
@@ -5499,7 +5509,7 @@ Flags (create):
5499
5509
  --name <name> Campaign name (required)
5500
5510
  --status <s> enabled, paused (default: paused)
5501
5511
  --budget-daily <n> Daily budget in account currency
5502
- --campaign-type <type> search, display, performance_max (performance_max needs --data for asset groups)
5512
+ --campaign-type <type> search, display, demand_gen, performance_max (performance_max needs --data for asset groups)
5503
5513
  --bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
5504
5514
  --target-cpa <n> Target CPA in account currency (with --bid-strategy maximize_conversions)
5505
5515
  --target-roas <n> Target ROAS ratio, e.g. 3.5 = 350% (with --bid-strategy maximize_conversion_value)
@@ -5527,6 +5537,7 @@ Note:
5527
5537
  For cities, regions, postal codes, and metros, run google geo-locations search and use the returned bare code.
5528
5538
  Advanced geo examples: adkit manage google campaigns --help full
5529
5539
  Display creation starts here: campaigns create --campaign-type display, then create Display ad groups and responsive_display ads.
5540
+ Demand Gen uses two calls: create the campaign, then create its ad group with the returned campaign ID. Geo and YouTube networks belong on the ad group.
5530
5541
  Performance Max: campaignType "performance_max" with asset groups \u2014 build it with --data. See --help full.
5531
5542
  conversion-goals chooses what to optimize toward; bid-strategy controls bidding. It works on Search, Display, and Performance Max.
5532
5543
  On create, omit conversion-goals to inherit account defaults. On update, omit it to leave goals unchanged or pass null to restore defaults.
@@ -5538,6 +5549,7 @@ Examples:
5538
5549
  adkit manage google campaigns list --account 1234567890
5539
5550
  adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
5540
5551
  adkit manage google campaigns create --name "Purchase Search" --campaign-type search --budget-daily 40 --bid-strategy maximize_conversion_value --conversion-goals purchase --account 1234567890
5552
+ adkit manage google campaigns create --name "YouTube Launch" --campaign-type demand_gen --budget-daily 40 --bid-strategy maximize_conversions --account 1234567890
5541
5553
  adkit manage google campaigns create --data '{"campaigns":[{"name":"Display US","campaignType":"display","budget":{"daily":50},"targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]}}}]}' --account 1234567890
5542
5554
  adkit manage google campaigns update 987654321 --data '{"targeting":{"geoLocations":{"match":"presence_or_interest"}}}' --account 1234567890
5543
5555
  adkit manage google campaigns update 987654321 --status paused --account 1234567890
@@ -5557,7 +5569,7 @@ Flags (create):
5557
5569
  --name <name> Campaign name (required)
5558
5570
  --status <s> enabled, paused (default: paused)
5559
5571
  --budget-daily <n> Daily budget in account currency
5560
- --campaign-type <type> search, display, performance_max (performance_max needs --data for asset groups)
5572
+ --campaign-type <type> search, display, demand_gen, performance_max (performance_max needs --data for asset groups)
5561
5573
  --bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
5562
5574
  --conversion-goals <list> Comma-separated goal categories or conversion-action IDs; never mix them
5563
5575
  ${FLAG.account}
@@ -5588,6 +5600,7 @@ Notes:
5588
5600
  platformLocation accepts exact Google geo objects for advanced criteria and existing raw values.
5589
5601
  Google exclusions support named locations. Proximity circles belong in include.
5590
5602
  Display campaign creation uses campaignType:"display" or --campaign-type display. Videos are not supported for Display media in v1.
5603
+ Demand Gen: create campaignType:"demand_gen" first. Then use the returned campaign ID in a separate ad-groups create call with targeting.geoLocations and targeting.inventory.networks.
5591
5604
  Performance Max: campaignType "performance_max" requires at least one assetGroups[] entry and supports multiple groups. Text assets are inline ({role,text}); upload images with google media first, then reference their id/resourceName. Needs an enabled conversion (publish is blocked without one). Non-retail only. Build it with --data.
5592
5605
  conversion-goals works on Search, Display, and Performance Max. It chooses what to optimize toward; bid-strategy controls bidding.
5593
5606
  Valid goal categories: ${GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORY_LIST}
@@ -5600,6 +5613,7 @@ Examples:
5600
5613
  adkit manage google campaigns list --account 1234567890
5601
5614
  adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
5602
5615
  adkit manage google campaigns create --name "Purchase Search" --campaign-type search --budget-daily 40 --bid-strategy maximize_conversion_value --conversion-goals purchase --account 1234567890
5616
+ adkit manage google campaigns create --name "YouTube Launch" --campaign-type demand_gen --budget-daily 40 --bid-strategy maximize_conversions --account 1234567890
5603
5617
  adkit manage google campaigns create --data '{"campaigns":[{"name":"Display US","campaignType":"display","budget":{"daily":50},"targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]}}}]}' --account 1234567890
5604
5618
  adkit manage google campaigns create --data '{"campaigns":[{"name":"Austin Search","campaignType":"search","budget":{"daily":5},"targeting":{"geoLocations":{"include":[{"type":"city","code":"1026201"}]}}}]}' --account 1234567890
5605
5619
  adkit manage google campaigns create --data '{"campaigns":[{"name":"SF Radius","campaignType":"search","budget":{"daily":5},"targeting":{"geoLocations":{"include":[{"type":"platformLocation","platform":"google","value":{"proximity":{"geo_point":{"latitude_in_micro_degrees":37774900,"longitude_in_micro_degrees":-122419400},"radius":5,"radius_units":"MILES"}}}]}}}]}' --account 1234567890
@@ -5683,6 +5697,7 @@ Flags (list):
5683
5697
 
5684
5698
  Note:
5685
5699
  Display targeting is created or updated with --data using targeting.audience.interests, targeting.audience.customAudiences, targeting.content.topics, and targeting.content.websites.
5700
+ Demand Gen creation uses --data with targeting.geoLocations and targeting.inventory.networks (youtube_in_feed, youtube_in_stream, youtube_shorts). These fields are create-only.
5686
5701
  Topic and interest search return IDs used in Display targeting: adkit manage google research topics <query>, adkit manage google research interests <query>.
5687
5702
  WARNING: On update, targeting replaces the full Display targeting set. If you send only one new website, old websites/topics/audiences can be removed.
5688
5703
  list returns ad group configuration only (name, status, bid).
@@ -5692,6 +5707,7 @@ Examples:
5692
5707
  adkit manage google ad-groups list --account 1234567890 --campaign 987654321
5693
5708
  adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
5694
5709
  adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Display SaaS","targeting":{"audience":{"interests":{"include":["804"]},"customAudiences":{"include":["123456789"]}},"content":{"topics":{"include":["3"]},"websites":{"include":["https://example.com"],"exclude":["https://bad.example.com"]}}}}]}' --account 1234567890
5710
+ adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"YouTube US","targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]},"inventory":{"networks":["youtube_in_feed","youtube_in_stream","youtube_shorts"]}}}]}' --account 1234567890
5695
5711
  adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890
5696
5712
  adkit manage google ad-groups update 555666777 --status paused --account 1234567890
5697
5713
  adkit manage google ad-groups delete 555666777 --account 1234567890
@@ -5727,6 +5743,7 @@ Notes:
5727
5743
  Topic and interest search return IDs used in targeting: research topics -> targeting.content.topics; research interests -> targeting.audience.interests.
5728
5744
  Websites use direct URLs in targeting.content.websites.include/exclude. Bare numeric customAudiences IDs are treated as Google user lists (remarketing/Customer Match); for custom segments, pass a customAudiences/{id} or customers/{accountId}/customAudiences/{id} resource path.
5729
5745
  WARNING: On update, targeting replaces the full Display targeting set. If you send only one new website, old websites/topics/audiences can be removed.
5746
+ Demand Gen creation uses targeting.geoLocations plus targeting.inventory.networks (youtube_in_feed, youtube_in_stream, youtube_shorts). Omit targeting.inventory for Google's default channels. Demand Gen geo/inventory updates are not supported yet.
5730
5747
  list returns ad group configuration only (name, status, bid).
5731
5748
  For spend/clicks/conversions: adkit manage google results
5732
5749
 
@@ -5735,6 +5752,7 @@ Examples:
5735
5752
  adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
5736
5753
  adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Brand Terms","cpcBid":2}]}' --account 1234567890
5737
5754
  adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Display SaaS","targeting":{"audience":{"interests":{"include":["804"]},"customAudiences":{"include":["123456789"]}},"content":{"topics":{"include":["3"]},"websites":{"include":["https://example.com"],"exclude":["https://bad.example.com"]}}}}]}' --account 1234567890
5755
+ adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"YouTube US","targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]},"inventory":{"networks":["youtube_in_feed","youtube_in_stream","youtube_shorts"]}}}]}' --account 1234567890
5738
5756
  adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890 --publish
5739
5757
  adkit manage google ad-groups update 555666777 --status paused --account 1234567890
5740
5758
  adkit manage google ad-groups delete 555666777 --account 1234567890 --publish`;
@@ -5754,7 +5772,7 @@ Flags (create/update):
5754
5772
  --sitelink <value> Repeatable. Use asset:<id> or "text|url"
5755
5773
  --final-url <url> Landing page URL
5756
5774
  --path <text> Optional display URL path as segment-one[/segment-two]
5757
- --status <s> enabled, paused, removed
5775
+ --status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
5758
5776
 
5759
5777
  Flags (list):
5760
5778
  --campaign <ids> Filter by Google campaign IDs (platformId, comma-separated)
@@ -5765,8 +5783,10 @@ Flags (list):
5765
5783
  --offset <n> Pagination offset
5766
5784
 
5767
5785
  Notes:
5768
- Google enforces strict character limits: each headline max 30 chars, each description max 90 chars, long headline max 90 chars, each path segment max 15 chars.
5786
+ Google enforces strict character limits: Search/Display headlines max 30 chars, Demand Gen video headlines max 40 chars, descriptions and long headlines max 90 chars, business name max 25 chars, and each path segment max 15 chars.
5769
5787
  update is a partial object update. If --data sends an array field, it replaces that whole array; omitted arrays stay unchanged.
5788
+ Demand Gen video ads use --data with adType:"demand_gen_video_responsive" and creative:{ headlines, longHeadlines, descriptions, businessName, finalUrls, media }.
5789
+ First run google media upload with the videoId from the YouTube URL. Then run google ads create with the returned video asset ID and an existing Google logo asset ID.
5770
5790
  Display ads use --data with type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
5771
5791
  Display media roles: marketing_image, square_marketing_image, logo, square_logo.
5772
5792
  Upload images first with google media upload, then use returned Google asset id/resourceName in media[].id.
@@ -5779,6 +5799,7 @@ Notes:
5779
5799
  Examples:
5780
5800
  adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
5781
5801
  adkit manage google ads list --account 1234567890 --ad-group 555666777
5802
+ adkit manage google ads create --data '{"ads":[{"adType":"demand_gen_video_responsive","adGroupId":"555666777","creative":{"headlines":[{"text":"Make every launch count"}],"longHeadlines":[{"text":"Turn your next product launch into lasting demand"}],"descriptions":[{"text":"Reach the right buyers with a focused product story."}],"businessName":"AdKit","finalUrls":["https://example.com/demand-gen"],"media":[{"role":"video","id":"customers/1234567890/assets/501"},{"role":"logo","id":"customers/1234567890/assets/502"}]},"status":"paused"}]}' --account 1234567890
5782
5803
  adkit manage google ads create --data '{"ads":[{"type":"responsive_display","adGroupId":"555666777","headlines":[{"text":"Display headline"}],"longHeadline":{"text":"A longer Display headline"},"descriptions":[{"text":"Display description"}],"businessName":"AdKit","finalUrls":["https://example.com"],"media":[{"role":"marketing_image","id":"customers/123/assets/111"},{"role":"square_marketing_image","id":"customers/123/assets/222"},{"role":"logo","id":"customers/123/assets/333"},{"role":"square_logo","id":"customers/123/assets/444"}]}]}' --account 1234567890
5783
5804
  adkit manage google ads create --ad-group 555666777 \\
5784
5805
  --headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
@@ -5807,7 +5828,7 @@ Flags (create/update):
5807
5828
  --sitelink <value> Repeatable. Use asset:<id> or "text|url"
5808
5829
  --final-url <url> Landing page URL
5809
5830
  --path <text> Optional display URL path as segment-one[/segment-two]
5810
- --status <s> enabled, paused, removed
5831
+ --status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
5811
5832
  ${FLAG.account}
5812
5833
  ${FLAG.publish}
5813
5834
  ${FLAG.data}
@@ -5821,8 +5842,10 @@ Flags (list):
5821
5842
  --offset <n> Pagination offset
5822
5843
 
5823
5844
  Notes:
5824
- Google enforces strict character limits: each headline max 30 chars, each description max 90 chars, long headline max 90 chars, each path segment max 15 chars.
5845
+ Google enforces strict character limits: Search/Display headlines max 30 chars, Demand Gen video headlines max 40 chars, descriptions and long headlines max 90 chars, business name max 25 chars, and each path segment max 15 chars.
5825
5846
  update is a partial object update. If --data sends an array field, it replaces that whole array; omitted arrays stay unchanged.
5847
+ Demand Gen video ads use --data with adType:"demand_gen_video_responsive" and creative:{ headlines, longHeadlines, descriptions, businessName, finalUrls, media }.
5848
+ First run google media upload with the videoId from the YouTube URL. Then run google ads create with the returned video asset ID and an existing Google logo asset ID.
5826
5849
  Display ads use type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
5827
5850
  Display media roles: marketing_image, square_marketing_image, logo, square_logo.
5828
5851
  For Display updates, omitted media is preserved. If --data includes media, that media array replaces all Display media.
@@ -5837,6 +5860,7 @@ Notes:
5837
5860
  Examples:
5838
5861
  adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
5839
5862
  adkit manage google ads list --account 1234567890 --ad-group 555666777
5863
+ adkit manage google ads create --data '{"ads":[{"adType":"demand_gen_video_responsive","adGroupId":"555666777","creative":{"headlines":[{"text":"Make every launch count"}],"longHeadlines":[{"text":"Turn your next product launch into lasting demand"}],"descriptions":[{"text":"Reach the right buyers with a focused product story."}],"businessName":"AdKit","finalUrls":["https://example.com/demand-gen"],"media":[{"role":"video","id":"customers/1234567890/assets/501"},{"role":"logo","id":"customers/1234567890/assets/502"}]},"status":"paused"}]}' --account 1234567890
5840
5864
  adkit manage google ads create --data '{"ads":[{"type":"responsive_display","adGroupId":"555666777","headlines":[{"text":"Display headline"}],"longHeadline":{"text":"A longer Display headline"},"descriptions":[{"text":"Display description"}],"businessName":"AdKit","finalUrls":["https://example.com"],"media":[{"role":"marketing_image","id":"customers/123/assets/111"},{"role":"square_marketing_image","id":"customers/123/assets/222"},{"role":"logo","id":"customers/123/assets/333"},{"role":"square_logo","id":"customers/123/assets/444"}]}]}' --account 1234567890
5841
5865
  adkit manage google ads create --ad-group 555666777 \\
5842
5866
  --headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adkit/cli",
3
- "version": "1.13.25",
3
+ "version": "1.13.26",
4
4
  "description": "The Ads CLI for AI agents — manage Meta & Google ad campaigns, browse the ad library, and generate creatives from your terminal.",
5
5
  "keywords": [
6
6
  "ads cli",