hermoso 0.1.201 → 0.1.203

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 (3) hide show
  1. package/README.md +2 -2
  2. package/mcp/tools.mjs +115 -6
  3. package/package.json +2 -2
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
- **781 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **789 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
  **What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
171
171
 
172
172
  Then ask your agent: *“Generate an image ad with Hermoso.”*
173
173
 
174
- ### What the 781 tools cover
174
+ ### What the 789 tools cover
175
175
 
176
176
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
177
177
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
package/mcp/tools.mjs CHANGED
@@ -4141,6 +4141,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
4141
4141
  locationId: z.string().optional().describe("GOOGLE BUSINESS PROFILE — which listing, e.g. 'locations/123' from list_business_locations. Needed when the account manages more than one storefront; it is never chosen for the user."),
4142
4142
  visibility: z.enum(['public', 'unlisted', 'private', 'draft']).optional().describe("how it should be published — DEFAULT 'public' (live). Only pass something else if the user explicitly asked to stage/hide it. Not every channel supports every value; an impossible combination is refused when you schedule it, with the reason."),
4143
4143
  visibilityByChannel: z.record(z.string()).optional().describe('override visibility for one channel, e.g. { "tiktok": "draft" } to go live everywhere but stage TikTok for review'),
4144
+ optimizeCopy: z.boolean().optional().describe('RECOMMENDED when one caption goes to several channels: fit the shared caption to each channel’s own rules at publish time wherever no per-channel caption was written — YouTube gets a keyword title, a structured multi-paragraph description and search tags; Instagram/TikTok hashtags; LinkedIn longer; X/Bluesky short; Pinterest keyword-rich. The angle and every claim stay the author’s; a channel with its own caption is left exactly as written. Off by default so nobody’s words are rewritten unasked.'),
4144
4145
  },
4145
4146
  outputSchema: { id: z.string().optional(), at: z.string().optional(), channels: z.array(z.string()).optional(), label: z.string().optional() },
4146
4147
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
@@ -5082,8 +5083,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
5082
5083
  account: z.string().optional().describe("WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one youtube account connected (several and none named is refused by name, never guessed); omit when there is one."),
5083
5084
  ...HOOK_ATTR,
5084
5085
  videoUrl: z.string().describe('the video to post — a Hermoso render URL or an upload_file url'),
5085
- title: z.string().optional().describe('video title (≤100 chars)'),
5086
- description: z.string().optional().describe('video description (≤5000 chars)'),
5086
+ title: z.string().optional().describe('REQUIRED in practice — the headline every search result and the Shorts feed shows (≤100 chars); an upload with no title is refused'),
5087
+ description: z.string().optional().describe('REQUIRED in practice — what the video shows, a line about the brand, a link and a few #hashtags (≤5000 chars); YouTube search, suggested videos and the Shorts feed rank on it, so an upload with no description (and no caption) is refused'),
5087
5088
  tags: z.array(z.string()).optional().describe('up to 30 tags'),
5088
5089
  privacy: z.enum(['private', 'unlisted', 'public']).optional().describe('default unlisted (anyone-with-link, ad-ready); public = live + searchable on the channel (confirm first); private = eyes-only (cannot run as an ad)'),
5089
5090
  categoryId: z.string().optional().describe('YouTube category id, NUMERIC — default "22" (People & Blogs), which is wrong for most ads. 1 Film & Animation · 2 Autos & Vehicles · 10 Music · 15 Pets & Animals · 17 Sports · 19 Travel & Events · 20 Gaming · 22 People & Blogs · 23 Comedy · 24 Entertainment · 25 News & Politics · 26 Howto & Style · 27 Education · 28 Science & Technology · 29 Nonprofits & Activism. The assignable set is region-specific, so the value is forwarded as given and YouTube has the last word.'),
@@ -6686,6 +6687,92 @@ function buildTools(rawServer, opts = {}, sink = null) {
6686
6687
  const d = await apiPost('/api/meta/audience/delete', a);
6687
6688
  return ok(d.note, d);
6688
6689
  }));
