adloop 0.19.0__tar.gz → 0.19.2__tar.gz

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 (64) hide show
  1. {adloop-0.19.0 → adloop-0.19.2}/PKG-INFO +5 -5
  2. {adloop-0.19.0 → adloop-0.19.2}/README.md +4 -4
  3. {adloop-0.19.0 → adloop-0.19.2}/pyproject.toml +1 -1
  4. {adloop-0.19.0 → adloop-0.19.2}/pyproject.toml.orig +1 -1
  5. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/custom_conversion_goals.py +0 -2
  6. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/read.py +6 -27
  7. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/write.py +21 -2
  8. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/server.py +37 -23
  9. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/__init__.py +0 -0
  10. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/__main__.py +0 -0
  11. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/_mcp_patches.py +0 -0
  12. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/__init__.py +0 -0
  13. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/ai_max.py +0 -0
  14. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/brands.py +0 -0
  15. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/client.py +0 -0
  16. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/conversion_actions.py +0 -0
  17. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/currency.py +0 -0
  18. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/enums.py +0 -0
  19. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/forecast.py +0 -0
  20. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/gaql.py +0 -0
  21. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/image_fetch.py +0 -0
  22. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/pmax.py +0 -0
  23. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ads/validate_only.py +0 -0
  24. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/auth.py +0 -0
  25. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/cli.py +0 -0
  26. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/config.py +0 -0
  27. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/crossref.py +0 -0
  28. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/diagnostics.py +0 -0
  29. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ga4/__init__.py +0 -0
  30. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ga4/client.py +0 -0
  31. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ga4/reports.py +0 -0
  32. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ga4/tracking.py +0 -0
  33. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/ga4/write.py +0 -0
  34. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gsc/__init__.py +0 -0
  35. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gsc/client.py +0 -0
  36. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gsc/reports.py +0 -0
  37. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gtm/__init__.py +0 -0
  38. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gtm/client.py +0 -0
  39. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/gtm/read.py +0 -0
  40. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/merchant/__init__.py +0 -0
  41. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/merchant/client.py +0 -0
  42. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/merchant/read.py +0 -0
  43. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/pagespeed.py +0 -0
  44. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/__init__.py +0 -0
  45. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/auth.py +0 -0
  46. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/client.py +0 -0
  47. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/read.py +0 -0
  48. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/schedule.py +0 -0
  49. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/reddit/write.py +0 -0
  50. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/__init__.py +0 -0
  51. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/adloop.md +0 -0
  52. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
  53. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/budget-plan.md +0 -0
  54. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/create-ad.md +0 -0
  55. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/create-campaign.md +0 -0
  56. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  57. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  58. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/rules_install.py +0 -0
  59. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/runtime.py +0 -0
  60. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/safety/__init__.py +0 -0
  61. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/safety/audit.py +0 -0
  62. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/safety/guards.py +0 -0
  63. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/safety/preview.py +0 -0
  64. {adloop-0.19.0 → adloop-0.19.2}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.19.0
3
+ Version: 0.19.2
4
4
  Summary: The AI command center for Google Ads, Reddit Ads, GA4, and tracking code.
5
5
  Keywords: mcp,google-ads,reddit-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
@@ -255,7 +255,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
255
255
  | `draft_callouts` | Create campaign callout assets from 1-25 character text snippets. |
256
256
  | `draft_structured_snippets` | Create campaign structured snippet assets using official header values and 3-10 snippet values. |
257
257
  | `draft_image_assets` | Create campaign image assets from local files or public image URLs (PNG, JPEG, or GIF). |
258
- | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
258
+ | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — flags BROAD match on campaigns without Smart Bidding as dangerous in the preview. |
259
259
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
260
260
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
261
261
  | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
@@ -314,8 +314,8 @@ AdLoop manages real ad spend, so safety is not optional.
314
314
  - **Budget caps.** Configurable maximum daily budget — the server rejects anything above the cap.
315
315
  - **Audit log.** Every operation (including dry runs) is logged to `~/.adloop/audit.log`.
316
316
  - **New campaigns and ads are PAUSED.** Nothing goes live without manual enablement.
