mcp-google-ads 1.8.0 → 1.11.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.
@@ -0,0 +1,3 @@
1
+ export declare const AD_SERVING_OPTIMIZATION_STATUS: Record<string, number>;
2
+ export declare const AD_SERVING_OPTIMIZATION_STATUS_ENUM_TO_NAME: Record<number, string>;
3
+ export declare function buildAdRotationCampaignUpdate(resourceName: string, mode: string): Record<string, any>;
@@ -0,0 +1,30 @@
1
+ const AD_SERVING_OPTIMIZATION_STATUS = {
2
+ OPTIMIZE: 2,
3
+ CONVERSION_OPTIMIZE: 3,
4
+ ROTATE: 4,
5
+ ROTATE_INDEFINITELY: 5
6
+ };
7
+ const AD_SERVING_OPTIMIZATION_STATUS_ENUM_TO_NAME = {
8
+ 2: "OPTIMIZE",
9
+ 3: "CONVERSION_OPTIMIZE",
10
+ 4: "ROTATE",
11
+ 5: "ROTATE_INDEFINITELY"
12
+ };
13
+ function buildAdRotationCampaignUpdate(resourceName, mode) {
14
+ const statusEnum = AD_SERVING_OPTIMIZATION_STATUS[mode];
15
+ if (statusEnum === void 0) {
16
+ throw new Error(
17
+ `Unsupported ad rotation mode: ${mode}. Expected one of ${Object.keys(AD_SERVING_OPTIMIZATION_STATUS).join(", ")}`
18
+ );
19
+ }
20
+ return {
21
+ resource_name: resourceName,
22
+ ad_serving_optimization_status: statusEnum
23
+ };
24
+ }
25
+ export {
26
+ AD_SERVING_OPTIMIZATION_STATUS,
27
+ AD_SERVING_OPTIMIZATION_STATUS_ENUM_TO_NAME,
28
+ buildAdRotationCampaignUpdate
29
+ };
30
+ //# sourceMappingURL=adRotationUpdate.js.map
@@ -95,4 +95,66 @@ export declare function normalizeReplaceSitelinkArgs(raw: Record<string, unknown
95
95
  error: string;
96
96
  };
97
97
  export declare function buildReplaceSitelinkDryRun(args: ReplaceSitelinkArgs): ReplaceSitelinkDryRun;
98
+ export interface AssetLinkCandidate {
99
+ resource_name: string;
100
+ attach_to: string;
101
+ parent_status: string;
102
+ }
103
+ export interface PartitionedAssetLinks<T extends AssetLinkCandidate = AssetLinkCandidate> {
104
+ active: T[];
105
+ skippedRemoved: T[];
106
+ }
107
+ export declare function partitionAssetLinksByParentStatus<T extends AssetLinkCandidate>(rows: T[]): PartitionedAssetLinks<T>;
108
+ export interface CreateCalloutArgs {
109
+ customer_id?: string;
110
+ callout_text: string;
111
+ confirm?: boolean;
112
+ }
113
+ export interface CreateCalloutDryRun {
114
+ dry_run: true;
115
+ message: string;
116
+ customer_id: string;
117
+ callout_text: string;
118
+ }
119
+ export declare function normalizeCreateCalloutArgs(raw: Record<string, unknown> | undefined): CreateCalloutArgs | {
120
+ error: string;
121
+ };
122
+ export declare function buildCreateCalloutDryRun(args: CreateCalloutArgs): CreateCalloutDryRun;
123
+ export declare const STRUCTURED_SNIPPET_HEADERS: readonly ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
124
+ export interface CreateStructuredSnippetArgs {
125
+ customer_id?: string;
126
+ header: string;
127
+ values: string[];
128
+ confirm?: boolean;
129
+ }
130
+ export interface CreateStructuredSnippetDryRun {
131
+ dry_run: true;
132
+ message: string;
133
+ customer_id: string;
134
+ header: string;
135
+ values: string[];
136
+ }
137
+ export declare function normalizeCreateStructuredSnippetArgs(raw: Record<string, unknown> | undefined): CreateStructuredSnippetArgs | {
138
+ error: string;
139
+ };
140
+ export declare function buildCreateStructuredSnippetDryRun(args: CreateStructuredSnippetArgs): CreateStructuredSnippetDryRun;
141
+ export declare const CUSTOMER_ASSET_FIELD_TYPES: readonly ["SITELINK", "CALLOUT", "STRUCTURED_SNIPPET"];
142
+ export type CustomerAssetFieldType = (typeof CUSTOMER_ASSET_FIELD_TYPES)[number];
143
+ export interface LinkAssetToCustomerArgs {
144
+ customer_id?: string;
145
+ asset_id: string;
146
+ field_type: CustomerAssetFieldType;
147
+ confirm?: boolean;
148
+ }
149
+ export interface LinkAssetToCustomerDryRun {
150
+ dry_run: true;
151
+ message: string;
152
+ customer_id: string;
153
+ asset_id: string;
154
+ field_type: CustomerAssetFieldType;
155
+ }
156
+ export declare function normalizeLinkAssetToCustomerArgs(raw: Record<string, unknown> | undefined): LinkAssetToCustomerArgs | {
157
+ error: string;
158
+ };
159
+ export declare function buildLinkAssetToCustomerDryRun(args: LinkAssetToCustomerArgs): LinkAssetToCustomerDryRun;
98
160
  export declare function buildPauseLinksDryRun(args: PauseAssetLinksArgs): PauseLinksDryRun;