6690
+
6691
+ // ── MESSENGER MARKETING MESSAGES (2026-09-05). Meta's paid re-engagement channel: messages OUTSIDE the 24-hour
6692
+ // window to people who opted in, billed by Meta to the brand's ad account per delivered message. Flow 2 of Meta's
6693
+ // onboarding (user token + marketing_messages_messenger) rides the existing Meta connection. App-Review gated:
6694
+ // until Meta grants the scope it works for app-role holders only, and list_messenger_subscribers says which.
6695
+ // Every endpoint, limit and error code: lib/messenger-marketing.mjs (read live 2026-09-05).
6696
+ server.registerTool('list_messenger_subscribers', {
6697
+ title: 'List Messenger marketing-message subscribers',
6698
+ description: 'The people who OPTED IN to marketing messages from a Facebook Page — their subscription tokens, the only address a paid Messenger marketing message can go to (there is no "message everyone"). Also reports whether this Meta connection carries the marketing_messages_messenger permission; a connection made before it was added must be reconnected. Free.',
6699
+ inputSchema: {
6700
+ pageId: z.string().optional().describe('Facebook Page id (from list_meta_pages); defaults to the brand’s one shared Page'),
6701
+ limit: z.number().optional().describe('1–1000, default 100'),
6702
+ customAudienceIds: z.string().optional().describe('comma-separated Messenger custom audience ids to filter by'),
6703
+ },
6704
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
6705
+ }, wrap(async (a) => {
6706
+ const d = await apiGet('/api/meta/marketing-messages/subscribers', a);
6707
+ const lines = (d.subscribers || []).slice(0, 50).map(r => `• ${r.token}${r.topic ? ` — “${r.topic}”` : ''}${r.status ? ` (${r.status})` : ''}${r.nextEligible ? `, next eligible ${r.nextEligible}` : ''}`);
6708
+ return ok(`${d.note}${lines.length ? `\n${lines.join('\n')}` : ''}${(d.subscribers || []).length > 50 ? `\n…and ${d.subscribers.length - 50} more` : ''}`, d);
6709
+ }));
6710
+ server.registerTool('create_messenger_marketing_campaign', {
6711
+ title: 'Create a Messenger marketing-message campaign',
6712
+ description: 'The container a paid Messenger marketing message is sent from (Meta act_<AD>/message_campaign). Budgets in USD; Meta bills per DELIVERED message against the campaign budget, and creating it sends and charges nothing. dailyBudgetUsd OR lifetimeBudgetUsd (omit both and Meta sets an estimated daily cap; Meta may spend up to 175% of a daily budget on one day, never more than 7× per week). REGION LAW: Meta lets us serve businesses in 20 countries only — US, Mexico, Brazil, India, Australia, Singapore, UAE, Saudi Arabia, Hong Kong, Taiwan, Thailand, Malaysia, Indonesia, Philippines, Vietnam, New Zealand, Chile, Colombia, Peru, Israel — NOT Canada, the EU or the UK — and cannot deliver to people in the EU, UK, Japan, South Korea or Australia. Political Pages are excluded. dryRun:true shows the exact body.',
6713
+ inputSchema: {
6714
+ adAccountId: z.string().describe('ad account id (act_… or digits)'),
6715
+ pageId: z.string().optional().describe('the sending Page; defaults to the brand’s one shared Page'),
6716
+ name: z.string(),
6717
+ dailyBudgetUsd: z.number().optional(),
6718
+ lifetimeBudgetUsd: z.number().optional(),
6719
+ pixelId: z.string().optional().describe('Meta Pixel for offsite conversion attribution'),
6720
+ startTime: z.string().optional().describe('ISO time, now or within 30 days'),
6721
+ endTime: z.string().optional(),
6722
+ dryRun: z.boolean().optional(),
6723
+ },
6724
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
6725
+ }, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/campaign', a); return ok(d.note, d); }));
6726
+ server.registerTool('list_messenger_marketing_campaigns', {
6727
+ title: 'List Messenger marketing-message campaigns',
6728
+ description: 'The marketing-message campaigns on an ad account: name, id, budgets, schedule. Free.',
6729
+ inputSchema: { adAccountId: z.string() },
6730
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
6731
+ }, wrap(async (a) => {
6732
+ const d = await apiGet('/api/meta/marketing-messages/campaigns', a);
6733
+ return ok(`${d.note}${(d.campaigns || []).length ? `\n${d.campaigns.map(c => `• ${c.name || '(unnamed)'} — ${c.campaignId}${c.dailyBudgetUsd != null ? `, $${c.dailyBudgetUsd}/day` : ''}${c.lifetimeBudgetUsd != null ? `, $${c.lifetimeBudgetUsd} lifetime` : ''}`).join('\n')}` : ''}`, d);
6734
+ }));
6735
+ server.registerTool('estimate_messenger_marketing_delivery', {
6736
+ title: 'Estimate Messenger marketing-message delivery and cost',
6737
+ description: 'Meta’s own estimate of how many marketing messages a budget would deliver from a Page and what it would cost, before anything is created. Free. Meta marks the metric “in development”, so treat it as a range.',
6738
+ inputSchema: { adAccountId: z.string(), pageId: z.string().optional(), dailyBudgetUsd: z.number().optional(), lifetimeBudgetUsd: z.number().optional(), lifetimeDays: z.number().optional() },
6739
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
6740
+ }, wrap(async (a) => { const d = await apiGet('/api/meta/marketing-messages/estimate', a); return ok(d.note, d); }));
6741
+ server.registerTool('send_messenger_marketing_message', {
6742
+ title: 'Send a paid Messenger marketing message',
6743
+ description: 'Send a PAID marketing message on Messenger to opted-in subscribers (Meta act_<AD>/messages). One message per subscriber per 12 hours — Meta’s rule, enforced before dispatch and by Meta. message.type: text | button (text + up to 3 web_url buttons) | generic (a card: title, subtitle, image, tap-through url, up to 3 buttons) | media (imageUrl or videoId + buttons). Give subscriptionTokens (≤200 per call, from list_messenger_subscribers) OR customAudienceId of a MESSENGER_SUBSCRIBER_LIST audience of 100+ people for a bulk send. Meta bills the ad account per delivered message; delivery, read and click events arrive on the Page webhook. dryRun:true previews the wire body and sends nothing. Meta’s frequency caps are silent: a refusal saying the person is capped is Meta protecting them, not a broken send.',
6744
+ inputSchema: {
6745
+ adAccountId: z.string(),
6746
+ campaignId: z.string().describe('from create_messenger_marketing_campaign'),
6747
+ subscriptionTokens: z.array(z.string()).optional(),
6748
+ customAudienceId: z.string().optional(),
6749
+ message: z.object({
6750
+ type: z.enum(['text', 'button', 'generic', 'media']),
6751
+ text: z.string().optional(), title: z.string().optional(), subtitle: z.string().optional(),
6752
+ imageUrl: z.string().optional(), videoId: z.string().optional(), url: z.string().optional().describe('card tap-through URL'),
6753
+ buttons: z.array(z.object({ title: z.string(), url: z.string() })).optional(),
6754
+ }),
6755
+ minConversationGapSeconds: z.number().optional().describe('hold the send if the person chatted with the Page within this many seconds'),
6756
+ dryRun: z.boolean().optional(),
6757
+ },
6758
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
6759
+ }, wrap(async (a) => {
6760
+ const d = await apiPost('/api/meta/marketing-messages/send', a);
6761
+ const bad = (d.results || []).filter(r => r.error);
6762
+ return ok(`${d.note}${bad.length ? `\nRefused: ${bad.slice(0, 10).map(r => `${r.token ? r.token.slice(0, 12) + '…' : 'audience'}: ${r.error}`).join('; ')}` : ''}`, d);
6763
+ }));
6764
+ server.registerTool('request_messenger_optin', {
6765
+ title: 'Ask a Messenger contact to subscribe to marketing messages',
6766
+ description: 'Send Meta’s opt-in request (the notification_messages template) to someone ALREADY in a Messenger conversation with the Page, inside the 24-hour window. If they accept, Meta posts a messaging_optin webhook with their subscription token and they appear in list_messenger_subscribers. One identical-title request per person per week. Free to send. The PSID comes from the Page inbox.',
6767
+ inputSchema: { pageId: z.string().optional(), psid: z.string().describe('the person’s Page-scoped id'), title: z.string().describe('what they are subscribing to, ≤65 characters'), imageUrl: z.string().optional(), timezone: z.string().optional().describe('their timezone, e.g. America/New_York'), dryRun: z.boolean().optional() },
6768
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
6769
+ }, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/optin', a); return ok(d.note, d); }));
6770
+ server.registerTool('delete_messenger_marketing_campaign', {
6771
+ title: 'Delete a Messenger marketing-message campaign',
6772
+ description: 'Delete a marketing-message campaign. Irreversible: the first call names it and refuses; confirm:true deletes.',
6773
+ inputSchema: { adAccountId: z.string(), campaignId: z.string(), confirm: z.boolean().optional() },
6774
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
6775
+ }, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/campaign/delete', a); return ok(d.note, d); }));
6689
6776
  // ---------- Google Ads: read + manage (flagship, Meta-parity). Every spend change is confirm-gated. ----------
6690
6777
  server.group('ads');
6691
6778
  // GOOGLE MERCHANT CENTER (2026-08-07) — rides the google_ads connection (same grant, extra `content` scope).
@@ -11267,7 +11354,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
11267
11354
  budget: z.number().describe('budget in the ad account’s currency (not micro-currency — the conversion is handled)'),
11268
11355
  objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
11269
11356
  goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
11270
- bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
11357
+ bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional(),
11271
11358
  bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
11272
11359
  bidAmount: z.number().optional(),
11273
11360
  startTime: z.string().optional().describe('ISO 8601'),
@@ -11287,7 +11374,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
11287
11374
  adAccountId: z.string().optional(),
11288
11375
  budget: z.number().describe('budget in the ad account’s currency'),
11289
11376
  objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
11290
- bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional().describe('default CPC — must fit the campaign objective'),
11377
+ bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional().describe('default CPC — must fit the campaign objective'),
11291
11378
  bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
11292
11379
  goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
11293
11380
  startTime: z.string().optional(),
@@ -11380,7 +11467,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
11380
11467
  name: z.string(),
11381
11468
  budget: z.number().describe('budget in the ad account’s currency, paired with goalType'),
11382
11469
  goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional().describe('default DAILY_SPEND'),
11383
- bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).describe('must fit the campaign objective — CLICKS campaigns take CPC'),
11470
+ bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).describe('must fit the campaign objective — CLICKS campaigns take CPC'),
11384
11471
  bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).describe('MANUAL_BIDDING needs bidAmount'),
