# Changelog

## 2026.7.6 (2026-07-06)

### Added — Google Ads: PMax asset groups, Shopping, DemandGen ads, ad-group device bids

25 new Google Ads tools (plugin tool count 188 → 213):

**Performance Max asset-group management (A4)** — deep asset-group lifecycle
plus per-field asset edits:

- `get_pmax_asset_groups`, `add_pmax_asset_group`, `update_pmax_asset_group`,
  `remove_pmax_asset_group`
- `list_pmax_asset_group_assets`, `add_pmax_asset_group_asset`,
  `remove_pmax_asset_group_asset`

**Shopping (net-new campaign type)** — Merchant Center linkage, deep create,
product-partition trees, and campaign removal:

- **Merchant Center:** `get_merchant_center_links`, `link_merchant_center`,
  `unlink_merchant_center`
- **Create + settings:** `create_shopping_campaign` (multi-ad-group, multi-ad,
  all bidding strategies), `get_shopping_settings`, `add_shopping_ad_group`,
  `add_shopping_product_ad`
- **Standard product groups:** `get_product_groups`, `add_product_group`,
  `update_product_group`, `remove_product_group`
- **Retail PMax listing filters:** `get_asset_group_listing_filters`,
  `add_asset_group_listing_filter`, `remove_asset_group_listing_filter`
- **Removal:** `remove_campaign` — permanent campaign deletion (any type)

**DemandGen ad CRUD (A5)** — `add_demandgen_ad` (video-responsive or
multi-asset ads into an existing DemandGen ad group).

**Ad-group device bid modifiers (A6)** — `get_ad_group_device_bid_modifiers`,
`set_ad_group_device_bid_modifiers` (mobile/tablet/desktop bid adjustments at
the ad-group level, complementing the existing campaign-level device bids).

## 2026.7.4 (2026-07-03)

### Added — Google Ads campaign-level depth (A2)

10 new Google Ads tools for campaign-level management (plugin tool count 178 → 188):

- **Tracking:** `get_campaign_tracking` — read a campaign's tracking URL template,
  final URL suffix, and custom parameters (settable via `update_campaign`).
- **Dates:** `get_campaign_dates`, `update_campaign_dates` — read/set a campaign's
  start and end dates (any campaign type).
- **Frequency caps:** `get_campaign_frequency_caps`, `update_campaign_frequency_caps`
  — read/set how often a user sees the ads.
- **Labels:** `list_labels`, `create_label`, `get_campaign_labels`,
  `add_campaign_label`, `remove_campaign_label` — organize and tag campaigns.

## 2026.7.3 (2026-07-03)

### Added — Google Ads deep management: Search full-depth (A0) + Demand Gen (A1)

16 new Google Ads tools, bringing full Manager-parity management to the Search and
Demand Gen (YouTube) campaign families (plugin tool count 162 → 178).

**Search full-depth deep management (A0, 12 tools):** `add_search_ad_group`,
`get_ad_group_settings`, `update_ad_group_settings`, `remove_ad_group`, `remove_ad`,
`get_ad_group_demographics`, `update_ad_group_demographics`, `get_ad_group_audiences`,
`add_ad_group_audiences`, `add_campaign_proximity`, `set_campaign_ad_schedule`,
`set_campaign_device_bid_modifiers` — add/edit/remove ad groups, ads, demographics,
audiences, plus campaign-level proximity/radius, ad scheduling, and device bid modifiers.

**Demand Gen deep management (A1, 4 tools):** `get_demandgen_channel_controls`,
`update_demandgen_channel_controls`, `get_demandgen_audiences`,
`replace_demandgen_audiences` — read/toggle the 6 Demand Gen delivery channels
(YouTube in-stream/in-feed/Shorts, Discover, Gmail, Display) and read/replace the
grouped audience on a Demand Gen ad group.

## 2026.6.30 (2026-06-30)

### Added — Meta Advantage+ Catalog (Dynamic Product Ads), full end-to-end (#768)

The complete Meta catalog / Dynamic Product Ad (DPA) tool suite — discover →
analyze readiness → create → deeply manage → monitor — surfacing 17 new tools
under the `meta_ads` router:

- **Discovery & readiness** — `validate_catalog_readiness` (pre-flight: products
  with images? pixel firing product events? retargeting viable vs prospecting?),
  `list_meta_catalogs`, `list_meta_product_sets`, `list_meta_products`,
  `create_meta_product_set`, `get_meta_pixel_stats`.
- **Create & manage** — `create_meta_catalog_campaign` (prospecting + retargeting,
  created PAUSED), `add_meta_catalog_ad_set` / `add_meta_catalog_ad` (multiple ad
  sets/ads per campaign, each with its own product set / audience / budget),
  `update_meta_catalog_campaign` / `_ad_set` / `_ad` (deep field-level updates).
- **Analytics (read-only, cache-only)** — `analyze_meta_catalog_performance` and
  `get_meta_product_performance` (per-product / per-set ROAS, funnel, units),
  `diagnose_meta_catalog_funnel` (view→cart→checkout→purchase drop-off, reported
  as measured facts), `detect_meta_catalog_waste` (recoverable SKU spend),
  `compare_meta_catalog_product_sets` (set-vs-set ROAS + overlap). These read the
  nightly product-metrics cache and never call Meta live.

### Changed

