Chore/merge upstream - #55
Closed
Wade-Randel wants to merge 50 commits into
Closed
Conversation
This script performs a series of pre-flight checks for the GAds-MCP setup, including OS detection, Homebrew installation, Python version check, Git installation, and network connectivity.
This script installs GAds-MCP (AdLoop) and connects it to Claude, checking prerequisites, installing dependencies, and configuring settings.
Adds the ability to create additional ad groups within existing campaigns, closing the gap where only draft_campaign could create the initial ad group. The new tool follows the same two-step draft/confirm pattern with BROAD match safety checks, keyword validation, and atomic execution via GoogleAdsService.mutate(). https://claude.ai/code/session_01NNbaDSsxSFVeTEPgRWqVVM
…e, CPC bid Three new safety checks before creating an ad group: 1. Reject non-SEARCH campaigns (DISPLAY/SHOPPING) since ad group type is SEARCH_STANDARD 2. Warn if an ad group with the same name already exists (duplicate name confusion) 3. Warn if cpc_bid_micros is set on a Smart Bidding campaign (bid will be ignored) Also consolidates the broad match safety check into the same preflight function, reducing API calls by combining campaign info into a single GAQL query. https://claude.ai/code/session_01NNbaDSsxSFVeTEPgRWqVVM
- Renumber check comments sequentially (1, 2, 3, 4) and align docstring - Fix CLAUDE.md tool count: actual @mcp.tool registrations is 29, not 27 https://claude.ai/code/session_01NNbaDSsxSFVeTEPgRWqVVM
- Use `(kw.get("match_type") or "").upper()` to handle null/None match_type
from MCP/JSON callers instead of raising AttributeError
- Replace blanket `except Exception: pass` in preflight checks with a
warning so users know validations were skipped
- Add tests for null and missing match_type keys
https://claude.ai/code/session_01NNbaDSsxSFVeTEPgRWqVVM
…ufYuh Add draft_ad_group tool for creating ad groups in existing campaigns
…tion
SEARCH campaigns created via draft_campaign now auto-set the Final URL
Suffix with standard UTM parameters using Google Ads ValueTrack macros:
utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_content={adgroupid}&utm_term={keyword}
- Add _DEFAULT_FINAL_URL_SUFFIX constant in write.py
- Add final_url_suffix param to draft_campaign (auto-applied for SEARCH, pass "" to disable)
- Set campaign.final_url_suffix on the proto in _apply_create_campaign
- Add final_url_suffix param to update_campaign for changing/clearing it
- Apply final_url_suffix via field mask in _apply_update_campaign
- Expose final_url_suffix in both MCP tool registrations in server.py
- Update orchestration docs in .claude/rules and .cursor/rules
https://claude.ai/code/session_01SE2vhVvPdaWTGvm2T1mpoF
…x-LO09F Add automatic Final URL Suffix (UTM tracking) to search campaign creation
The Google Ads API does not reliably support in-place updates to RSA headlines/descriptions (IMMUTABLE_FIELD errors are common). This adds a composite tool that automates the industry-standard workaround: create a new RSA with updated copy and pause (or optionally remove) the old one. - _fetch_existing_rsa: GAQL helper to retrieve current ad details - draft_rsa_replacement: validates new copy, shows old-vs-new diff preview - _apply_replace_rsa: execution handler (create new + pause/remove old) - Inherits ad_group_id and final_url from the old ad automatically - remove_old=true triggers double confirmation (default: pause) - 12 unit tests covering validation, inheritance, and safety checks - Orchestration rules added for both .claude/ and .cursor/ rule files https://claude.ai/code/session_01DXyhvDjrZE96YUEUA9ia5c
This tool is for fixing broken ads, not A/B testing. Defaulting to remove_old=True ensures the broken ad cannot be accidentally re-enabled. Docstrings and orchestration rules now clearly distinguish: - draft_rsa_replacement: fix issues with existing ads (removes old) - draft_responsive_search_ad: create new ad variants for testing https://claude.ai/code/session_01DXyhvDjrZE96YUEUA9ia5c
Claude/evaluate rsa editing goz e9
New read-only MCP tools: get_impression_share, get_change_history, get_device_performance, get_location_performance, get_quality_score_details, get_bid_strategy_status, get_budget_pacing, get_ad_schedule_performance, get_auction_insights. Includes 32 tests, orchestration rules, and updated analyze-performance command. https://claude.ai/code/session_01EtMthH7NLzMFcy6hsDUfQt
… command Simplify get_change_history to use a single code path instead of duplicate query construction with redundant date clauses. Remove the unused _change_event_date_clause helper. Add missing conditional tool references (change_history, location, schedule, auction) to the analyze-performance slash command. https://claude.ai/code/session_01EtMthH7NLzMFcy6hsDUfQt
Codex review feedback: - P1: change_date_time is timestamp-based, so BETWEEN with date-only end bound truncates at midnight. Now appends 23:59:59 to bare dates. - P2: change_event has a hard 10,000 row API limit. Now clamps caller- provided limit to 1..10000 before interpolating into the query. https://claude.ai/code/session_01EtMthH7NLzMFcy6hsDUfQt
…-2apH1 Add 9 Google Ads insights tools for performance analysis
- run_ga4_report: add dimension_filter parameter for server-side filtering (e.g. sessionSource=google + sessionMedium=cpc for paid traffic isolation) - get_keyword_performance: add ad_group.id and criterion_id to GAQL SELECT so callers can construct entity_id strings for pause_entity - get_search_terms: add optional campaign_id filter parameter - get_ad_schedule_performance: add conversion_rate enrichment via existing _enrich_conversion_rate function - validate_tracking: add optional customer_id to cross-reference GA4 events with Google Ads conversion actions - Update server.py wrappers, docstrings, and orchestration rules - Add tests for all new functionality (101 tests passing) https://claude.ai/code/session_01Hbuiiebx4iQBDnnUtcknFP
- get_search_terms: add campaign.id to GAQL SELECT so callers can pass it directly to add_negative_keywords without a separate lookup - analyze_campaign_conversions: add campaign_id and conversion_discrepancy_pct fields to each per-campaign row - Update server.py docstrings and orchestration rules - Add tests for crossref tool and search terms campaign.id field Items already done from first round (no changes needed): - get_ad_schedule_performance conversion_rate + CPA (already enriched) - get_keyword_performance ad_group.name (already in GAQL SELECT) https://claude.ai/code/session_01Hbuiiebx4iQBDnnUtcknFP
… docstring - Add _campaign_filter_clause helper that validates campaign_id is numeric before interpolating into GAQL queries (prevents injection) - Fix _enrich_conversion_rate to always set metrics.conversion_rate (0.0 when clicks=0 instead of omitting the field) - Fix server.py docstring: ad_group_id -> ad_group.id to match actual GAQL field names returned - Add tests for invalid campaign_id and zero-click conversion_rate https://claude.ai/code/session_01Hbuiiebx4iQBDnnUtcknFP
…-EclYk Claude/research google ads api ecl yk
…onfusion The phrase "local service businesses" in device and hourly performance tool descriptions was causing Claude to incorrectly associate the MCP with Local Service Ads (LSA). Simplified to "businesses" / "service businesses" to eliminate the ambiguity. https://claude.ai/code/session_01SMZJ8aG5M8u2EX2RJods52
…07YV Remove "local service" phrasing from tool docstrings to prevent LSA c…
Ad URLs were normalized (trailing slash stripped) but GA4 pagePath values were not, causing false "orphaned page" warnings when both had trailing slashes (e.g. /locations/plano-tx/ in ads vs GA4). https://claude.ai/code/session_01B7ykudEJX1LWdMA4ZkbTGY
…ash-dGZCz Fix trailing slash mismatch in landing_page_analysis URL matching
MCP clients (like Claude) don't handle Pydantic's anyOf JSON schemas from
`list[str] | None = None` unions — they serialize arrays/dicts as strings
instead of native types. Changed 10 optional list/dict parameters across 6
tools from `X | None = None` to `X = []` (or `X = {}`) so Pydantic generates
schemas with a direct "type": "array" / "type": "object" field.
For update_campaign's sentinel parameters (geo_target_ids, language_ids),
empty list is converted back to None at the server.py boundary to preserve
the "don't change" semantics in the implementation layer.
https://claude.ai/code/session_01SDbWis2165nVMS1nszVFjm
Fix GA4 tool schema types: replace anyOf unions with direct type fields
Support optional pinning of RSA headlines and descriptions to specific
positions (HEADLINE_1/2/3, DESCRIPTION_1/2) via the Google Ads API
AdTextAsset.pinned_field. Headlines/descriptions now accept either plain
strings (unpinned, backward compatible) or dicts with {"text": "...",
"pinned_to": "HEADLINE_1"}.
Changes:
- gaql.py: _to_python preserves pinned_field on AdTextAsset reads
- write.py: Add _normalize_assets helper, update _validate_rsa for
pinning validation, update draft/apply functions to handle dicts
- server.py: Update tool signatures and docstrings for pinning format
- Rules: Document pinning in orchestration rules (.claude + .cursor)
- Tests: Update RSA replacement tests for normalized dict format
https://claude.ai/code/session_01QX9Uk947P69wPMfeFufQF7
- Fix _apply_create_rsa: use getattr() instead of bracket notation for ServedAssetFieldTypeEnum, matching the pattern used everywhere else in the module. Bracket notation fails on proto-plus enums at runtime. - Fix _to_python: use explicit `is not None` check instead of truthiness for pin_name to avoid silently skipping edge cases. - Improve _format_table/_format_csv: render pinned assets as "Text [HEADLINE_1]" instead of raw dict repr. - Add 15 new tests covering: _normalize_assets, pinning validation (valid/invalid pins, cross-type rejection, missing text), pinned headlines/descriptions in draft plans and diffs. https://claude.ai/code/session_01QX9Uk947P69wPMfeFufQF7
When a dict asset has a non-string text value (e.g. {"text": 123}),
_normalize_assets now coerces it to str instead of passing it through
to _validate_rsa where len() would raise TypeError.
https://claude.ai/code/session_01QX9Uk947P69wPMfeFufQF7
…g-9rHil Claude/research ads api pinning 9r hil
Phase 1 of PMax support — diagnostics only, no write paths yet. - 7 PMax read tools in src/adloop/ads/pmax_read.py: get_pmax_campaigns, get_pmax_channel_breakdown, get_asset_groups, get_asset_group_assets, get_asset_group_signals, get_asset_group_top_combinations, get_pmax_search_terms (v23.2+ campaign_search_term_insight with fallback). - analyze_pmax_performance in crossref.py — single call returns campaign metrics + asset groups + asset performance labels + channel breakdown + GA4 paid sessions, with auto-generated insights[] for POOR/AVERAGE ad strength, LOW assets, channel skew, zero-conversion campaigns, and GDPR consent gaps. - All 8 tools registered in server.py. - Pinned Google Ads API to v24, bumped google-ads dep to >=30.0.0. - Rules file (.cursor/rules/adloop.mdc) extended with Performance Max read-tool inventory, PMax-specific orchestration pattern, GAQL resource reference for asset_group/asset_group_asset/asset_group_signal, example PMax queries, and a marketing best-practices note. - 30 new tests covering each tool's filters, enrichment, error paths, and the cross-ref aggregation. Full suite: 154 passed. https://claude.ai/code/session_019r7TECd9gTVkcUZqmQivwz
- get_pmax_channel_breakdown: docstring claimed it filters with segments.asset_interaction_target.interaction_on_this_asset = false, but the query just uses segments.ad_network_type. Corrected to match actual behavior. - get_pmax_search_terms: docstring claimed a fallback path that doesn't exist in the code (we just return a structured error). Corrected. - analyze_pmax_performance: LOW-asset insight referenced draft_pmax_assets and replace_pmax_asset which don't exist yet (Phase 2/3). Made the message agnostic so users can act on it today. - analyze_pmax_performance: GA4 failures were silently swallowed. Now surfaced as a single warning in insights[] so the user knows why click-to-session and conversion comparisons are missing, while PMax metrics still render. - CLAUDE.md tool count: claimed 37 (was 29 before, also wrong); actual count is 47 @mcp.tool registrations. Pattern count corrected to 16. Tests: 154 passed. https://claude.ai/code/session_019r7TECd9gTVkcUZqmQivwz
When run_ga4_report fails, the per-campaign loop was still falling back to
{"sessions": 0, "conversions": 0} and emitting that as ga4_paid — making
"GA4 unavailable" indistinguishable from "campaign has zero paid sessions"
for any consumer reading the structured fields.
Now ga4_paid is None whenever the GA4 fetch failed (error dict or
exception), and the warning continues to surface in insights[]. Two
regression tests cover both the error-dict and exception paths.
https://claude.ai/code/session_019r7TECd9gTVkcUZqmQivwz
…i-ittO6 Add Performance Max read tools and cross-ref diagnostic
A live MCP test exposed broken queries and a malformed PMax create flow
after the v24 pin. Five of eight PMax read tools failed, draft_campaign's
PMax path was structurally wrong, and confirm_and_apply(dry_run=true)
returned DRY_RUN_SUCCESS for plans the API would reject. This commit
fixes those issues, adds the missing PMax write surface, and adds label
tools that were previously absent.
API v24 read-tool fixes:
- get_pmax_campaigns: drop campaign.url_expansion_opt_out (removed in v24)
- get_asset_group_assets: drop asset_group_asset.performance_label (removed
in v24); return policy_summary.review_status instead. Update docstring
to point users to asset_group_top_combination_view / asset_field_type_view
for per-asset performance.
- get_asset_group_top_combinations: drop metrics.* fields and ORDER BY
metrics.impressions; the resource exposes no metrics in v24.
- get_pmax_search_terms: drop metrics.cost_micros / conversions /
conversions_value; campaign_search_term_insight rejects them with
PROHIBITED_METRIC_IN_SELECT_OR_WHERE_CLAUSE. Surface a `note` so callers
know cost is not available.
- analyze_pmax_performance: replace the LOW-asset insight with a
missing-asset-minimums insight that flags asset groups below Google's
documented PMax minimums (3+ HEADLINE, 1+ LONG_HEADLINE, 2+ DESCRIPTION,
1+ BUSINESS_NAME, 1+ MARKETING_IMAGE / SQUARE_MARKETING_IMAGE / LOGO).
confirm_and_apply dry_run safety:
- dry_run=true now plumbs validate_only=True through each _apply_*
helper to the Google Ads API. The API runs full server-side validation
(PMax network_settings rules, enum values, references, etc.) and
commits nothing. A failing validation now returns
DRY_RUN_VALIDATION_FAILED with the actual API error rather than a
false DRY_RUN_SUCCESS.
PMax write tools (new ads/pmax_write.py):
- draft_pmax_campaign: creates a CampaignBudget + Campaign (no
network_settings, no advertising_channel_sub_type) + geo/language
CampaignCriteria + AssetGroup + every Asset + every AssetGroupAsset
link + every AssetGroupSignal in one bulk mutate. PMax requires this
all-in-one shape per Google's documentation.
- draft_asset_group: adds an asset group + assets + signals to an
existing PMax campaign.
- draft_asset_group_assets: extends an asset group with more headlines /
descriptions / images / videos.
- draft_asset_group_signal: adds one search theme or audience signal.
- Smart-Bidding-only enforcement (rejects MANUAL_CPC and TARGET_SPEND).
- Per-field-type asset minimums and char limits validated before
drafting; image/logo assets must be pre-uploaded resource_names
(binary upload is out of scope for this MCP).
draft_campaign now rejects channel_type=PERFORMANCE_MAX with a clear
error pointing to draft_pmax_campaign — the previous behavior produced
a malformed mutate that the API rejected with four cascading errors.
Asset group support in pause/enable/remove. AssetGroupService is now
wired into the existing _apply_status_change and _apply_remove helpers;
entity_type='asset_group' works for all three.
Label tools (new ads/labels.py):
- list_labels (read), draft_label, apply_label, unapply_label.
- remove_entity entity_type='label' deletes the Label resource itself
(cascades to all assignments).
Rules and docs:
- .cursor/rules/adloop.mdc rewritten for v24 reality:
* removed claims about url_expansion_opt_out and performance_label
* documented the prohibited metrics on campaign_search_term_insight
and asset_group_top_combination_view
* added "Removed in API v24" reference section
* added the new write tools to the inventory table
* added orchestration patterns for "create new PMax campaign", "add
asset group / assets / signals to existing PMax campaign", and
"add labels"
* documented the new validate_only-backed dry_run behavior
- scripts/sync-rules.py run to sync .claude/rules/adloop.md.
- CLAUDE.md tool counts updated (47 -> 55).
Tests: 192 passing (was 159). New test_pmax_write.py covers the four
new draft tools' validation paths and the draft_campaign-rejects-PMax
behavior. New test_labels.py covers list/draft/apply/unapply.
test_pmax_read.py updated for the dropped fields and the new
missing_asset_minimums insight.
https://claude.ai/code/session_018uRgkMdVJKZcfCV6NE2MSP
Findings from a self-review and an independent reviewer pass over the PMax + label changes from 2779ede. Code: - pmax_write.py: drop the `_LIMITS["CALL_TO_ACTION_SELECTION"]` entry (never read) and the unreachable `ASSET_MAXIMUMS` keys for LANDSCAPE_LOGO / PORTRAIT_MARKETING_IMAGE / YOUTUBE_VIDEO (the validator never iterates over them). - pmax_write.py: drop unused `asset_service` and `asset_group_service` locals from `_apply_create_pmax_campaign` — `_build_asset_group_operations` fetches its own. - pmax_write.py: rename `text_field_type` to `field_type_enum` in `_apply_create_asset_group_assets` — it's the same `AssetFieldTypeEnum` used for text, video, image, and logo field types. - pmax_write.py: restructure `_apply_create_asset_group_assets` to emit all `Asset.create` operations first and all `AssetGroupAsset.create` links after, matching `_build_asset_group_operations` and Google's documented "AssetOperations consecutive, before AssetGroupAssets" rule. Image/logo links are now built alongside text/video links instead of in a separate trailing loop. - crossref.py: replace the in-loop `_ASSET_MINIMUMS` literal in `analyze_pmax_performance` with an import from `pmax_write.ASSET_MINIMUMS` — single source of truth, no per-iteration redeclaration. - crossref.py: when `campaign.brand_guidelines_enabled` is true, skip the BUSINESS_NAME and LOGO minimum checks at the asset-group level (those assets live on the campaign in that mode, so flagging them as "missing" is a false positive). - server.py: move the four label tools out from between `draft_asset_group_assets` and `draft_asset_group_signal`. PMax write block now stays contiguous; labels live in their own section header. Tests: - test_pmax_write.py: replace the convoluted `"MANUAL_CPC" not in details or "PMax" in details` assertion with two direct checks on the rejection message. Rules: - .cursor/rules/adloop.mdc (synced to .claude/rules/adloop.md): drop the stale "drill into LOW-labeled assets" instruction (those arrays no longer exist after `performance_label` was removed in v24) and replace with guidance pointing at `asset_counts_by_type`, `missing_asset_minimums`, and `get_asset_group_top_combinations`. - Same file: revise the Performance Max marketing best-practice bullet to drop the stale LOW-asset language, mention the asset-type minimums flagged by `analyze_pmax_performance`, and document explicitly that image/logo upload happens in the Google Ads UI (the MCP only accepts pre-uploaded resource_names). Tests still 192 passing. https://claude.ai/code/session_018uRgkMdVJKZcfCV6NE2MSP
Two validation gaps surfaced by the PR review. P1: BUSINESS_NAME was added to text_groups only when business_name was non-empty, so an omitted business_name silently skipped its minimum check (declared as 1 in ASSET_MINIMUMS). The asset group passed draft validation and the failure was deferred to apply-time API validation. Fix: always include BUSINESS_NAME in text_groups with an empty list when business_name is empty so the minimum check fires. P2: Image/logo lists (marketing_image_assets, square_marketing_image_assets, logo_assets) were validated against ASSET_MINIMUMS but never against ASSET_MAXIMUMS. Drafts could accept oversized lists that the API would later reject. Fix: mirror the text-group pattern — also enforce the per-field-type maximum and emit a clear error when exceeded. Tests: two new regression tests cover both paths (test_requires_business_name_when_omitted, test_rejects_too_many_marketing_images). Full suite 194 passing. https://claude.ai/code/session_018uRgkMdVJKZcfCV6NE2MSP
Add Performance Max write tools and label management
Every Google Ads service-client mutate method was being called with
validate_only as a Python kwarg (e.g. service.mutate_labels(customer_id=cid,
operations=[...], validate_only=True)). The gapic-generated methods do not
accept validate_only as a kwarg — it is a field on the request proto. This
raised a TypeError before any network round-trip, blocking the entire write
surface (draft_pmax_campaign, draft_asset_group, draft_asset_group_assets,
draft_asset_group_signal, draft_label, apply_label, unapply_label, plus the
older Search write paths and remove/status-change calls).
Fix: rewrite all 29 mutate call sites across write.py, pmax_write.py, and
labels.py to use the request={...} dict form, which the gapic client coerces
into the appropriate request proto with validate_only set as a field.
Also tighten the DRY_RUN_VALIDATION_FAILED message in confirm_and_apply to
distinguish a real Google Ads API rejection from an internal Python error
that never reached the API — the previous message ("Google Ads rejected the
plan") was misleading when the failure was a server-side bug.
https://claude.ai/code/session_013NS9Ru7b2RSFVpA8vKxhed
…BqQux Refactor Google Ads API calls to use request dict parameter
The Google Ads API rejects asset_group creates that are missing any of MARKETING_IMAGE / SQUARE_MARKETING_IMAGE / LOGO with ASSET_GROUP_NOT_ENOUGH_*_ASSET, so PMax campaigns cannot be drafted text-only and patched later. draft_image_asset closes the gap by letting users upload local JPG/PNG/GIF files (≤5 MB, magic-byte checked, batch upload supported) through the same draft → confirm_and_apply flow as every other write tool. The apply helper reads bytes fresh at apply time and feeds AssetService.MutateAssets, returning resource_names ready for draft_pmax_campaign / draft_asset_group / draft_asset_group_assets. Also tightens the missing-asset error messages in the existing PMax draft tools to name draft_image_asset as the upload path, and updates the orchestration rules with a dedicated upload pattern. https://claude.ai/code/session_01WjDCCdg7BRfa6HZR4uUi2X
…l removal
Three production bugs surfaced by Cowork's end-to-end PMax test in
Round 3 are now closed on the same branch as draft_image_asset.
1. WhichOneof proto-plus method-vs-field bug. proto-plus's wrapper of
MutateOperationResponse does not always expose WhichOneof as a
callable method — calling it raised "Unknown field for
MutateOperationResponse: WhichOneof" *after* the mutate had
succeeded. The asset group existed on Google's side but the apply
pipeline reported a failure, so a retry would create a silent
duplicate. Fixed by dropping to the underlying protobuf via
type(resp).pb(resp).WhichOneof("response") in all five apply
helpers (search create_campaign, search create_ad_group, PMax
create_pmax_campaign, create_asset_group, create_asset_group_assets).
2. Brand Guidelines required CampaignAsset links. New PMax campaigns
default to brand_guidelines_enabled=True on Google's side and the
API rejects the create mutate with
REQUIRED_BUSINESS_NAME_ASSET_NOT_LINKED /
REQUIRED_LOGO_ASSET_NOT_LINKED unless the business name and a logo
are linked at the *campaign* level, not just on the asset group.
draft_pmax_campaign now defaults brand_guidelines_enabled=True to
match Google's default and auto-generates the two CampaignAsset
create operations (BUSINESS_NAME from the inline text asset, LOGO
from the first pre-uploaded logo) in the same atomic mutate. Pass
brand_guidelines_enabled=False to opt out — the tool then sets the
campaign field to False and skips the CampaignAsset wiring.
3. remove_entity didn't accept asset_group_signal. draft_asset_group_signal
creates signals; remove_entity couldn't unmake them, leaving an
asymmetric API and orphaned test signals in user accounts. Added
"asset_group_signal" to _REMOVABLE_ENTITY_TYPES with the composite
{assetGroupId~criterionId} id format (matches what
get_asset_group_signals returns) and an AssetGroupSignalService
remove branch in _apply_remove.
Tests: 209 passing (4 new — brand_guidelines default + opt-out,
asset_group_signal removal allowlist).
https://claude.ai/code/session_01WjDCCdg7BRfa6HZR4uUi2X
Codex review on PR #13 flagged a content-swap hole in _apply_upload_image_asset: the apply step only checked os.path.getsize() against the draft-time size, so a file replaced with different bytes of the same length between draft and confirm_and_apply would silently upload the wrong asset. draft_image_asset now hashes the file at draft time and the apply step re-hashes before sending to AssetService.MutateAssets. A mismatch raises ValueError ("sha256 mismatch — same byte count, different bytes") with guidance to re-draft. The pre-mutate verification was also lifted out of the apply loop into _read_image_unchanged so the whole batch is verified before any image reaches the API — partial batch uploads on a swap are no longer possible. https://claude.ai/code/session_01WjDCCdg7BRfa6HZR4uUi2X
…B7ngd Add image asset upload and brand guidelines support for PMax
Catches the fork up to upstream kLOsk/adloop (109 commits): Search Console, GTM, Merchant Center, PageSpeed, toolset filtering, two-phase apply, and currency generalization — combined with the fork's PMax read/write, labels, insights, and campaign-builder tools (union of both toolsets). All 8 conflicts resolved: - pyproject: google-ads>=31.1.0 - read.py: upstream currency-parameterized _enrich_cost_fields + fork helpers - crossref.py / server.py: union; big tool-registration blocks reconstructed from clean branches - write.py: union + upstream two-phase-apply and _extract_resource_name - RSA pinning standardized on `pinned_field` (the real Google Ads API field) - rules docs unioned/deduped + regenerated via sync-rules.py - fork tools tagged for toolset filtering; _config -> current_config() Follow-up commits reconcile the apply layer to upstream's dry-run model, re-graft final_url_suffix (auto-UTM), and clean up. Full suite green (632). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the upstream merge — resolves the 20 post-merge test failures. Full suite now passes (632 passed). - Adopt upstream's dry-run architecture for the apply layer: drop the fork's validate_only threading; _execute_plan + apply handlers use upstream's mutate(customer_id=, mutate_operations=) convention; confirm_and_apply uses upstream's two-phase/local-marker model. (Decision: simpler + aligns with upstream; dry-run is a local marker.) - Take upstream's _apply_create_campaign/_apply_update_campaign/ _apply_remove/_apply_campaign_assets; drop duplicate _apply_create_ad_group. - Tag all fork tools with a toolset tag (upstream toolset filtering); _config -> current_config() in fork tools; remove duplicate _VALID_*_PINS. - draft_rsa_replacement: unpack _validate_urls 2-tuple; restore empty-text validation in _validate_rsa; fix stale test mocks to 2-tuple shape. - cli: json.dumps the python path so Windows backslash paths stay valid JSON in generated MCP snippets (cross-platform fix). Known follow-up: final_url_suffix (UTM) no longer applied on create/update since we took upstream's apply bodies — needs re-grafting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Taking upstream's _apply_create_campaign/_apply_update_campaign in the merge dropped the fork's auto-UTM handling. Re-add it on top of upstream's bodies: set campaign.final_url_suffix on create when non-empty, and on update add it to the field mask (empty string clears, None = no change). draft_campaign already records it in plan.changes. Suite still green (632). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rules doc: drop duplicate GAQL "Common Resources" rows for asset_group / asset_group_asset / asset_group_top_combination_view left by the union merge (kept the detailed, v24-accurate rows); regenerated derived docs. - pmax_read: rename metrics.average_cpc_eur -> metrics.average_cpc_amount to match read.py's currency-agnostic convention; update its test. Suite green (632 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wade-Randel
force-pushed
the
chore/merge-upstream
branch
from
July 27, 2026 19:18
7041f72 to
c345606
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Catches the fork up to upstream kLOsk/adloop (109 commits): Search Console,
GTM, Merchant Center, PageSpeed, toolset filtering, two-phase apply, and
currency generalization — combined with the fork's PMax read/write, labels,
insights, and campaign-builder tools (union of both toolsets).
Conflict resolution (8 files):
from clean branches
pinned_field(the real Google Ads API field)Apply layer reconciled to upstream's dry-run model (local-marker + two-phase);
fork tools tagged for toolset filtering; _config -> current_config();
final_url_suffix (auto-UTM) re-grafted onto upstream's apply bodies; a
cross-platform CLI JSON-escaping fix for Windows paths.
Full test suite green: 632 passed.