11385
11472
  bidAmount: z.number().optional().describe('bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion'),
11386
11473
  startTime: z.string().describe('ISO 8601, e.g. 2026-08-15T00:00:00Z — Reddit rejects the create without one'),
@@ -11432,7 +11519,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
11432
11519
  budget: z.number().optional(),
11433
11520
  goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
11434
11521
  bidAmount: z.number().optional(),
11435
- bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
11522
+ bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional(),
11436
11523
  bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
11437
11524
  startTime: z.string().optional(),
11438
11525
  endTime: z.string().optional(),
@@ -17155,6 +17242,28 @@ function memoryNoteVerdict(text) {
17155
17242
  return adsOut('ads', d.totalAds ?? raw.length, ads, '', 'linkedin');
17156
17243
  }));
17157
17244
 
17245
+ // ── CREATOR SEARCH (2026-09-05): the creators already winning in a niche, folded out of the organic posts Ad Spy
17246
+ // already searches. Real people, scored on median views + engagement + consistency; no marketplace scope needed.
17247
+ server.registerTool('find_creators', {
17248
+ title: 'Find the creators already winning in a niche',
17249
+ description: 'Scan organic TikTok, Instagram Reels and YouTube for a niche across a few query variants, fold the posts into creators, and rank them on median views, engagement rate and how often they show up for that niche; the top rows get follower counts. Real people, not AI actors — for influencer sourcing, UGC casting and partnership prospecting ("who should we send product to?"). About one credit per search call (platforms × queries, default 3 × 3) plus one per enriched profile; repeats inside 20 minutes are free. Then shortlist (save_to_swipefile), check a profile (instagram_profile / fetch_social_data), draft outreach (generate_text), or approve them for Partnership Ads (manage_meta_partnership_creator).',
17250
+ inputSchema: {
17251
+ niche: z.string().describe('product category, topic or hashtag — "calorie tracker app", "matcha", "#cleanbeauty"'),
17252
+ platforms: z.array(z.enum(['tiktok', 'instagram', 'youtube'])).optional().describe('default all three'),
17253
+ limit: z.number().optional().describe('creators to return, 1–30 (default 12)'),
17254
+ queries: z.number().optional().describe('query variants per platform, 1–4 (default 3); each is a paid search call'),
17255
+ minAvgViews: z.number().optional(),
17256
+ minEngagement: z.number().optional().describe('interactions per view, 0–1 (0.05 = 5%)'),
17257
+ enrich: z.boolean().optional().describe('read follower counts for the top 6 (default true, ~1 credit each)'),
17258
+ },
17259
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
17260
+ }, wrap(async (a) => {
17261
+ const d = await apiPost('/api/creators/search', a);
17262
+ const fmt = (v) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(v >= 1e5 ? 0 : 1)}K` : String(v);
17263
+ if (!(d.creators || []).length) return ok(`${d.summary} Nobody passed the floors — widen the niche, lower minAvgViews / minEngagement, or add platforms.`, d);
17264
+ const lines = d.creators.map(c => `• @${c.handle} (${c.platform})${c.name && c.name !== c.handle ? ` — ${c.name}` : ''}: ${c.posts} post${c.posts === 1 ? '' : 's'} in this niche, median ${fmt(c.medianPlays)} views, ${c.engagementRate == null ? 'engagement unknown' : `${(100 * c.engagementRate).toFixed(1)}% engagement`}${c.followers != null ? `, ${fmt(c.followers)} followers` : ''}, score ${c.score}${c.top?.link ? ` — top: ${c.top.link}` : ''}${c.profileUrl ? ` — ${c.profileUrl}` : ''}`);
17265
+ return ok(`${d.note}\n${lines.join('\n')}`, d);
17266
+ }));
17158
17267
  server.registerTool('search_tiktok', {
17159
17268
  _meta: openaiMeta(AD_SPY_URI, 'Searching TikTok videos…', 'Found TikTok videos'),
17160
17269
  title: 'Search TikTok',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.201",
3
+ "version": "0.1.203",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI ad studio and marketing MCP server with 781 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
5
+ "description": "AI ad studio and marketing MCP server with 789 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "hermoso": "bin/hermoso.mjs"