317
- - **Destructive ops require double confirmation.** Removing entities or large budget increases trigger extra warnings.
318
- - **Broad Match + Manual CPC blocked.** The #1 cause of wasted ad spend is automatically prevented — `draft_keywords` refuses to add BROAD match keywords to campaigns without Smart Bidding.
317
+ - **Destructive ops are flagged for double confirmation.** Removing entities or large budget increases come back with extra warnings, and the AI is instructed to confirm twice.
318
+ - **Broad Match without Smart Bidding caught.** The #1 cause of wasted ad spend: `draft_campaign` refuses BROAD keywords unless the campaign uses Smart Bidding, and `draft_keywords` / `draft_ad_group` mark them as dangerous in the preview, so the AI has to raise it before anything is applied.
319
319
  - **Pre-write validation.** Before any write, the AI checks bidding strategy, conversion tracking status, and quality scores. If the campaign is fundamentally broken, AdLoop warns you instead of making things worse.
320
320
  - **Structured error handling.** All tools return actionable error messages with hints instead of raw exceptions. Auth errors include specific re-authorization steps.
321
321
  - **API version pinning.** The Google Ads API version is pinned to prevent silent breaking changes from library updates. `health_check` warns when a newer version is available.
@@ -578,7 +578,7 @@ What's been shipped and what's next:
578
578
  - ~~PyPI package~~ ✓ — `pip install adloop`
579
579
  - ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version: no Google Cloud project, no API access application, connect Google in two clicks (EU-hosted, GDPR-first)
580
580
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
581
- - ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
581
+ - ~~Behavioral eval suites~~ ✓ — prompt-and-expectation tests in `tests/evals/` covering read, write, tracking, and planning workflows
582
582
  - ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
583
583
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
584
584
  - **Video walkthrough**
@@ -228,7 +228,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
228
228
  | `draft_callouts` | Create campaign callout assets from 1-25 character text snippets. |
229
229
  | `draft_structured_snippets` | Create campaign structured snippet assets using official header values and 3-10 snippet values. |
230
230
  | `draft_image_assets` | Create campaign image assets from local files or public image URLs (PNG, JPEG, or GIF). |
231
- | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
231
+ | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — flags BROAD match on campaigns without Smart Bidding as dangerous in the preview. |
232
232
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
233
233
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
234
234
  | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
@@ -287,8 +287,8 @@ AdLoop manages real ad spend, so safety is not optional.
287
287
  - **Budget caps.** Configurable maximum daily budget — the server rejects anything above the cap.
288
288
  - **Audit log.** Every operation (including dry runs) is logged to `~/.adloop/audit.log`.
289
289
  - **New campaigns and ads are PAUSED.** Nothing goes live without manual enablement.
290
- - **Destructive ops require double confirmation.** Removing entities or large budget increases trigger extra warnings.
291
- - **Broad Match + Manual CPC blocked.** The #1 cause of wasted ad spend is automatically prevented — `draft_keywords` refuses to add BROAD match keywords to campaigns without Smart Bidding.
290
+ - **Destructive ops are flagged for double confirmation.** Removing entities or large budget increases come back with extra warnings, and the AI is instructed to confirm twice.
291
+ - **Broad Match without Smart Bidding caught.** The #1 cause of wasted ad spend: `draft_campaign` refuses BROAD keywords unless the campaign uses Smart Bidding, and `draft_keywords` / `draft_ad_group` mark them as dangerous in the preview, so the AI has to raise it before anything is applied.
292
292
  - **Pre-write validation.** Before any write, the AI checks bidding strategy, conversion tracking status, and quality scores. If the campaign is fundamentally broken, AdLoop warns you instead of making things worse.
293
293
  - **Structured error handling.** All tools return actionable error messages with hints instead of raw exceptions. Auth errors include specific re-authorization steps.
294
294
  - **API version pinning.** The Google Ads API version is pinned to prevent silent breaking changes from library updates. `health_check` warns when a newer version is available.
@@ -551,7 +551,7 @@ What's been shipped and what's next:
551
551
  - ~~PyPI package~~ ✓ — `pip install adloop`
552
552
  - ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version: no Google Cloud project, no API access application, connect Google in two clicks (EU-hosted, GDPR-first)
553
553
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
554
- - ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
554
+ - ~~Behavioral eval suites~~ ✓ — prompt-and-expectation tests in `tests/evals/` covering read, write, tracking, and planning workflows
555
555
  - ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