@@ -188,6 +188,120 @@ function buildReplaceSitelinkDryRun(args) {
188
188
  warning: "Will create a NEW sitelink asset, re-link every campaign/ad-group/customer link currently pointing at old_asset_id, then remove the old links. The old Asset itself is NOT deleted and can still be re-used manually."
189
189
  };
190
190
  }
191
+ function partitionAssetLinksByParentStatus(rows) {
192
+ const active = [];
193
+ const skippedRemoved = [];
194
+ for (const r of rows) {
195
+ (r.parent_status === "REMOVED" ? skippedRemoved : active).push(r);
196
+ }
197
+ return { active, skippedRemoved };
198
+ }
199
+ function normalizeCreateCalloutArgs(raw) {
200
+ const r = raw ?? {};
201
+ const customer_id = typeof r.customer_id === "string" ? r.customer_id : void 0;
202
+ const calloutText = validateSitelinkText(r.callout_text, "callout_text", 25);
203
+ if (!calloutText.ok) return { error: calloutText.error };
204
+ return {
205
+ customer_id,
206
+ callout_text: calloutText.value,
207
+ confirm: r.confirm === true || r.confirm === "true"
208
+ };
209
+ }
210
+ function buildCreateCalloutDryRun(args) {
211
+ return {
212
+ dry_run: true,
213
+ message: "DRY RUN. Nothing created. Pass confirm: true to actually create the callout asset.",
214
+ customer_id: args.customer_id ?? "",
215
+ callout_text: args.callout_text
216
+ };
217
+ }
218
+ const STRUCTURED_SNIPPET_HEADERS = [
219
+ "Amenities",
220
+ "Brands",
221
+ "Courses",
222
+ "Degree programs",
223
+ "Destinations",
224
+ "Featured hotels",
225
+ "Insurance coverage",
226
+ "Models",
227
+ "Neighborhoods",
228
+ "Service catalog",
229
+ "Shows",
230
+ "Styles",
231
+ "Types"
232
+ ];
233
+ const STRUCTURED_SNIPPET_HEADERS_BY_LOWER = new Map(
234
+ STRUCTURED_SNIPPET_HEADERS.map((h) => [h.toLowerCase(), h])
235
+ );
236
+ function normalizeCreateStructuredSnippetArgs(raw) {
237
+ const r = raw ?? {};
238
+ const customer_id = typeof r.customer_id === "string" ? r.customer_id : void 0;
239
+ if (typeof r.header !== "string" || !r.header.trim()) {
240
+ return { error: "header must be a non-empty string" };
241
+ }
242
+ const canonicalHeader = STRUCTURED_SNIPPET_HEADERS_BY_LOWER.get(r.header.trim().toLowerCase());
243
+ if (!canonicalHeader) {
244
+ return {
245
+ error: `Unrecognized structured-snippet header "${r.header}". Must be one of: ${STRUCTURED_SNIPPET_HEADERS.join(", ")}.`
246
+ };
247
+ }
248
+ const rawValues = coerceArray(r.values);
249
+ if (!rawValues || rawValues.length < 3 || rawValues.length > 10) {
250
+ return { error: `values must be an array of 3-10 strings (got ${rawValues?.length ?? 0})` };
251
+ }
252
+ const values = [];
253
+ for (const v of rawValues) {
254
+ const validated = validateSitelinkText(v, "values entry", 25);
255
+ if (!validated.ok) return { error: validated.error };
256
+ values.push(validated.value);
257
+ }
258
+ return {
259
+ customer_id,
260
+ header: canonicalHeader,
261
+ values,
262
+ confirm: r.confirm === true || r.confirm === "true"
263
+ };
264
+ }
265
+ function buildCreateStructuredSnippetDryRun(args) {
266
+ return {
267
+ dry_run: true,
268
+ message: "DRY RUN. Nothing created. Pass confirm: true to actually create the structured snippet asset.",
269
+ customer_id: args.customer_id ?? "",
270
+ header: args.header,
271
+ values: args.values
272
+ };
273
+ }
274
+ const CUSTOMER_ASSET_FIELD_TYPES = ["SITELINK", "CALLOUT", "STRUCTURED_SNIPPET"];
275
+ function normalizeLinkAssetToCustomerArgs(raw) {
276
+ const r = raw ?? {};
277
+ const customer_id = typeof r.customer_id === "string" ? r.customer_id : void 0;
278
+ const assetIdRaw = typeof r.asset_id === "string" ? r.asset_id.trim() : typeof r.asset_id === "number" ? String(r.asset_id) : "";
279
+ if (!assetIdRaw || !/^\d+$/.test(assetIdRaw)) {
280
+ return { error: `invalid asset_id: ${JSON.stringify(r.asset_id)}. Expected numeric asset ID.` };
281
+ }
282
+ if (typeof r.field_type !== "string" || !r.field_type.trim()) {
283
+ return { error: "field_type must be a non-empty string" };
284
+ }
285
+ const fieldType = r.field_type.trim().toUpperCase();
286
+ if (!CUSTOMER_ASSET_FIELD_TYPES.includes(fieldType)) {
287
+ return { error: `Unsupported field_type: "${r.field_type}". Supported values: ${CUSTOMER_ASSET_FIELD_TYPES.join(", ")}` };
288
+ }
289
+ return {
290
+ customer_id,
291
+ asset_id: assetIdRaw,
292
+ field_type: fieldType,
293
+ confirm: r.confirm === true || r.confirm === "true"
294
+ };
295
+ }
296
+ function buildLinkAssetToCustomerDryRun(args) {
297
+ return {
298
+ dry_run: true,
299
+ message: "DRY RUN. Nothing linked. Pass confirm: true to actually link the asset at the customer (account) level.",
300
+ customer_id: args.customer_id ?? "",
301
+ asset_id: args.asset_id,
302
+ field_type: args.field_type
303
+ };
304
+ }
191
305
  function buildPauseLinksDryRun(args) {
192
306
  const would = {
193
307
  customer_asset: [],
@@ -208,15 +322,24 @@ function buildPauseLinksDryRun(args) {
208
322
  };
209
323
  }
210
324
  export {
325
+ CUSTOMER_ASSET_FIELD_TYPES,
326
+ STRUCTURED_SNIPPET_HEADERS,
327
+ buildCreateCalloutDryRun,
211
328
  buildCreateSitelinkDryRun,
329
+ buildCreateStructuredSnippetDryRun,
330
+ buildLinkAssetToCustomerDryRun,
212
331
  buildPauseLinksDryRun,
213
332
  buildReplaceSitelinkDryRun,
214
333
  buildUpdateUrlsDryRun,
334
+ normalizeCreateCalloutArgs,
215
335
  normalizeCreateSitelinkArgs,
336
+ normalizeCreateStructuredSnippetArgs,
337
+ normalizeLinkAssetToCustomerArgs,
216
338
  normalizePauseAssetLinksArgs,
217
339
  normalizeReplaceSitelinkArgs,
218
340
  normalizeUpdateAssetUrlsArgs,
219
341
  parseAssetLinkResourceName,
342
+ partitionAssetLinksByParentStatus,
220
343
  validateFinalUrls
221
344
  };
222
345
  //# sourceMappingURL=assetHelpers.js.map
@@ -0,0 +1,19 @@
1
+ /** Extract the selected field paths from a GAQL query's SELECT clause. */
2
+ export declare function extractSelectedFields(query: string): string[];
3
+ /**
4
+ * Resolve a dotted GAQL field path (e.g. "ad_group.optimized_targeting_enabled")
5
+ * to its primitive type tag (e.g. "BOOL"). Returns undefined if the path can't
6
+ * be resolved, or if it resolves to a nested message/enum object rather than a
7
+ * scalar leaf -- callers only act on an exact string type match.
8
+ */
9
+ export declare function resolveFieldType(fieldPath: string): string | undefined;
10
+ /**
11
+ * Google Ads' REST API omits fields at their proto3 default value (e.g.
12
+ * `false`) entirely from the JSON body -- our tool then serializes the row
13
+ * with no key at all, indistinguishable from "not returned"/error. This
14
+ * backfills an explicit `false` for any selected BOOL leaf field missing from
15
+ * a row whose parent object IS present, so callers can trust that an absent
16
+ * boolean key never means "unknown" -- only a truly absent parent resource
17
+ * (e.g. a metrics-only row) is left untouched, never fabricated.
18
+ */
19
+ export declare function backfillOmittedBooleans<T = any>(query: string, rows: T[]): T[];
@@ -0,0 +1,57 @@
1
+ import { parse as circJsonParse } from "circ-json";
2
+ import fieldsMod from "google-ads-api/build/src/protos/autogen/fields.js";
3
+ let cachedFieldDataTypes = null;
4
+ function getFieldDataTypes() {
5
+ if (!cachedFieldDataTypes) {
6
+ cachedFieldDataTypes = circJsonParse(fieldsMod.fieldDataTypes);
7
+ }
8
+ return cachedFieldDataTypes;
9
+ }
10
+ function snakeToPascal(snake) {
11
+ return snake.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
12
+ }
13
+ function extractSelectedFields(query) {
14
+ const match = query.match(/SELECT\s+(.*?)\s+FROM\s/is);
15
+ if (!match) return [];
16
+ return match[1].split(",").map((f) => f.trim()).filter(Boolean);
17
+ }
18
+ function resolveFieldType(fieldPath) {
19
+ const segments = fieldPath.split(".");
20
+ if (segments.length < 2) return void 0;
21
+ const types = getFieldDataTypes();
22
+ let current = types[snakeToPascal(segments[0])];
23
+ for (let i = 1; i < segments.length && current !== void 0; i++) {
24
+ if (typeof current !== "object") return void 0;
25
+ current = current[segments[i]];
26
+ }
27
+ return typeof current === "string" ? current : void 0;
28
+ }
29
+ function backfillOmittedBooleans(query, rows) {
30
+ const boolPaths = extractSelectedFields(query).filter((f) => resolveFieldType(f) === "BOOL");
31
+ if (boolPaths.length === 0) return rows;
32
+ for (const row of rows) {
33
+ for (const path of boolPaths) {
34
+ const segments = path.split(".");
35
+ const leaf = segments[segments.length - 1];
36
+ let parent = row;
37
+ let reachable = true;
38
+ for (let i = 0; i < segments.length - 1; i++) {
39
+ if (parent == null || typeof parent !== "object") {
40
+ reachable = false;
41
+ break;
42
+ }
43
+ parent = parent[segments[i]];
44
+ }
45
+ if (reachable && parent != null && typeof parent === "object" && !(leaf in parent)) {
46
+ parent[leaf] = false;
47
+ }
48
+ }
49
+ }
50
+ return rows;
51
+ }
52
+ export {
53
+ backfillOmittedBooleans,
54
+ extractSelectedFields,
55
+ resolveFieldType
56
+ };
57
+ //# sourceMappingURL=backfillDefaults.js.map
@@ -0,0 +1,18 @@
1
+ export declare const BIDDING_TYPE_ENUM_TO_NAME: Record<number, string>;
2
+ export declare function wouldDetachPortfolioStrategy(currentBiddingStrategyResource: string | null | undefined): currentBiddingStrategyResource is string;
3
+ export declare class PortfolioDetachBlocked extends Error {
4
+ campaignId: string;
5
+ campaignName: string;
6
+ portfolioStrategyResource: string;
7
+ constructor(campaignId: string, campaignName: string, portfolioStrategyResource: string);
8
+ }
9
+ export declare const DEFAULT_MAGNITUDE_CEILING_PCT = 20;
10
+ export declare function computeMagnitudeDeltaPct(oldValue: number | undefined | null, newValue: number | undefined | null): number | null;
11
+ export declare function exceedsMagnitudeCeiling(oldValue: number | undefined | null, newValue: number | undefined | null, ceilingPct?: number): boolean;
12
+ export declare function isStrategyLooseningChange(oldTargetMicros: number | undefined | null, newTargetMicros: number | undefined | null, oldStrategy: string, newStrategy: string): boolean;
13
+ export interface BiddingUpdateOpts {
14
+ resourceName: string;
15
+ targetCpaMicros?: number;
16
+ targetRoas?: number;
17
+ }
18
+ export declare function buildBiddingCampaignUpdate(strategy: string, opts: BiddingUpdateOpts): Record<string, any>;
@@ -0,0 +1,102 @@
1
+ const BIDDING_TYPE_ENUM_TO_NAME = {
2
+ 3: "MANUAL_CPC",
3
+ 6: "TARGET_CPA",
4
+ 8: "TARGET_ROAS",
5
+ 9: "MAXIMIZE_CLICKS",
6
+ // API type TARGET_SPEND=9, exposed to callers as MAXIMIZE_CLICKS
7
+ 10: "MAXIMIZE_CONVERSIONS",
8
+ 11: "MAXIMIZE_CONVERSION_VALUE"
9
+ };
10
+ function wouldDetachPortfolioStrategy(currentBiddingStrategyResource) {
11
+ return typeof currentBiddingStrategyResource === "string" && currentBiddingStrategyResource.length > 0;
12
+ }
13
+ class PortfolioDetachBlocked extends Error {
14
+ campaignId;
15
+ campaignName;
16
+ portfolioStrategyResource;
17
+ constructor(campaignId, campaignName, portfolioStrategyResource) {
18
+ super(
19
+ `Campaign ${campaignId} ("${campaignName}") is attached to portfolio bid strategy ${portfolioStrategyResource}. This update would silently detach it. Use google_ads_detach_portfolio_bid_strategy instead if you intend to break the attachment.`
20
+ );
21
+ this.name = "PortfolioDetachBlocked";
22
+ this.campaignId = campaignId;
23
+ this.campaignName = campaignName;
24
+ this.portfolioStrategyResource = portfolioStrategyResource;
25
+ }
26
+ }
27
+ const DEFAULT_MAGNITUDE_CEILING_PCT = 20;
28
+ function computeMagnitudeDeltaPct(oldValue, newValue) {
29
+ if (oldValue === void 0 || oldValue === null || oldValue === 0) return null;
30
+ if (newValue === void 0 || newValue === null) return null;
31
+ return (newValue - oldValue) / oldValue * 100;
32
+ }
33
+ function exceedsMagnitudeCeiling(oldValue, newValue, ceilingPct = DEFAULT_MAGNITUDE_CEILING_PCT) {
34
+ const delta = computeMagnitudeDeltaPct(oldValue, newValue);
35
+ if (delta === null) return false;
36
+ return Math.abs(delta) > ceilingPct;
37
+ }
38
+ const TARGET_BASED_STRATEGIES = /* @__PURE__ */ new Set(["TARGET_CPA", "TARGET_ROAS"]);
39
+ function isStrategyLooseningChange(oldTargetMicros, newTargetMicros, oldStrategy, newStrategy) {
40
+ const hadTarget = typeof oldTargetMicros === "number" && oldTargetMicros > 0;
41
+ const hasTarget = typeof newTargetMicros === "number" && newTargetMicros > 0;
42
+ if (hadTarget && !hasTarget) return true;
43
+ if (TARGET_BASED_STRATEGIES.has(oldStrategy) && !TARGET_BASED_STRATEGIES.has(newStrategy)) {
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ function buildBiddingCampaignUpdate(strategy, opts) {
49
+ const { resourceName, targetCpaMicros, targetRoas } = opts;
50
+ const update = { resource_name: resourceName };
51
+ switch (strategy) {
52
+ // NOTE: the google-ads-api client derives the mutate field mask from
53
+ // populated fields and DROPS empty sub-messages ({}). A switch to Maximize
54
+ // Conversions / Maximize Conversion Value with no target must therefore
55
+ // carry an explicit zero sentinel (target_cpa_micros:0 / target_roas:0 ==
56
+ // "no target") so the field is non-empty and the mask includes it. An empty
57
+ // {} silently no-ops: the mutate returns success while the strategy never
58
+ // changes. (Same {target_cpa_micros:0} pattern used in the budget path.)
59
+ case "MAXIMIZE_CONVERSIONS":
60
+ update.maximize_conversions = {
61
+ target_cpa_micros: targetCpaMicros !== void 0 ? targetCpaMicros : 0
62
+ };
63
+ break;
64
+ case "MAXIMIZE_CONVERSION_VALUE":
65
+ update.maximize_conversion_value = {
66
+ target_roas: targetRoas !== void 0 ? targetRoas : 0
67
+ };
68
+ break;
69
+ case "TARGET_CPA":
70
+ if (targetCpaMicros === void 0) {
71
+ throw new Error("TARGET_CPA strategy requires target_cpa_dollars");
72
+ }
73
+ update.target_cpa = { target_cpa_micros: targetCpaMicros };
74
+ break;
75
+ case "TARGET_ROAS":
76
+ if (targetRoas === void 0) {
77
+ throw new Error("TARGET_ROAS strategy requires target_roas");
78
+ }
79
+ update.target_roas = { target_roas: targetRoas };
80
+ break;
81
+ case "MANUAL_CPC":
82
+ update.manual_cpc = {};
83
+ break;
84
+ case "MAXIMIZE_CLICKS":
85
+ update.target_spend = {};
86
+ break;
87
+ default:
88
+ throw new Error(`Unsupported strategy: ${strategy}`);
89
+ }
90
+ return update;
91
+ }
92
+ export {
93
+ BIDDING_TYPE_ENUM_TO_NAME,
94
+ DEFAULT_MAGNITUDE_CEILING_PCT,
95
+ PortfolioDetachBlocked,
96
+ buildBiddingCampaignUpdate,
97
+ computeMagnitudeDeltaPct,
98
+ exceedsMagnitudeCeiling,
99
+ isStrategyLooseningChange,
100
+ wouldDetachPortfolioStrategy
101
+ };
102
+ //# sourceMappingURL=biddingUpdate.js.map
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "b3b7dab",
3
- "builtAt": "2026-07-20T18:35:03.765Z",
2
+ "sha": "8219937",
3
+ "builtAt": "2026-09-09T04:33:00.640Z",
4
4
  "embeddedSecrets": true
5
5
  }
@@ -22,7 +22,15 @@ function buildCampaignCreatePayload(input) {
22
22
  // Required for all new campaigns in API v23+. Must be a non-zero enum value
23
23
  // (proto3 strips default/zero values, so `false`/0 gets omitted and the API
24
24
  // rejects with "required field not present").
25
- contains_eu_political_advertising: enums.EuPoliticalAdvertisingStatus?.DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING ?? 3
25
+ contains_eu_political_advertising: enums.EuPoliticalAdvertisingStatus?.DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING ?? 3,
26
+ // API defaults geo_target_type_setting.positive_geo_target_type to
27
+ // PRESENCE_OR_INTEREST when omitted, which serves ads worldwide to anyone
28
+ // "interested in" the targeted location regardless of correct geo_target_ids.
29
+ // Always PRESENCE — caught 3x in production (2026-07-20, 07-31, 08-01) via
30
+ // next-day monitoring because this field was never set at creation time.
31
+ geo_target_type_setting: {
32
+ positive_geo_target_type: enums.PositiveGeoTargetType.PRESENCE
33
+ }
26
34
  };
27
35
  if (channelType === "DEMAND_GEN") {
28
36
  campaign.network_settings = {
@@ -32,6 +40,7 @@ function buildCampaignCreatePayload(input) {
32
40
  target_partner_search_network: false
33
41
  };
34
42
  campaign.audience_setting = { use_audience_grouped: true };
43
+ campaign.demand_gen_campaign_settings = { upgraded_targeting: false };
35
44
  }
36
45
  if (input.start_date) campaign.start_date = input.start_date;
37
46
  if (input.end_date) campaign.end_date = input.end_date;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Canonical Claude audit-label format for Google Ads entities.
3
+ *
4
+ * GLOBAL rule: every Claude-created/edited campaign, ad group, ad group ad, or
5
+ * ad group criterion is stamped with an audit label so a batch can be found and
6
+ * rolled back. (Only those four resource types have a label surface — budgets,
7
+ * bidding, tracking edits, shared-set negatives and assets do NOT, so a
8
+ * "label every mutation" invariant is unbuildable; this module governs only the
9
+ * label STRING.)
10
+ *
11
+ * Format (locked 2026-07-23): `claude-MM-DD-YY` optionally followed by a
12
+ * kebab-case description — `claude-MM-DD-YY-<desc>`:
13
+ * - `claude-` prefix keeps the `label.name LIKE 'claude-%'` rollback filter,
14
+ * - the 2-digit date isolates a day's batch,
15
+ * - the description (recommended) says what the change was.
16
+ *
17
+ * Single source of truth: the generator, the validator, and the recognizer
18
+ * (used to avoid double-adding the auto label) all agree here, so the format
19
+ * can't drift between the apply path and the rollback queries.
20
+ */
21
+ /** True iff `label` matches the canonical `claude-MM-DD-YY[-desc]` format. */
22
+ export declare function isValidClaudeLabel(label: string): boolean;
23
+ /**
24
+ * Recognizer for the auto-applied label (any date, any/no description). Used to
25
+ * strip it from a caller's extra labels so it isn't added twice. Deliberately
26
+ * looser than the validator on the description (matches any suffix) so it still
27
+ * recognizes a label even if an older/newer descriptor convention was used.
28
+ */
29
+ export declare const AUTO_CLAUDE_LABEL_RE: RegExp;
30
+ /**
31
+ * Build the canonical audit label for `date`, optionally with a `descriptor`.
32
+ * The descriptor is slugged to kebab-case and dropped if it slugs to empty.
33
+ * The output is always a string `isValidClaudeLabel` accepts.
34
+ */
35
+ export declare function claudeAuditLabel(date: Date, descriptor?: string): string;
@@ -0,0 +1,37 @@
1
+ const DATE = "(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])-\\d{2}";
2
+ const DESC = "(?:-[a-z0-9]+(?:-[a-z0-9]+)*)?";
3
+ const VALID_LABEL_RE = new RegExp(`^claude-${DATE}${DESC}$`);
4
+ function isValidClaudeLabel(label) {
5
+ return VALID_LABEL_RE.test(label);
6
+ }
7
+ const AUTO_CLAUDE_LABEL_RE = new RegExp(`^claude-${DATE}(?:-.+)?$`, "i");
8
+ const MAX_LABEL_LEN = 80;
9
+ function slugify(descriptor) {
10
+ const slug = descriptor.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11
+ return slug;
12
+ }
13
+ function boundDescriptor(slug, budget) {
14
+ if (slug.length <= budget) return slug;
15
+ const clipped = slug.slice(0, budget);
16
+ const lastHyphen = clipped.lastIndexOf("-");
17
+ const kept = lastHyphen > 0 ? clipped.slice(0, lastHyphen) : clipped;
18
+ return kept.replace(/-+$/g, "");
19
+ }
20
+ function claudeAuditLabel(date, descriptor) {
21
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
22
+ const dd = String(date.getDate()).padStart(2, "0");
23
+ const yy = String(date.getFullYear()).slice(-2);
24
+ const base = `claude-${mm}-${dd}-${yy}`;
25
+ if (!descriptor) return base;
26
+ let slug = slugify(descriptor);
27
+ if (!slug) return base;
28
+ const budget = MAX_LABEL_LEN - base.length - 1;
29
+ slug = boundDescriptor(slug, budget);
30
+ return slug ? `${base}-${slug}` : base;
31
+ }
32
+ export {
33
+ AUTO_CLAUDE_LABEL_RE,
34
+ claudeAuditLabel,
35
+ isValidClaudeLabel
36
+ };
37
+ //# sourceMappingURL=claudeLabel.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Query-time guard for the campaign_shared_set REMOVED-link trap.
3
+ *
4
+ * GAQL's `campaign_shared_set` resource returns links in every status, including
5
+ * REMOVED. A query that lists a shared list's campaigns WITHOUT filtering
6
+ * `campaign_shared_set.status = 'ENABLED'` therefore shows campaigns the list was
7
+ * UNLINKED from as if it were still attached.
8
+ *
9
+ * Origin: 2026-07-23 — a verify agent read `FROM campaign_shared_set` without the
10
+ * status filter, saw a REMOVED "Compliance Negatives" link, and reported a
11
+ * completed shared-set swap as "incomplete" (nearly triggering 5 needless unlinks).
12
+ * Memory alone can't help a subagent; this warning rides in the tool result every
13
+ * caller sees. Scoped tightly to campaign_shared_set to avoid warning-fatigue.
14
+ *
15
+ * Returns a warning string when the trap is possible, else null. Never blocks —
16
+ * some queries legitimately want removed rows.
17
+ */
18
+ export declare function sharedSetLinkWarning(query: string): string | null;
@@ -0,0 +1,11 @@
1
+ function sharedSetLinkWarning(query) {
2
+ const q = (query || "").toLowerCase();
3
+ if (!/\bfrom\s+campaign_shared_set\b/.test(q)) return null;
4
+ const filtersStatus = /\bstatus\s*=\s*'enabled'/.test(q) || /\bstatus\s*!=\s*'removed'/.test(q) || /\bstatus\s+in\s*\([^)]*'enabled'/.test(q);
5
+ if (filtersStatus) return null;
6
+ return "\u26A0 campaign_shared_set returns REMOVED links too \u2014 this query has no ENABLED-status filter, so lists that were UNLINKED still appear as if attached (this caused a false 'swap incomplete' verdict on 2026-07-23). If you're checking whether a shared set is CURRENTLY attached, add: AND campaign_shared_set.status = 'ENABLED'.";
7
+ }
8
+ export {
9
+ sharedSetLinkWarning
10
+ };
11
+ //# sourceMappingURL=gaqlSharedSetGuard.js.map