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.
- package/dist/adRotationUpdate.d.ts +3 -0
- package/dist/adRotationUpdate.js +30 -0
- package/dist/assetHelpers.d.ts +62 -0
- package/dist/assetHelpers.js +123 -0
- package/dist/backfillDefaults.d.ts +19 -0
- package/dist/backfillDefaults.js +57 -0
- package/dist/biddingUpdate.d.ts +18 -0
- package/dist/biddingUpdate.js +102 -0
- package/dist/build-info.json +2 -2
- package/dist/campaignBuilder.js +10 -1
- package/dist/claudeLabel.d.ts +35 -0
- package/dist/claudeLabel.js +37 -0
- package/dist/gaqlSharedSetGuard.d.ts +18 -0
- package/dist/gaqlSharedSetGuard.js +11 -0
- package/dist/index.d.ts +206 -50
- package/dist/index.js +663 -83
- package/dist/resourceNames.d.ts +1 -0
- package/dist/resourceNames.js +5 -1
- package/dist/tools.js +186 -3
- package/dist/validateDemandGenAd.d.ts +2 -0
- package/dist/validateDemandGenAd.js +3 -0
- package/dist/validateRsa.d.ts +11 -0
- package/dist/validateRsa.js +3 -2
- package/dist/writeGate.js +8 -1
- package/package.json +5 -4
|
@@ -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
|
package/dist/assetHelpers.d.ts
CHANGED
|
@@ -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;
|
package/dist/assetHelpers.js
CHANGED
|
@@ -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
|
package/dist/build-info.json
CHANGED
package/dist/campaignBuilder.js
CHANGED
|
@@ -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
|