- Tool schemas refreshed to include the destructiveHint annotations (#765) and the
  honest cache-freshness messaging (#757) shipped since 2026.6.17.
- Static fallback + `contracts.tools` regenerated: 162 tools total.

## 2026.6.17 (2026-06-17)

### Added — ChatGPT Ads (OpenAI Ads), 7th platform (#688)

- **`chatgpt_ads`** — router meta-tool for the ChatGPT Ads (OpenAI Ads)
  federated sub-server. Conversationally create / manage / analyze ads shown
  inside ChatGPT responses, backed by the OpenAI Advertiser API. Connects via a
  pasted Advertiser API key (no OAuth); one key = one ad account. Surfaces 31
  underlying tools: account, campaigns/ad-groups/ads (list/get/create/update/
  pause/resume/archive), one-shot launch, creative upload, geo lookup, full
  scoped insights (fields/filters/sort/pagination/zero-impression), and
  conversions config/events/test. Ads are created PAUSED; review status is
  surfaced honestly (OpenAI rejects third-party-AI-service ads).

## 2026.6.12 (2026-06-12)

### Added — Google Ads ad-level performance + creative export (#646)

- **`get_ad_performance`** — Per-ad performance breakdown (one row per ad):
  impressions, clicks, CTR, cost, conversions, conversion value, CPC, CPA,
  ROAS, video views + view rate, ad strength, is_live flag, head-to-head
  comparison for ad groups with multiple ads. Search / Display / Demand Gen /
  YouTube. Live GAQL + 5-min cache.
- **`get_ad_creative`** — Export the creative composition of existing ads:
  YouTube video IDs + watch/thumbnail URLs, downloadable image URLs with
  role + dimensions, headlines/descriptions/business name/CTAs, carousel
  cards. Demand Gen (video responsive, multi-asset, carousel) + RSA + RDA.

### Changed

- **`discover_existing_assets`** — now PAGINATED across all asset types
  (50 per type per page, `page` parameter): returns the complete asset
  inventory instead of capping at 15 images / 8 sitelinks / 10 videos.
  Adds downloadable image URLs and an explicit warning + forward path when
  no square logo exists (previously a silent dead-end for PMax creation).

## 2026.6.10 (2026-06-10)

### Added — Amazon Ads (6th ad platform)

- **`amazon_ads`** — Router meta-tool for the new `adspirer-mcp-amazon`
  sub-server (61 tools): Sponsored Products full management + creation,
  Sponsored Brands (campaigns, ad groups, all 7 ad types, creative asset
  pipeline), Sponsored Display via Amazon's unified v1 API, plus advanced
  analytics (wasted spend, budget optimizer, targeting analysis, placements,
  per-ASIN performance, purchased-products cross-sell) and Reporting v3
  with async polling (`get_amazon_report`).
- New `amazon_ads` tool group (enable with `enabledGroups: ["amazon_ads"]`).

### Changed

- `remove_structured_snippets` (Google Ads) added to its tool group.
- Meta tool schemas refreshed: EU DSA fields (`dsa_beneficiary`/`dsa_payor`)
  and #610 input coercions.
- Static fallback + `contracts.tools`: 142 tools.


## 2026.5.30 (2026-05-30)

### Added — Diagnostic & helper tools (#501)

- **`diagnostic_and_helper_tools`** — Router meta-tool exposing read-only
  diagnostic and onboarding helpers from the new `adspirer-mcp-retention`
  sub-server. Use `action: "list_tools"` to discover operations, then
  `action: "execute"`. Covers: `get_campaign_spec` (exact field/asset
  requirements per platform + campaign type), `preflight_campaign`
  (launch-readiness checks before creating), `validate_campaign_draft`
  (dry-run validation), `explain_platform_error` (plain-language cause + fix),
  `diagnose_my_setup` (connection/auth health), `why_did_this_fail` (recent
  failures + remediation), and `verify_campaign_is_live`. All read-only and
  quota-exempt. Grouped under `system`. Tool count 139 → 140.

## 2026.5.12 (2026-05-23)

### Added — Third-party integrations (PIPEDREAM_V1)

- **`google_analytics`** — Read Google Analytics 4 reports + metadata for
  the user's connected GA4 properties. Two-step interface: call with
  `action: "list_tools"` to discover available GA actions
  (`google_analytics-run-report-in-ga4`, `google_analytics-list-property-options`,
  etc.), then `action: "execute"` with `tool_name` + `arguments` to run one.
  Returns the standard {content, isError} MCP shape. If the user hasn't
  connected GA in the Adspirer Connections tab, returns a friendly prompt.
- **`klaviyo`** — Same interface for Klaviyo (email + SMS marketing).
  Lists, segments, profiles, campaign performance, flows.

These route through a new `adspirer-mcp-pipedream` Cloud Run sub-server
that proxies to Pipedream Connect's remote MCP. Each tool is 1:1 with a
user-OAuth-connected upstream — so unlike `google_ads`/`linkedin_ads`/etc.
(router meta-tools that fan out to many actions), each integrations tool
is registered as a first-class entry.

### Added — Google Ads PMax + ad-group tools (#341)

- **PMax asset-group images**: `add_pmax_asset_group_image`,
  `list_pmax_asset_group_images`, `remove_pmax_asset_group_image`,
  `get_pmax_asset_performance`.
- **Search campaign + business assets**: `add_business_name_asset`,
  `add_call_asset`, `add_search_campaign_image`.
- **Ad-group lifecycle**: `pause_ad_group`, `resume_ad_group`.

### Added — Watch agent integration (monitoring)

The `monitoring_and_reporting` router gained `apply_signal_actions`,
`get_signal_briefing`, `run_watch_now` (these live in the sub-server's
tool catalog; the hub-side router meta-tool routes them by name).

### Internal

- New `INTEGRATIONS_TOOLS` group + `"integrations"` `ToolGroup` type
  (`src/tool-groups.ts`, `src/types.ts`).
- `scripts/generate-static-tools.py` now imports `INTEGRATIONS_ROUTER_TOOLS`
  alongside the per-platform routers so the static fallback always reflects
  what the hub actually exposes.

**Tool count: 125 → 139** (+14)

---

## 2026.5.11 (2026-05-11)

### Added — Google Ads PMax asset group images (issue #341)

- **`list_pmax_asset_group_images`** — list image assets linked to a PMax
  campaign's asset groups. Read-only. Returns per-image URL, dimensions,
  field type, status, performance label, and the
  `asset_group_asset_resource_name` you pass straight back to remove.
- **`add_pmax_asset_group_image`** — upload a new image (http(s) URL or
  `data:image/...;base64,...` URI) and link it to an existing PMax asset
  group. Pre-flight cap check: 20 marketing images per group (across
  `MARKETING_IMAGE` / `SQUARE_MARKETING_IMAGE` / `PORTRAIT_MARKETING_IMAGE` /
  `TALL_PORTRAIT_MARKETING_IMAGE`), 5 of each logo type (`LOGO` /
  `LANDSCAPE_LOGO` / `BUSINESS_LOGO`). Atomic mutate batches asset create
  and asset_group_asset link in one Google Ads request.
- **`remove_pmax_asset_group_image`** — unlink an image from a PMax asset
  group. Accepts either an `asset_group_asset_resource_name` (preferred)
  or an `asset_id` (with optional `asset_group_id` for disambiguation).
  Does NOT delete the underlying Asset — it stays in the Asset Library and
  can be re-linked.
- **`get_pmax_asset_performance`** — read Google's own per-asset
  `performance_label` for every image asset, plus asset-group-level
  rollup metrics (impressions / clicks / conversions / cost_micros /
  CTR / CVR) for the requested date range. Derives a `recommended_action`
  from the label (LOW → REPLACE, LEARNING / PENDING → WAIT,
  GOOD / BEST → KEEP, UNKNOWN / UNSPECIFIED → REVIEW). Optional
  `min_label_severity` filter (LOW / GOOD / BEST).

## 2026.5.9 (2026-05-10)

### Added — TikTok Layer 2 (issue #299)

- **`explain_tiktok_objective`** — read-only capability check. Wraps the
  Layer-1 validity matrix so callers can sanity-check an objective BEFORE
  attempting a campaign create. Returns required fields, forbidden fields,
  currency-aware minimum daily budget, and flags objectives that need
  account-level grant (LEAD_GENERATION → lead form, PRODUCT_SALES → catalog).
- **`delete_tiktok_campaign`** / **`delete_tiktok_ad_group`** / **`delete_tiktok_ad`**
  — explicit DELETE tools (status=DELETE injected by the wrapper) for
  atomic-cleanup workflows. Cascades to children on campaign-level delete.

### Added — Google Ads (issue #325)

- **`update_pmax_audience_signal`** — modify an existing Performance Max
  audience signal in-place instead of remove + add.

### Changed — TikTok schema depth

- **`pause_tiktok_campaign` / `pause_tiktok_ad_group` / `pause_tiktok_ad`**
  + corresponding `resume_*` — slim input schemas, no longer require an
  explicit `status` field. The wrapper injects DISABLE/ENABLE based on
  tool name. Fixes the crash where calling these without status returned
  "Field required: status" to customers.
- **`create_tiktok_campaign`** — added `image_urls: List[str]` field for
  the direct single-image happy path. No more 2-step
  `validate_and_prepare_tiktok_assets` → `create_tiktok_campaign` dance
  for single images; just pass the HTTPS URL and Adspirer converts to
  slideshow video via ffmpeg.
- **`search_tiktok_targeting`** — major upgrade:
  - Returns 13K rich locations (location_id, name, level, country, parent_id,
    support_below_18) instead of 65 raw country codes. Parser was reading
    the wrong key in `region_info` vs `region_list`.
  - New filter fields: `country_codes`, `state_filter`, `name_contains`,
    `level`, `os_filter`, `brand_filter` — apply client-side BEFORE
    rendering so the LLM doesn't scroll through 13K rows.
  - 3 new formatters for `action_categories`, `carriers`, `device_models`
    (were raw JSON dumps before). `action_categories` no longer returns
    `ID: N/A` for every row.
  - Dropped misleading `audience_size` column from `interest_keywords`
    output — TikTok's API doesn't return that field.

### Notes

- 124 → 128 tools (+4 new tiktok deletes, +1 update_pmax_audience_signal —
  net +5, minus 1 that was reclassified elsewhere).
- `contracts.tools` array in `openclaw.plugin.json` regenerated (128 entries).
- Sync check: ALL=128, Static=128 — drift-free.

## 2026.5.8 (2026-05-09)

### Added (Google Ads, issue #327)
- **`list_conversion_actions`** — full per-action metadata for the connected Google Ads account: id, name, category, status, type, counting mode, click-through and view-through lookback windows, `primary_for_goal` flag, value settings (default value, currency, always_use_default), attribution model. Filters: `status_filter` (ENABLED / primary / all) and `include_removed`. Live + Redis 1h cache.
- **`get_conversion_action_performance`** — per-conversion-action breakdown showing conversions, conversion_value, all_conversions, all_conversions_value, avg value per conversion, share of conversions, share of value. Account or campaign-level totals (spend / CPA / ROAS) shown separately. `campaign_id` optional. Live + Redis 5-min cache. Per-action CPA/ROAS deliberately not provided — Google Ads API rejects cost segmentation by `conversion_action`.

### Changed (Google Ads, issue #327)
- **`get_campaign_performance`** — now accepts `campaign_id` to scope the analysis to a single campaign. When provided, the response renders "Single-Campaign Performance Analysis" header with the campaign label, drops the `Total Campaigns` line, and hides the redundant "Top Performing Campaign" block. Account-wide path (no `campaign_id`) is byte-identical to the prior output.
- **`get_campaign_targeting`** — extended from locations-only to full targeting view across all dimensions: locations, languages, demographics (gender / age range / parental status / income range), audiences (user lists, user interests, custom audiences, combined audiences), devices, ad schedule, topics, placements, keywords, plus negatives mirrored across each dimension. Two collapsed GAQL queries; Redis 15-min cache. Output includes ad-group attribution and bid modifiers per criterion.

### Tool count
- 125 → **127** (+2 net new in google_ads group)

### Verified
- All 4 tools tested via live REST API (`api.adspirer.ai`) and federated MCP (`mcp.adspirer.com`) round-trips before publish.
- `static-fallback.ts` and `tool-groups.ts` in sync (127 = 127).

## 2026.5.7 (2026-05-09)

### Fixed
- **`openclaw adspirer login` ECONNREFUSED on macOS browsers** — the local OAuth callback server was bound only to IPv4 loopback (`127.0.0.1`), but macOS browsers (Safari, Chrome) resolve `localhost` to IPv6 `::1` first. So the redirect URL `http://localhost:<port>/callback` reached the v6 stack and got `ECONNREFUSED`, even though `lsof -i :<port>` showed the v4 listener up. The CLI's "⏳ Waiting for authentication…" log fired correctly — it was just listening on the wrong stack for the browser. Fix: callback server now binds two HTTP servers on the same ephemeral port — one to `127.0.0.1`, one to `::1` (`ipv6Only: true`) — so whichever address the browser resolves `localhost` to, the request hits a listener. Stays loopback-only (no `0.0.0.0` / `::` bind) so the manifest `localhost` permission justification is unchanged. If the host has no IPv6 stack, the v6 bind fails silently and we proceed v4-only.
- **No tool / schema changes** — same 125 tools as `2026.5.6`. Login-path fix only.

### Verified
- E2E probe: GET `http://127.0.0.1:<port>/`, GET `http://[::1]:<port>/`, GET `http://localhost:<port>/` all reach the callback handler.
- E2E callback: `/callback?code=…&state=…` over `[::1]` (the previously-broken path) resolves the OAuth promise and returns `{code, state}` correctly.

## 2026.5.6 (2026-05-09)

### Fixed
- **OpenClaw 2026.5.7 compatibility — `contracts.tools` declaration** — OpenClaw 2026.4.29 introduced a strict gate that requires every plugin to declare its tool names in the manifest under `contracts.tools`; tools registered without a matching declaration are silently rejected and the plugin loads in `non-capability` mode (zero tools usable). On the latest OpenClaw 2026.5.7, `openclaw plugins inspect openclaw-adspirer` reported `Shape: non-capability` / `Capability mode: none`, and the doctor surfaced `plugin must declare contracts.tools before registering agent tools`. Fix: added a `contracts.tools` array listing all 125 tool names to `openclaw.plugin.json`, generated from the same canonical sorted list as `static-fallback.ts` so it can never drift. The regen script (`scripts/generate-static-tools.py`) now writes both files in one pass.
- **No tool / schema changes** — same 125 tools as `2026.5.5`. Manifest-only fix for the new OpenClaw enforcement.

### Notes
- `contracts.tools` is read once at plugin load by OpenClaw's loader (see `dist/tool-contracts-*.js` → `findUndeclaredPluginToolNames`). It is **not** injected into the agent system prompt or LLM payload — zero customer token impact.

## 2026.5.5 (2026-05-06)

### Added
- **`get_meta_adset_performance`** (#317) — net-new tool that fills the gap between `get_meta_campaign_performance` and `analyze_meta_ad_performance`. Returns per-ad-set spend, CTR, frequency, conversions, CPA, ROAS, with mode-aware ranking that uses `classify_campaign()` to dispatch ROAS-vs-CPA-vs-CTR per the parent campaign's objective. Surfaces `analysis_mode` + `is_roas_applicable` + `mixed_objectives` at the response level so the LLM never recommends ROAS strategies for lead-gen accounts. Tool count: **125** (was 124). Verified live across 6 prod accounts spanning USD/ARS/CLP/ZAR/GBP and all five Meta objective categories (sales, lead-gen, traffic, awareness, mixed).

## 2026.5.4 (2026-05-03)

### Fixed
- **Plugin tarball missing `dist/` directory** — published `2026.5.3` shipped TypeScript source (`./src/index.ts`) but no compiled output, so OpenClaw rejected installs with `"package install requires compiled runtime output for TypeScript entry ./src/index.ts"`. Root causes:
  - `package.json` had no `prepublishOnly` / `prepack` hook → `tsc` never ran on publish.
  - `files` array did not include `dist/` and `openclaw.extensions` pointed at the `.ts` source.
  - `tsconfig.json` had `noEmit: true` (typecheck-only), so even a manual `npm run build` produced nothing.
- **Fix:** split into `tsconfig.json` (typecheck) + `tsconfig.build.json` (emit to `./dist`). Added `prepack` + `prepublishOnly` scripts that clean + build. Updated `files`, `main`, `types`, `exports`, and `openclaw.extensions` to point at `./dist/index.js`. Rewrote all 24 internal import specifiers from `.ts` → `.js` for NodeNext-compatible emit.
- **No tool / schema changes** — same 124 tools as `2026.5.3`. Republish-only.

## 2026.5.3 (2026-05-03)

### Added
- **TikTok Ads complete overhaul (#299 Phase 0+1)** — closes every customer-blocking bug class on the TikTok integration. Brings TikTok to feature parity with Meta on schema depth.
- **3 new TikTok read-only discovery tools:**
  - `list_tiktok_identities` — surfaces every identity (BC_AUTH_TT, TT_USER, CUSTOMIZED_USER) with `would_resolve_to` + `usable_for_new_ads` flags. Helps the LLM avoid TikTok's "Custom identities are no longer supported" error by showing which identity Adspirer auto-picks.
  - `list_tiktok_custom_audiences` — DMP custom audiences for `audience_ids` / `excluded_audience_ids` targeting on ad groups.
  - `list_tiktok_saved_audiences` — re-usable demographic + interest audience presets.

### Changed (schema depth — Phase 1)
- **`add_tiktok_ad_group`: 22 → 65 fields (+43)** — full TikTok `AdgroupCreateBody` SDK depth exposed via Pydantic schema. New themes: bidding (bid_price, bid_type, conversion_bid_price, deep_bid_type, deep_cpa_bid, roas_bid, skip_learning_phase, bid_display_mode), scheduling (schedule_type, dayparting), frequency (frequency, frequency_schedule), demo + device targeting (device_model_ids, device_price_ranges, network_types, carrier_ids, spending_power, household_income, contextual_tag_ids), deep audience (audience_rule, audience_type, included/excluded_custom_actions, interest_keyword_ids, interest_keywords, purchase_intention_keyword_ids), Pangle + brand safety (blocked_pangle_app_ids, included/excluded_pangle_audience_package_ids, brand_safety_partner, brand_safety_type), creative engagement (auto_targeting_enabled, targeting_expansion, pacing, creative_material_mode, is_hfss, statistic_type, secondary_optimization_event, next_day_retention, conversion_id).
- **`update_tiktok_ad_group`: 6 → 64 fields (+58)** — was capped at name/budget/age_groups/gender/location_ids/schedule_end_time. Now full `AdgroupUpdateBody` parity, sharing the same deep-fields mixin as create.
- **`add_tiktok_ad`: 17 → 39 fields (+22)** — 22 new creative fields: tracking URLs (click/impression/video_view/tracking_pixel_id), deep links (deeplink, deeplink_type, fallback_type), disclaimer (text + clickable_texts + type for regulated industries), video engagement (vertical_video_strategy, item_duet_status, item_stitch_status, promotional_music_disabled, creative_authorized, dark_post_status), brand-safety creative (postbid_partner, vast_url, viewability_postbid_partner, viewability_vast_url, vast_moat_enabled), lead form (page_id).
- **`create_tiktok_video_campaign`: +1 field** (`video_id` alongside `video_url` for pre-uploaded video reuse — fixes repeated "Field required: video_url" errors when customers had already uploaded the video).

### Validators
- **`dayparting`** — enforces 336-character binary (48 half-hour slots × 7 days). The TikTok SDK docstring saying 96 was incorrect; live-verified 2026-05-04. Rejects non-binary input with helpful error.
- **`identity_type=CUSTOMIZED_USER` rejected pre-flight** on `add_tiktok_ad` (TikTok deprecated 2026-04 — fixes a customer-reported "Custom identities are no longer supported" error).
- **CONVERSIONS objective** requires `pixel_id` + `optimization_event` pre-flight.
- **One-of: `video_url` / `video_id` / `tiktok_item_id`** required on video campaigns.

### Notes
- Phase 0 (commit `5b40ac3`) shipped: error envelope, image-slideshow rewrite, URL fetcher hardening, identity resolver fix, MCP federation routing.
- Phase 1 (commit `8a4187b`) shipped: full SDK schema depth via shared `TikTokAdGroupDeepFields` mixin + generic passthrough at MCP execute layer.
- All schema changes verified via MCP federation E2E on TJ's prod account (DISABLE+DELETE) — 8/8 green for Phase 1.
- Tool count: 124 (unchanged — schema depth grew without adding tools).

## 2026.4.17 (2026-04-27)

### Added
- **Display Ads functional gaps end-to-end (#301)** — 5 net-new Google Ads tools to close customer-reported gaps:
  - `update_location_targeting` — flip a campaign's geo-target mode (`PRESENCE` vs `PRESENCE_OR_INTEREST` vs `SEARCH_INTEREST`) without recreating it. Channel-aware: rejects `PRESENCE_OR_INTEREST` on Display negative side and `SEARCH_INTEREST` on Display, auto-defaults Display negative to `PRESENCE`.
  - `remove_sitelinks` — unlink sitelink extensions from a campaign. Targeted (by `asset_ids`) or bulk (omit to unlink all). Footgun guard: empty list rejects with helpful error. Idempotent (already-REMOVED rows skipped). Closes #157.
  - `remove_callouts` — unlink callout extensions from a campaign, same shape as `remove_sitelinks`.
  - `get_ad_policy_violations` — read-only. Returns approval/review status + topic entries (with text/URL evidence + constraints) for ads. Filters: `ad_id` / `ad_group_id` / `campaign_id` (at least one required), `status_filter` (DISAPPROVED, LIMITED, DISAPPROVED_OR_LIMITED, ALL).
  - `create_conversion_action` — create WEBPAGE conversion goals so the account can use TARGET_CPA / MAXIMIZE_CONVERSIONS / TARGET_ROAS bidding. 21 categories supported; value/currency/lookback/counting/attribution-model controls.

### Changed
- **Improved `update_bid_strategy` TARGET_CPA error message** — when called without `target_cpa`, the error now references `create_conversion_action` and explains the full setup flow (create goal → install Google tag → wait 24-48h → switch bidding).
- **Improved Google Ads image fetch error messages** (#301 PR-1) — when an image URL can't be fetched (DNS failure, 4xx/5xx, HTML response), the error now points customers to host on a URL they own (their website / S3 / GCS) rather than failing silently. DemandGen image fetcher also adopts the Display UA-headers pattern, fixing previously-silent 403s on CDNs that block default Python UA.

### Notes
- Customer gap #6 (`upload_local_image`) was deliberately scoped out — Adspirer is not a customer image host.

## 2026.4.16 (2026-04-25)

### Added
- **Meta App Promotion campaigns end-to-end (#296)** — full E2E support for OUTCOME_APP_PROMOTION across all 5 Meta tools:
  - `create_meta_image_campaign`, `create_meta_video_campaign`, `create_meta_carousel_campaign`, `add_meta_ad_set` accept new app fields: `app_id` (Facebook App ID, numeric — NOT iOS bundle ID or Android package name), `app_store_url` (apps.apple.com/... or play.google.com/...), `app_event_type` (in-app event for optimization), `deep_link_url_ios` + `deep_link_url_android` (deep-link installed users to specific app screens)
  - `add_meta_ad` and `create_meta_dco_ad` accept the 2 deep-link fields (deep links live on ad creative, not promoted_object)
  - `update_meta_ad_set` accepts `app_id` / `app_store_url` / `app_event_type` for in-place app campaign updates (e.g. swap optimized event from APP_INSTALLS → PURCHASE)
  - `select_meta_campaign_type` accepts new `'app'` option with full guided workflow prompt
  - `duplicate_meta_campaign` confirmed to preserve all app fields (analytical verification)
  - `update_meta_ad_set` `optimization_goal` documented to accept new app values: `APP_INSTALLS`, `APP_INSTALLS_AND_OFFSITE_CONVERSIONS`, `OFFSITE_CONVERSIONS`, `IN_APP_VALUE`, `VALUE`
  - `get_meta_campaign_details` and `list_meta_ad_sets` formatters surface dedicated **App Campaign** sections showing promoted FB App ID, store platform (iOS App Store / Google Play), and optimization target

### Changed
- **Meta video creative deep-link rejection** — schemas + helpers + routes all reject `deep_link_url_ios` / `deep_link_url_android` on `OUTCOME_APP_PROMOTION` video ads (Meta's `AdCreativeVideoData` has no `app_link_spec` field — same SDK constraint as `display_link` per #295). Use image or carousel for app deep links.

## 2026.4.15 (2026-04-25)

### Changed
- **Regenerated schemas for Display Ads bugfixes (#294)** — picks up 7 user-hit bugs closed from production tool_call_logs

## 2026.4.14 (2026-04-23)

### Changed
- **Regenerated schemas for Meta Ads #295** — 7 items landed:
  - Item 1: multi-text `asset_feed_spec` for single-image + single-video, DCO video restriction lifted
  - Item 2: Advantage+ Audience flag with safe update merge
  - Items 3+4: granular Advantage+ Creative opt-outs via `degrees_of_freedom_spec` (16 feature fields), `standard_enhancements` rejection, creative_helpers foundation
  - Item 5: `display_link` / `caption` on write + read paths (image + carousel; rejected on video)
  - Item 6: `ad_set_name` + `ad_name` write with response echo verification
  - Item 7: `multi_advertiser` enroll_status surfaced in read path
- `switch_primary_account` — single `account_id` now ADDS instead of REPLACES (multi-account fix)

## 2026.4.13 (2026-04-23)

### Added
- **Google Display Ads end-to-end (#294)** — 22 new tools for full CRUD on Display campaigns and Smart Display:
  - `create_display_campaign`
  - Ad groups: `add_display_ad_group`, `update_display_ad_group`, `remove_display_ad_group`, `get_display_ad_group_settings`
  - Ads: `add_display_ad`, `update_display_ad_creative`, `remove_display_ad`
  - Targeting: `add_display_audiences`/`get_display_audiences`, `add_display_keywords`/`get_display_keywords`, `add_display_placements`/`get_display_placements`, `add_display_topics`/`get_display_topics`, `update_display_demographics`/`get_display_demographics`, `remove_display_criteria`
  - Controls: `update_display_frequency_caps`/`get_display_frequency_caps`, `update_display_campaign_schedule`
- Location/language helpers: `list_google_languages`, `resolve_google_locations`
- **Tool count: 95 → 119**

## 2026.4.12 (2026-04-22)

### Added
- New `list_connected_accounts` tool (multi-account support #274) — lists all connected ad accounts across platforms with optional platform filter
- **Tool count: 94 → 95**

### Changed
- **Regenerated schemas** — picks up all changes since v2026.4.11:
  - Multi-account support (#274): Phase 1-2, `switch_primary_account` multi-account activation, `account_id` parameter wired through all tools
  - `raw_data=true` flag added to performance tools across all 4 platforms
  - Federated router descriptions updated with raw_data flag
  - Google anomaly schema improvements
  - TikTok optimization_event values corrected
  - 5 TikTok campaign creation bugs fixed (#262)
  - `list_meta_custom_audiences` timeout + status display fix (#264)
  - REST API surface (X-Client-Type: cli for flat tool list)

## 2026.4.11 (2026-04-08)

### Fixed
- **Regenerated schemas** — picks up Meta Ads #264 fixes (add_meta_ad_set parity, custom conversion, serialization), custom audiences API v21.0 fix, date_range preset pass-through, customer_id_override validation, delete_monitor tool + improved monitoring descriptions

## 2026.4.10 (2026-04-06)

### Fixed
- **Regenerated schemas** — picks up `add_meta_ad_set` CBO support (budget_daily now optional, new campaign_budget_optimization, daily_min_spend_target, daily_spend_cap fields), `update_bid_strategy` MAXIMIZE_CLICKS fix

## 2026.4.9 (2026-04-06)

### Changed
- **Regenerated static-fallback.ts from server registry** — picks up all new tools from last 7 days: Google Ads `get_campaign_targeting`, Meta Ads `list_meta_custom_audiences`, removed stale individual TikTok tools (now behind `tiktok_ads` router)
- **Tool count: 95 → 94** (Google 51, Meta 37, 3 routers, 1 audit, 2 account = 94 direct schemas; 184 total including 93 tools behind routers)
- **Full SKILL.md rewrite** — TikTok Ads now fully documented with router pattern (29 tools: campaign management, Spark Ads, carousel, APP_PROMOTION, performance analytics, geo analysis). Updated LinkedIn (50 tools), Meta (37 tools including custom audiences), Google (51 tools including campaign targeting). Accurate tool counts and router invocation examples throughout.
- **tool-groups.ts updated** — added `get_campaign_targeting` (Google), `list_meta_custom_audiences` (Meta)
- **Generator script fixed** — sub-server paths appended after hub to prevent `platforms/` package shadowing

## 2026.4.8 (2026-04-01)

### Fixed
- **Tools not callable in OpenClaw >=2026.3.22** — OpenClaw's `registerTool` API now requires `@sinclair/typebox` `TSchema` for `parameters`, not raw JSON Schema objects. All 95 tool schemas are now wrapped with `Type.Unsafe()` to satisfy the `AgentTool<TSchema>` contract.
- **`needsConfirmation` option removed** — replaced with `{ name: toolName }` per the new `OpenClawPluginToolOptions` type (`{ name?, names?, optional? }`).
- **Plugin entry point migrated** — now uses `definePluginEntry()` from `openclaw/plugin-sdk/plugin-entry` instead of a raw default export object.
- Minimum OpenClaw peer dependency bumped to `>=2026.3.22`.

## 2026.4.7 (2026-04-01)

### Changed
- **Full SKILL.md rewrite for federated MCP** — teaches the LLM the router tool pattern for LinkedIn and Monitoring. Every workflow section updated with correct invocation examples. Clear "Direct vs Router" tool type distinction at the top. LLM now knows to call `linkedin_ads` and `monitoring_and_reporting` with `action: "list_tools"` then `action: "execute"`.

## 2026.4.6 (2026-04-01)

### Fixed
- SKILL.md had stale tool counts (103 tools, old individual tool names). Partial update.

## 2026.4.5 (2026-04-01)

### Changed
- **Federated MCP architecture (#255)** — Monitoring (12 tools) and LinkedIn Ads (43 tools) are now fully federated to dedicated sub-servers. Individual tools replaced by `monitoring_and_reporting` and `linkedin_ads` router meta-tools.
- Tool count: 148 → 95 (55 individual tools replaced by 2 routers)
- Static fallback regenerated with router tool schemas
- Router tools mapped to original groups (`monitoring_and_reporting` → `manus`, `linkedin_ads` → `linkedin_ads`) so existing user `enabledGroups` configs continue to work
- Generator script updated to import from sub-server directories

### Removed
- Individual manus tools (schedule_brief, list_monitors, etc.) from ALL_TOOLS — replaced by `monitoring_and_reporting` router
- Individual LinkedIn Ads tools from ALL_TOOLS — replaced by `linkedin_ads` router

## 2026.4.2 (2026-04-01)

### Fixed
- **Critical: Skill prompt injected every turn** — `before_agent_start` hook fires per-turn, not per-session. This injected ~15,000 characters on every message, consuming all tokens within minutes. Removed the hook entirely — skill prompt now bundled in `skills/SKILL.md` (OpenClaw native skill system, loaded per-session).
- **Install blocked by security scanner** — Removed `child_process` import (used for browser auto-open during OAuth). OAuth login/connect commands now print the URL for the user to open manually. This passes OpenClaw's dangerous code pattern scanner.

## 2026.3.31 (2026-03-31)

### Added
- **Meta Ads destination_type override** — new `destination_type` parameter on `create_meta_image_campaign`, `create_meta_video_campaign`, `create_meta_carousel_campaign`, and `add_meta_ad_set`. Enables OUTCOME_LEADS with WEBSITE destination for pixel-based lead tracking instead of Meta's built-in lead form. Options: WEBSITE, ON_AD, WEBSITE_AND_ON_AD.
- **Campaign Budget Optimization (CBO)** — new `campaign_budget_optimization` parameter on all 3 campaign creation tools. When enabled, budget is set at campaign level and Meta distributes across ad sets (Advantage Campaign Budget).
- **Google Ads rate guard** — global API rate limiter coordinating MCP, signup sync, and nightly collector to prevent 429 quota exhaustion. Admin UI at /rate-limits for monitoring and per-user limit management.

### Fixed
- Performance tools: learning phase early graduation, budget optimizer daily budget accuracy, anomaly explainer rounding, creative fatigue CTR sign, audience segment N/A display, search term fractional conversions
- Keyword and ad group names showing N/A in `get_campaign_performance`
- Hyphenated Google Ads customer IDs in performance routes

## 2026.3.28 (2026-03-28)

### Added
- **Dynamic Creative Optimization (DCO)** — new `create_meta_dco_ad` tool lets users provide up to 10 images, 5 headlines, 5 primary texts, and 5 descriptions in a single ad. Meta automatically tests all combinations and optimizes delivery.
- `is_dynamic_creative` parameter on `add_meta_ad_set` for creating DCO-enabled ad sets
- DCO fields (`image_urls`, `headlines`, `primary_texts`, `descriptions`) on `add_meta_ad` as secondary path
- Tool count: 148 (Meta Ads 34 → 35)

## 2026.3.25 (2026-03-25)

### Fixed
- **Empty parameter schemas** — all 143 tools now ship with complete `inputSchema` including `properties`, `required`, `$defs`, and type information. Previously, 137 tools had empty `{}` schemas causing AI agents (especially OpenClaw) to send tool calls with no arguments, resulting in "Missing Required Parameter" errors.

### Added
- **41 missing tools** added to tool-groups.ts:
  - Google Ads (11): `select_google_campaign_type`, `create_youtube_campaign`, `create_demandgen_campaign`, `add_demandgen_ad_group`, `search_audiences`, PMax audience signals & search themes management
  - Meta Ads (14): `list_meta_campaigns`, `list_meta_ad_sets`, `list_meta_ads`, `list_meta_pixels`, `list_meta_lead_forms`, `list_meta_instagram_accounts`, `get_meta_lead_form_submissions`, `get_meta_campaign_details`, `get_meta_ad_creatives`, `add_meta_ad_set`, `add_meta_ad`, `update_meta_ad_set`, `update_meta_ad`, `duplicate_meta_campaign`
  - LinkedIn Ads (15): `select_linkedin_campaign_type`, `create_linkedin_video_campaign`, `create_linkedin_carousel_campaign`, `create_linkedin_text_campaign`, `add_linkedin_text_creative`, `add_linkedin_video_creative`, `delete_linkedin_creative`, `clone_linkedin_campaign`, `batch_update_linkedin_campaigns`, `list_linkedin_campaign_groups`, `manage_linkedin_conversions`, campaign budget/group/schedule/targeting updates
  - Audit (1): `audit_conversion_tracking` — new platform group
- **Generator script** (`scripts/generate-static-tools.py`) to regenerate `static-fallback.ts` from the server registry whenever tools or schemas change
- Write-tool classification for `delete_`, `clone_`, `batch_update_`, `duplicate_` prefixed tools

### Changed
- Static fallback schemas are now the **sole source of truth** for tool parameter definitions — removed the unreliable dynamic schema fetch in `before_agent_start` that depended on `registerTool()` overwrite behavior
- Tool count: 103 → 143 (Google 40→50, Meta 20→34, LinkedIn 28→43, Audit 0→1, System/TikTok/Manus unchanged)
- Token budget: ~69K tokens for all tool schemas (slimmed from ~123K full schemas)

### Removed
- `fetchAndRegisterTools()` — dynamic schema fetch that re-registered tools during `before_agent_start`. Replaced by pre-computed static schemas.
- `slimSchema()`, `slimObjectSchema()`, `slimProperty()`, `truncateDescription()` — runtime slimming functions. Slimming is now done at build time by the generator script.
- Stale `account_info` tool reference (never existed on server)
- Duplicate `get_usage_status` registration (was in both `GOOGLE_ADS_TOOLS` and `SYSTEM_TOOLS`)

## 2026.3.16 (2026-03-16)

### Added
- **Placement-specific image creatives** for Meta campaigns — use different images for Feed (1080x1080), Stories/Reels (1080x1920), and Right Column (1200x628) via `asset_feed_spec`
- Auto-detection of Page-Backed Instagram Account (PBIA) for `asset_feed_spec` creatives
- `story_image_url` and `right_column_image_url` parameters for `create_meta_image_campaign` and `add_meta_ad`
- **City-level targeting** for Meta campaigns — target specific cities with optional radius
- **Emoji/rich text support** in Meta `primary_text` — emojis, line breaks, bullet points for higher engagement
- Graceful fallback to standard `object_story_spec` when placement image uploads fail

### Fixed
- Meta `asset_feed_spec` requires explicit `instagram_user_id` (unlike `object_story_spec` which auto-assigns)
- Meta `asset_customization_rules` requires at least 2 rules — single-image fallback prevents API errors

## 2026.3.15 (2026-03-15)

### Added
- **Input format requirements** section in embedded skill prompt — guides AI clients on IDs-as-strings, text length limits, enum casing, budget types, and keywords format
- Server-side input resilience: auto-coerces int IDs to strings, normalizes enum casing, strips whitespace, ignores unknown fields, parses JSON string arrays

### Fixed
- Reduced ~160 preventable validation errors (22% of all tool call failures) through server-side coercion and client-side guidance

## 2026.3.7 (2026-03-07)

### Added
- **API Key authentication** for headless servers, Docker, and CI/CD pipelines
- `openclaw adspirer login --token <key>` — authenticate with a Personal Access Token
- `ADSPIRER_API_KEY` environment variable support (takes priority over stored config)
- `apiKey` config field in `AdspirerConfig` interface
- API key status display in `openclaw adspirer status`

### Changed
- `openclaw adspirer logout` now also clears stored API key
- `openclaw adspirer uninstall` now also clears stored API key
- OAuth token refresh is skipped when using API key authentication
- Version string updated to match package version

## 2026.3.4 (2026-03-03)

### Security
- Replace dynamic `require()` with static imports to avoid obfuscation flags (V-003)
- Sanitize OAuth error responses to prevent credential leakage in logs (V-004)
- Add explicit `permissions` block to `openclaw.plugin.json` with justifications for filesystem, network, localhost, and child_process usage
- Document server-side SSRF protections in skill prompt (HTTPS-only, private IP blocking, content-type validation, size limits)

## 0.2.0 (2026-02-02)

### Fixed
- OAuth flow now works end-to-end (allowed_scopes, correct scopes, token persistence)
- Callback timeout increased to 120s for slower auth flows
- Token storage uses direct config file writes (removed dependency on non-existent api.updateConfig)

## 0.1.1 (2026-02-02)

- Cleaned up documentation for public release
- Removed internal development references

## 0.1.0 (2026-02-02)

### Initial Release

- Dynamic tool registration from Adspirer MCP server (103 tools)
- OAuth 2.1 + PKCE authentication flow
- CLI commands: login, logout, status, accounts, tools
- Tool groups: Google Ads (40), Meta Ads (20), TikTok Ads (4), LinkedIn Ads (28), Manus (8), System (4)
- Read/Write tool safety model with confirmation for mutating operations
- Static fallback tools when server is unreachable
- Configurable server URL, timeout, and tool filtering
- Zero runtime dependencies
