hermoso 0.1.88 → 0.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
5
5
  composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
6
6
  campaigns behind them — all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
7
7
 
8
- **415 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **420 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
9
9
  catalog with exact per-render credit costs plus the full capability map.
10
10
 
11
11
  **It is not all-or-nothing.** Research, creation, publishing/scheduling and ads management are four *independent*
@@ -53,7 +53,7 @@ Cursor / Codex — add to `mcp.json` (Codex uses the TOML equivalent):
53
53
 
54
54
  Then ask your agent: *“Generate an image ad with Hermoso.”*
55
55
 
56
- ### What the 415 tools cover
56
+ ### What the 420 tools cover
57
57
 
58
58
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
59
59
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
package/mcp/tools.mjs CHANGED
@@ -4514,6 +4514,65 @@ export function registerTools(rawServer, opts = {}) {
4514
4514
  outputSchema: { campaignId: z.string().nullable().optional(), lineItemId: z.string().nullable().optional(), status: z.string().optional(), parentStatus: z.string().nullable().optional(), note: z.string().optional() },
4515
4515
  annotations: { openWorldHint: true },
4516
4516
  }, wrap(async (a) => { const d = await apiPost('/api/x-ads/status', a); return ok(d.note, d); }));
4517
+ // ── APPLE ADS (Apple Search Ads) — READ-ONLY (2026-08-14) ──────────────────────────────────────────────────────
4518
+ // Every description says read-only in its own words, because an agent that believes it can build a campaign here
4519
+ // will try, fail, and the user will blame Hermoso ([[prompt-rosters-go-stale]]). When write tools land, these
4520
+ // sentences move with them.
4521
+ server.registerTool('list_apple_ads_campaigns', {
4522
+ title: 'List Apple Ads campaigns',
4523
+ description: 'Read the brand’s connected Apple Ads (Apple Search Ads) campaigns — App Store search ads. Each row carries status, servingStatus, daily and total budget, the countries it runs in and, when a campaign cannot run, servingStateReasons stating exactly why. Read-only, free, zero spend risk. Needs Apple Ads connected (Settings ▸ Connectors ▸ Apple Ads): there is no OAuth — Hermoso generates an EC signing key, the user pastes the public half into Apple Ads ▸ Account Settings ▸ API and pastes back clientId / teamId / keyId. Hermoso CANNOT create or edit Apple Ads campaigns — never offer that.',
4524
+ inputSchema: {
4525
+ limit: z.number().optional().describe('page size, default 100, Apple’s max is 1000'),
4526
+ offset: z.number().optional().describe('offset pagination'),
4527
+ },
4528
+ outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), count: z.number().optional(), total: z.number().nullable().optional(), campaigns: z.array(z.any()).optional(), note: z.string().optional() },
4529
+ annotations: { readOnlyHint: true, openWorldHint: true },
4530
+ }, wrap(async (a) => { const d = await apiGet('/api/apple-ads/campaigns', a); return ok(`${d.count} Apple Ads campaign(s):\n${d.note}`, d); }));
4531
+ server.registerTool('list_apple_ads_ad_groups', {
4532
+ title: 'List Apple Ads ad groups',
4533
+ description: 'Ad groups inside one Apple Ads campaign — status, servingStatus, default bid, and whether Search Match (Apple’s automated keyword matching) is on. campaignId is REQUIRED: Apple publishes no org-wide ad-group list, so an ad group can only be reached through its campaign. Read-only, free.',
4534
+ inputSchema: {
4535
+ campaignId: z.string().describe('REQUIRED — from list_apple_ads_campaigns'),
4536
+ limit: z.number().optional(), offset: z.number().optional(),
4537
+ },
4538
+ outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), campaignId: z.string().optional(), count: z.number().optional(), total: z.number().nullable().optional(), adGroups: z.array(z.any()).optional(), note: z.string().optional() },
4539
+ annotations: { readOnlyHint: true, openWorldHint: true },
4540
+ }, wrap(async (a) => { const d = await apiGet('/api/apple-ads/adgroups', a); return ok(`${d.count} ad group(s) in Apple Ads campaign ${d.campaignId}:\n${d.note}`, d); }));
4541
+ server.registerTool('list_apple_ads_keywords', {
4542
+ title: 'List Apple Ads targeting or negative keywords',
4543
+ description: 'Targeting or negative keywords with their match type, status and bid. Apple splits these four ways and the paths are not interchangeable: TARGETING keywords are listed PER AD GROUP, so pass campaignId AND adGroupId. NEGATIVE keywords (negative:true) exist at campaign level — pass campaignId alone — or per ad group with both ids. Apple allows up to 5000 keywords per ad group. Read-only, free.',
4544
+ inputSchema: {
4545
+ campaignId: z.string().describe('REQUIRED'),
4546
+ adGroupId: z.string().optional().describe('required for TARGETING keywords; optional for negatives'),
4547
+ negative: z.boolean().optional().describe('list negative keywords instead of targeting keywords'),
4548
+ limit: z.number().optional(), offset: z.number().optional(),
4549
+ },
4550
+ outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), campaignId: z.string().optional(), adGroupId: z.string().nullable().optional(), count: z.number().optional(), total: z.number().nullable().optional(), keywords: z.array(z.any()).optional(), note: z.string().optional() },
4551
+ annotations: { readOnlyHint: true, openWorldHint: true },
4552
+ }, wrap(async (a) => { const d = await apiGet('/api/apple-ads/keywords', a); return ok(`${d.count} ${d.level === 'negativeKeyword' ? 'negative ' : ''}keyword(s):\n${d.note}`, d); }));
4553
+ server.registerTool('apple_ads_report', {
4554
+ title: 'Apple Ads performance report',
4555
+ description: 'Apple Ads performance — impressions, taps, installs, spend, TTR, CPT, CPA. level is campaign (organization-wide) or adgroup / keyword / searchterm / ad, and every level except campaign REQUIRES campaignId. startTime and endTime are REQUIRED, as YYYY-MM-DD. Apple enforces three interlocking rules and the reply reports back which were applied: with no granularity row totals are forced on; with granularity grand totals are forced off; grouping by a demographic or geo dimension forces BOTH off — so a total of zero can mean "Apple did not return that total", not "no spend". A report with NO rows genuinely means there was NO delivery in that window: say exactly that, and never present zeros as measured performance. Read-only and free, so run it first after connecting — it proves the credentials work with zero spend risk.',
4556
+ inputSchema: {
4557
+ level: z.enum(['campaign', 'adgroup', 'keyword', 'searchterm', 'ad']).optional().describe('default campaign'),
4558
+ campaignId: z.string().optional().describe('REQUIRED for every level except campaign'),
4559
+ startTime: z.string().describe('YYYY-MM-DD (required)'),
4560
+ endTime: z.string().describe('YYYY-MM-DD (required)'),
4561
+ granularity: z.enum(['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY']).optional(),
4562
+ groupBy: z.array(z.string()).optional().describe('adminArea, ageRange, countryCode, countryOrRegion, deviceClass, gender, locality'),
4563
+ timeZone: z.enum(['ORTZ', 'UTC']).optional().describe('ORTZ = the organization time zone, Apple’s default'),
4564
+ limit: z.number().optional(),
4565
+ },
4566
+ outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), startTime: z.string().optional(), endTime: z.string().optional(), count: z.number().optional(), rows: z.array(z.any()).optional(), grandTotals: z.any().optional(), note: z.string().optional() },
4567
+ annotations: { readOnlyHint: true, openWorldHint: true },
4568
+ }, wrap(async (a) => { const d = await apiPost('/api/apple-ads/report', a); return ok(`${d.note}\n${JSON.stringify((d.rows || []).slice(0, 40))}`, d); }));
4569
+ server.registerTool('list_apple_ads_orgs', {
4570
+ title: 'List Apple Ads organizations',
4571
+ description: 'The Apple Ads organizations these credentials can act as, with each one’s currency, time zone, payment model and the API roles held. Apple treats an orgId like a CAMPAIGN GROUP, so one login can cover several — an agency managing multiple clients has one per client. Reading this does NOT switch organization: Apple Ads is pinned to the ONE organization chosen when the connection was made, so an agent can never act as another client’s campaign group. To use a different one, reconnect Apple Ads and name it there. A payment model of null is worth reporting: Apple states that without one, campaigns cannot run. Read-only, free.',
4572
+ inputSchema: {},
4573
+ outputSchema: { ok: z.boolean().optional(), active: z.string().nullable().optional(), count: z.number().optional(), orgs: z.array(z.any()).optional(), note: z.string().optional() },
4574
+ annotations: { readOnlyHint: true, openWorldHint: true },
4575
+ }, wrap(async () => { const d = await apiGet('/api/apple-ads/orgs', {}); return ok(d.note, d); }));
4517
4576
  // ── X: managing what you built — update, remove, and the reads both depend on (2026-08-05) ────────────────────
4518
4577
  // Hermoso could build an X campaign and activate it and then change NOTHING about it, and could remove nothing at
4519
4578
  // all: X was the eighth ad platform in the product and the only one with no removal path. Every field offered
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI ad studio + marketing MCP (415 tools): build and manage ad campaigns on Meta, Google Ads, Reddit, X, TikTok, Snapchat, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads \u2014 generate finished video, image and UGC avatar ads, publish them to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest, spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus organic TikTok/Instagram/YouTube/Reddit, and read what they achieved in Google Analytics 4. CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
5
+ "description": "AI ad studio + marketing MCP (420 tools): build and manage ad campaigns on Meta, Google Ads, Reddit, X, TikTok, Snapchat, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads \u2014 generate finished video, image and UGC avatar ads, publish them to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest, spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus organic TikTok/Instagram/YouTube/Reddit, and read what they achieved in Google Analytics 4. CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "hermoso": "bin/hermoso.mjs"