556
556
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
557
557
  - **Video walkthrough**
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.19.0"
3
+ version = "0.19.2"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.19.0"
3
+ version = "0.19.2"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -537,7 +537,6 @@ def draft_clear_custom_conversion_goal(
537
537
 
538
538
  Call ``confirm_and_apply`` with the returned plan_id to execute.
539
539
  """
540
- from adloop.ads.client import normalize_customer_id
541
540
  from adloop.safety.guards import SafetyViolation, check_blocked_operation
542
541
  from adloop.safety.preview import ChangePlan, store_plan
543
542
 
@@ -553,7 +552,6 @@ def draft_clear_custom_conversion_goal(
553
552
  "details": ["campaign_id must be a numeric ID"],
554
553
  }
555
554
 
556
- cid = normalize_customer_id(customer_id or config.ads.customer_id)
557
555
  state = _custom_goal_state(config, customer_id, campaign_id=campaign_id)
558
556
  campaign = find_campaign(state, campaign_id)
559
557
  if campaign is None:
@@ -373,7 +373,11 @@ def get_search_terms(
373
373
  """Get search terms report — what users actually typed before clicking ads."""
374
374
  from adloop.ads.gaql import execute_query
375
375
 
376
- date_clause = _date_clause(date_range_start, date_range_end)
376
+ # search_term_view requires an explicit date segment, so the default is
377
+ # DURING LAST_30_DAYS and a given range replaces it. ``_date_clause``
378
+ # prefixes its fragment with AND (the other read tools append it to a
379
+ # WHERE); here it is the only predicate, so the AND comes off.
380
+ where = _date_clause(date_range_start, date_range_end).removeprefix("AND ")
377
381
 
378
382
  query = f"""
379
383
  SELECT search_term_view.search_term,
@@ -381,35 +385,10 @@ def get_search_terms(
381
385
  metrics.impressions, metrics.clicks,
382
386
  metrics.cost_micros, metrics.conversions
383
387
  FROM search_term_view
384
- WHERE segments.date DURING LAST_30_DAYS
385
- {f"AND segments.date BETWEEN '{date_range_start}' AND '{date_range_end}'" if date_range_start and date_range_end else ""}
388
+ WHERE {where}
386
389
  ORDER BY metrics.clicks DESC
387
390
  LIMIT 200
388
391
  """
389
- # search_term_view requires an explicit date segment, so we always
390
- # include DURING LAST_30_DAYS as baseline and override if dates given.
391
- if date_range_start and date_range_end:
392
- query = f"""
393
- SELECT search_term_view.search_term,
394
- campaign.name, ad_group.name,
395
- metrics.impressions, metrics.clicks,
396
- metrics.cost_micros, metrics.conversions
397
- FROM search_term_view
398
- WHERE segments.date BETWEEN '{date_range_start}' AND '{date_range_end}'
399
- ORDER BY metrics.clicks DESC
400
- LIMIT 200
401
- """
402
- else:
403
- query = """
404
- SELECT search_term_view.search_term,
405
- campaign.name, ad_group.name,
406
- metrics.impressions, metrics.clicks,
407
- metrics.cost_micros, metrics.conversions
408
- FROM search_term_view
409
- WHERE segments.date DURING LAST_30_DAYS
410
- ORDER BY metrics.clicks DESC
411
- LIMIT 200
412
- """
413
392
 
414
393
  rows = execute_query(config, customer_id, query)
415
394
  currency_code = get_currency_code(config, customer_id)
@@ -2780,7 +2780,27 @@ def confirm_and_apply(
2780
2780
  "objects an earlier step would create, so Google could "
2781
2781
  "only validate the step(s) before them."
2782
2782
  )
2783
- if forced_by_config:
2783
+ from adloop.runtime import deployment_mode as _deployment_mode
2784
+
2785
+ if forced_by_config and _deployment_mode() == "server":
2786
+ # Hosted: the flag is the workspace's live-changes setting,
2787
+ # switched in the dashboard. There is no config file to edit
2788
+ # and no server for the user to restart.
2789
+ response["dry_run_forced_by"] = "config.safety.require_dry_run"
2790
+ response["remediation"] = (
2791
+ "Live changes are switched off for this workspace (or for "
2792
+ "this client). The workspace owner can switch them on in "
2793
+ "the AdLoop Cloud dashboard under Settings → Safety. Passing "
2794
+ "dry_run=false on this tool will keep being overridden until "
2795
+ "then."
2796
+ )
2797
+ response["message"] = (
2798
+ "dry_run=false was IGNORED because live changes are switched "
2799
+ "off for this workspace. No changes were made. Retrying this "
2800
+ "tool with dry_run=false will not succeed until live changes "
2801
+ "are switched on in the dashboard."
2802
+ )
2803
+ elif forced_by_config:
2784
2804
  # The caller passed dry_run=false but safety.require_dry_run
2785
2805
  # forced it back on. Tell them exactly why and how to unlock
2786
2806
  # real writes — without this, agents (e.g. Claude Code) retry
@@ -3867,7 +3887,6 @@ def _apply_create_campaign(client: object, cid: str, changes: dict) -> dict:
3867
3887
  results = {}
3868
3888
  num_keywords = len(kw_list)
3869
3889
  num_geo = len(changes.get("geo_target_ids") or [])
3870
- num_lang = len(changes.get("language_ids") or [])
3871
3890
  for i, resp in enumerate(response.mutate_operation_responses):
3872
3891
  rn = _extract_resource_name(resp)
3873
3892
  if rn:
@@ -13,9 +13,11 @@ from pydantic import BeforeValidator
13
13
  from adloop import diagnostics
14
14
  from adloop.runtime import current_config
15
15
 
16
- _READONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False)
17
- _WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False)
18
- _DESTRUCTIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=True)
16
+ # openWorldHint: every tool talks to an external service (Google, Reddit,
17
+ # a live web page), which directories such as ChatGPT's require to be said.
18
+ _READONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False, openWorldHint=True)
19
+ _WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False, openWorldHint=True)
20
+ _DESTRUCTIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=True, openWorldHint=True)
19
21
 
20
22
  # Toolset taxonomy: every tool carries exactly one of these tags (or "core",
21
23
  # which survives every ADLOOP_TOOLSETS selection — health_check and
@@ -308,13 +310,20 @@ def _structured_error(fn_name: str, exc: Exception) -> dict:
308
310
  }
309
311
 
310
312
  if "deleted_client" in err_lower or "invalid_client" in err_lower:
313
+ from adloop.runtime import deployment_mode
314
+
311
315
  return {
312
316
  "error": (
313
317
  "Authentication failed — the OAuth client behind your stored "
314
318
  "credentials no longer exists or is invalid."
315
319
  ),
320
+ # On a hosted server the user never sees an OAuth client: the
321
+ # fix is reconnecting, and pointing at a product would be an ad.
316
322
  "hint": (
317
- "If you set up AdLoop before v0.10 with its bundled "
323
+ "Reconnect Google in your AdLoop Cloud dashboard "
324
+ "(Settings → Google), then retry."
325
+ if deployment_mode() == "server"
326
+ else "If you set up AdLoop before v0.10 with its bundled "
318
327
  "credentials: that shared Google Cloud project has been "
319
328
  "retired. Fastest fix: AdLoop Cloud (https://getadloop.com) — "
320
329
  "connect Google in two clicks, no Google Cloud project "
@@ -552,6 +561,8 @@ def run_ga4_report(
552
561
 
553
562
  Date formats: "today", "yesterday", "7daysAgo", "28daysAgo", "90daysAgo", or "YYYY-MM-DD".
554
563
  If property_id is empty, uses the default from config.
564
+ Queries the GA4 Data API. Dimensions and metrics:
565
+ https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema
555
566
  """
556
567
  from adloop.ga4.reports import run_ga4_report as _impl
557
568
 
@@ -648,7 +659,7 @@ def run_gsc_report(
648
659
  finding keyword opportunities, and cross-referencing with GA4 and Ads data.
649
660
 
650
661
  site_url: the GSC property URL (e.g. "https://example.com/" or
651
- "sc-domain:example.com"). Defaults to gsc.site_url in config.yaml.
662
+ "sc-domain:example.com"). Defaults to the configured Search Console site.
652
663
  dimensions: one or more of ["query", "page", "country", "device", "date"].
653
664
  Defaults to ["query"].
654
665
  date_range_start / date_range_end: ISO dates (YYYY-MM-DD) or relative
@@ -660,6 +671,8 @@ def run_gsc_report(
660
671
  [{"filters": [{"dimension": "query", "operator": "contains",
661
672
  "expression": "analytics"}]}]
662
673
  limit: maximum rows to return (default 100, max 25000).
674
+ Queries the Search Console API:
675
+ https://developers.google.com/webmaster-tools/v1/searchanalytics/query
663
676
  """
664
677
  from adloop.gsc.reports import run_gsc_report as _impl
665
678
 
@@ -708,8 +721,9 @@ def analyze_page_speed(url: str, strategy: str = "mobile") -> dict:
708
721
  def list_merchant_accounts() -> dict:
709
722
  """List Google Merchant Center accounts the connected user can access.
710
723
 
711
- Call first to discover merchant IDs for get_merchant_feed_health.
712
- Distinguishes standalone accounts from aggregator (MCA) accounts.
724
+ Returns each account's numeric ID, name and whether it is a test
725
+ account. Call it to find the account_id for get_merchant_feed_health;
726
+ not needed when merchant.account_id is set in the config.
713
727
  """
714
728
  from adloop.merchant.read import list_merchant_accounts as _impl
715
729
 
@@ -899,9 +913,9 @@ def get_negative_keyword_lists(
899
913
  ) -> dict:
900
914
  """List all shared negative keyword lists (SharedSets) in the account.
901
915
 
902
- Returns each list's ID, name, status, and keyword count. Always call
903
- this before propose_negative_keyword_list to avoid creating duplicates —
904
- a suitable list may already exist and just need attaching to a campaign.
916
+ Returns each list's ID, name, status, and keyword count. Useful before
917
+ propose_negative_keyword_list: a suitable list may already exist and
918
+ just need attaching to a campaign.
905
919
  """
906
920
  from adloop.ads.read import get_negative_keyword_lists as _impl
907
921
 
@@ -1543,7 +1557,8 @@ def run_gaql(
1543
1557
  """Execute an arbitrary GAQL (Google Ads Query Language) query.
1544
1558
 
1545
1559
  Use this for advanced queries not covered by the other tools.
1546
- See the GAQL reference in the AdLoop cursor rules for syntax help.
1560
+ Queries the Google Ads API. GAQL syntax and fields:
1561
+ https://developers.google.com/google-ads/api/docs/query/overview
1547
1562
 
1548
1563
  format: "table" (default, readable), "json" (structured), "csv" (exportable)
1549
1564
  """
@@ -2125,8 +2140,8 @@ def add_to_negative_keyword_list(
2125
2140
 
2126
2141
  Use this when a suitable list already exists and only needs more keywords
2127
2142
  (instead of propose_negative_keyword_list, which creates a new list).
2128
- Always call get_negative_keyword_lists first to find the right shared_set_id
2129
- and get_negative_keyword_list_keywords to avoid duplicating existing terms.
2143
+ The shared_set_id comes from get_negative_keyword_lists;
2144
+ get_negative_keyword_list_keywords shows the terms already in a list.
2130
2145
 
2131
2146
  shared_set_id: numeric ID from get_negative_keyword_lists (shared_set.id).
2132
2147
  keywords: list of keyword strings to append (duplicates in the input list
@@ -2625,14 +2640,12 @@ def confirm_and_apply(
2625
2640
  to make real changes to the ad account (Google Ads or Reddit Ads — the
2626
2641
  plan knows which platform it targets).
2627
2642
 
2628
- Config override: if 'safety.require_dry_run: true' is set in the user's
2629
- config file (default ~/.adloop/config.yaml), dry_run=false is IGNORED
2630
- and this tool will keep returning DRY_RUN_SUCCESS. When that happens the
2631
- response includes 'dry_run_forced_by', 'config_path', and 'remediation'
2632
- fields — surface those to the user verbatim and STOP retrying. Calling
2633
- this tool again with dry_run=false will not change anything until the
2634
- user edits the config file, sets 'require_dry_run: false', and restarts
2635
- the AdLoop MCP server.
2643
+ Forced dry runs: when real changes are switched off (live changes off
2644
+ for the workspace in AdLoop Cloud, or safety.require_dry_run in a
2645
+ self-hosted config), dry_run=false is IGNORED and the result is still
2646
+ DRY_RUN_SUCCESS, with 'dry_run_forced_by' and 'remediation' fields.
2647
+ Pass the remediation on to the user and stop retrying: calling again
2648
+ with dry_run=false changes nothing until that setting is changed.
2636
2649
 
2637
2650
  Two-phase apply: if 'safety.two_phase_apply: true' is set (always on
2638
2651
  for AdLoop Cloud tenants), dry_run=false is REFUSED with status
@@ -2815,6 +2828,7 @@ def run_reddit_report(
2815
2828
  (HOUR and DATE cannot be combined). Dates are account-local days.
2816
2829
  filter: Reddit filter expression (e.g. "campaign_id==abc123").
2817
2830
  Microcurrency fields are converted to currency amounts.
2831
+ Queries the Reddit Ads API: https://ads-api.reddit.com/docs/v3/
2818
2832
  """
2819
2833
  from adloop.reddit.read import run_reddit_report as _impl
2820
2834
 
@@ -3734,8 +3748,8 @@ def get_brand_lists(
3734
3748
  ) -> dict:
3735
3749
  """List all brand lists (SharedSets of type BRANDS) in the account.
3736
3750
 
3737
- Returns each list's ID, name, status, and member count. Call this before
3738
- propose_brand_list so an existing list can be reused instead of duplicated.
3751
+ Returns each list's ID, name, status, and member count. Useful before
3752
+ propose_brand_list, so an existing list can be reused instead of duplicated.
3739
3753
 
3740
3754
  Args:
3741
3755
  customer_id: Ads account ID. Defaults to the configured account.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes