hermoso 0.1.502 → 0.1.504

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 image & video ad
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
- **1176 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **1182 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
  **No markup on models** on Starter, Pro and Studio. Stop overpaying markups on AI models. Plans: [hermoso.ai/pricing](https://hermoso.ai/pricing).
@@ -23,9 +23,9 @@ charges per API request (`hermoso_credits` prints the exact credits per X post).
23
23
 
24
24
  **What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
25
25
  Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus
26
- product feeds in Google Merchant Center. Publishing and scheduling — **fourteen** channels: Facebook, Instagram,
27
- Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram, WordPress, Discord, Slack and Google Business Profile. Messaging: WhatsApp (you message a
28
- person, so it is not a fifteenth publishing channel) and DM automations on Instagram, Facebook Pages and
26
+ product feeds in Google Merchant Center. Publishing and scheduling — **fifteen** channels: Facebook, Instagram,
27
+ Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram, WordPress, Discord, Slack, Whop and Google Business Profile. Messaging: WhatsApp (you message a
28
+ person, so it is not a sixteenth publishing channel) and DM automations on Instagram, Facebook Pages and
29
29
  Messenger, and X (a keyword comment, a DM, a story reply or an ig.me / m.me link gets an automatic reply). Ad research: the Meta, Google and LinkedIn ad libraries plus organic TikTok,
30
30
  Instagram, YouTube, Threads and Reddit. Analytics: Google Analytics 4, Google Search Console and every
31
31
  connected platform's own post and campaign insights. Files: Google Drive, Sheets, Docs and OneDrive.
@@ -242,7 +242,7 @@ block entirely if you signed in above; it is there for CI, where the process can
242
242
 
243
243
  Then ask your agent: *“Generate an image ad with Hermoso.”*
244
244
 
245
- ### What the 1176 tools cover
245
+ ### What the 1182 tools cover
246
246
 
247
247
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
248
248
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
@@ -278,7 +278,7 @@ Real faces from your own photos need a paid plan; AI people work on every plan.
278
278
  per-render credit cost) with no ad framing: `generate_image` / `generate_video` with `useBrand:false`,
279
279
  `generate_voice`, `generate_text`. The image default is Nano Banana 2.1.
280
280
 
281
- **Publish to your own channels** — **fourteen** of them: Facebook, Instagram and Threads (`post_to_meta`), TikTok
281
+ **Publish to your own channels** — **fifteen** of them: Facebook, Instagram and Threads (`post_to_meta`), TikTok
282
282
  (`post_to_tiktok`), YouTube (`post_to_youtube` + `update_youtube_video`, `youtube_video_insights`, comments
283
283
  read/reply), X (`post_to_x`, `x_post_metrics`, `x_post_insights`, `x_mentions`, `list_x_dms`, `send_x_dm`),
284
284
  LinkedIn profile **and** company Pages (`post_to_linkedin`, `post_to_linkedin_page`), Pinterest
@@ -286,7 +286,8 @@ LinkedIn profile **and** company Pages (`post_to_linkedin`, `post_to_linkedin_pa
286
286
  `list_bluesky_convos` / `read_bluesky_dm` / `send_bluesky_dm`) and Telegram (`post_to_telegram`,
287
287
  `delete_telegram_message`, `list_telegram_chats`), WordPress (`post_to_wordpress`, `update_wordpress_post`,
288
288
  `delete_wordpress_post`, `list_wordpress_posts`), Discord and Slack announcements (`post_to_discord`, `edit_discord_message`,
289
- `delete_discord_message`, `post_to_slack`) and Google Business Profile (below). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
289
+ `delete_discord_message`, `post_to_slack`), Whop (`post_to_whop`, `edit_whop_post`, `delete_whop_post`, `get_whop_post`,
290
+ `list_whop_destinations`) and Google Business Profile (below). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
290
291
  you one content calendar over exactly that set. `upload_file` brings in any external or local media, not just
291
292
  Hermoso renders.
292
293
  *X posting bills credits per API call (X charges per request); a post containing a link costs 13× one without.*
package/mcp/client.mjs CHANGED
@@ -152,7 +152,7 @@ export function signInMessage(status, body, msg, isSignedOut = signedOut()) {
152
152
  // unwrap the {data}|{error} envelope; throw a clean Error (with .status) on failure
153
153
  async function unwrap(res, { read = false } = {}) {
154
154
  let body = null;
155
- try { body = await res.json(); } catch {}
155
+ try { body = await res.json(); } catch (e) { if (read && isDeadline(e)) throw readDeadlineError(); } // a body cut off by the read deadline is not an empty answer
156
156
  if (!res.ok) {
157
157
  const msg = signInMessage(res.status, body, (body && (body.error || body.message)) || `HTTP ${res.status}`);
158
158
  // `_viaApi` MARKS AN ERROR THAT ALREADY REACHED THE SERVER, so route() has already recorded it in the error
@@ -299,14 +299,26 @@ async function throughFrontDoor(send, { read }) {
299
299
  return res;
300
300
  }
301
301
 
302
- async function fetchRead(url, init) { return throughFrontDoor(() => fetchReadOnce(url, init), { read: true }); }
302
+ // ── A READ HAS A DEADLINE (2026-11-04) ────────────────────────────────────────────────────────────────────────────
303
+ // fetch has none of its own, only undici's 300-second wait for response headers, and that wait ends in the same bare
304
+ // "fetch failed" as a dropped socket, so the blip retry below SENT THE SLOW READ AGAIN: one hosted
305
+ // read_meta_conversation sat open for the whole 10-minute MCP wall and held off every deploy. A read now ends at
306
+ // READ_DEADLINE_MS with a sentence, and a deadline is never retried (resending a read the server is still working on
307
+ // only doubles the wait). No read a tool makes runs this long: the slowest seen in a week of request logs was under
308
+ // 100 seconds, which is also the edge's limit for a stdio client. A read changes nothing, so ending it is safe.
309
+ export const READ_DEADLINE_MS = Number(process.env.HERMOSO_READ_DEADLINE_MS) > 0 ? Number(process.env.HERMOSO_READ_DEADLINE_MS) : 180000;
310
+ const isDeadline = (e) => !!e && (e.name === 'TimeoutError' || e.name === 'AbortError' || /UND_ERR_(HEADERS|BODY)_TIMEOUT/.test(String(e.cause?.code || '')));
311
+ const readDeadlineError = () => Object.assign(new Error(`Hermoso did not finish that read within ${Math.round(READ_DEADLINE_MS / 1000)} seconds, so it was stopped and nothing came back. It was a read, so nothing was changed. Try again, or ask for less (a smaller limit or one account).`), { status: 504, _readDeadline: true });
312
+ async function fetchRead(url, init) { return throughFrontDoor(() => fetchReadOnce(url, { ...init, signal: AbortSignal.timeout(READ_DEADLINE_MS) }), { read: true }); }
303
313
  async function fetchReadOnce(url, init) {
304
314
  try { return await fetch(url, init); }
305
315
  catch (e) {
316
+ if (isDeadline(e)) throw readDeadlineError();
306
317
  if (!isTransport(e)) throw e;
307
318
  await new Promise((r) => setTimeout(r, 400));
308
319
  try { return await fetch(url, init); }
309
320
  catch (e2) {
321
+ if (isDeadline(e2)) throw readDeadlineError();
310
322
  if (!isTransport(e2)) throw e2;
311
323
  throw Object.assign(new Error('Could not reach Hermoso just now — the request never got a response, so nothing was read. This is a connection problem, not a rejected request; try again.'), { _transport: true, _viaApi: true });
312
324
  }
@@ -1281,7 +1281,7 @@ const PROVIDER_INFO = {
1281
1281
  tiktok: ['TikTok', 'channels'], youtube: ['YouTube', 'channels'], linkedin: ['LinkedIn', 'channels'], x: ['X', 'channels'],
1282
1282
  pinterest: ['Pinterest', 'channels'], bluesky: ['Bluesky', 'channels'], telegram: ['Telegram', 'channels'],
1283
1283
  google_business: ['Google Business Profile', 'channels'], whatsapp: ['WhatsApp', 'channels'],
1284
- wordpress: ['WordPress', 'channels'], discord: ['Discord', 'channels'], slack: ['Slack', 'channels'],
1284
+ wordpress: ['WordPress', 'channels'], discord: ['Discord', 'channels'], slack: ['Slack', 'channels'], whop: ['Whop', 'channels'],
1285
1285
  google_ads: ['Google Ads', 'ads'], tiktok_ads: ['TikTok Ads', 'ads'], linkedin_ads: ['LinkedIn Ads', 'ads'], microsoft_ads: ['Microsoft Advertising', 'ads'],
1286
1286
  reddit_ads: ['Reddit Ads', 'ads'], pinterest_ads: ['Pinterest Ads', 'ads'], snapchat_ads: ['Snapchat Ads', 'ads'], x_ads: ['X Ads', 'ads'],
1287
1287
  openai_ads: ['ChatGPT Ads', 'ads'], apple_ads: ['Apple Ads', 'ads'], applovin_ads: ['AppLovin Ads', 'ads'],
@@ -1296,10 +1296,10 @@ export const providerInfo = (p) => {
1296
1296
  return { name, group: /_ads$/.test(String(p)) ? 'ads' : 'data' };
1297
1297
  };
1298
1298
  // The publishing channels a connector carries, for the "post here by default" settings. The ids are schedule_post's.
1299
- // WordPress, Discord and Slack publish through their own connector rows (lib/channel-registry.mjs). Whop is left out
1300
- // while its tools ship dark (mcp/roster-scope.mjs WHOP_DARK_TOOLS): a default nobody can post to is not offered.
1301
- const CHANNELS_OF = { meta: ['facebook', 'instagram'], instagram: ['instagram'], threads: ['threads'], tiktok: ['tiktok'], youtube: ['youtube'], linkedin: ['linkedin'], x: ['x'], pinterest: ['pinterest'], google_business: ['google_business'], bluesky: ['bluesky'], telegram: ['telegram'], wordpress: ['wordpress'], discord: ['discord'], slack: ['slack'] };
1302
- const CHANNEL_LABEL = { facebook: 'Facebook', instagram: 'Instagram', threads: 'Threads', tiktok: 'TikTok', youtube: 'YouTube', linkedin: 'LinkedIn', x: 'X', pinterest: 'Pinterest', google_business: 'Google Business Profile', bluesky: 'Bluesky', telegram: 'Telegram', wordpress: 'WordPress', discord: 'Discord', slack: 'Slack' };
1299
+ // WordPress, Discord, Slack and Whop publish through their own connector rows (lib/channel-registry.mjs). A gated
1300
+ // connector (Whop while WHOP_API_KEY is unset) is not in the server's connector list, so it never surfaces here.
1301
+ const CHANNELS_OF = { meta: ['facebook', 'instagram'], instagram: ['instagram'], threads: ['threads'], tiktok: ['tiktok'], youtube: ['youtube'], linkedin: ['linkedin'], x: ['x'], pinterest: ['pinterest'], google_business: ['google_business'], bluesky: ['bluesky'], telegram: ['telegram'], wordpress: ['wordpress'], discord: ['discord'], slack: ['slack'], whop: ['whop'] };
1302
+ const CHANNEL_LABEL = { facebook: 'Facebook', instagram: 'Instagram', threads: 'Threads', tiktok: 'TikTok', youtube: 'YouTube', linkedin: 'LinkedIn', x: 'X', pinterest: 'Pinterest', google_business: 'Google Business Profile', bluesky: 'Bluesky', telegram: 'Telegram', wordpress: 'WordPress', discord: 'Discord', slack: 'Slack', whop: 'Whop' };
1303
1303
  export const ALL_CHANNELS = Object.freeze(Object.keys(CHANNEL_LABEL));
1304
1304
 
1305
1305
  // THE HOME SCREEN'S CARD FOR AN ACCOUNT WITH NOTHING TO POST TO (2026-11-02): the first channels on offer, in the server's
@@ -250,6 +250,16 @@ export const SHOPIFY_STORE_NOTE = 'works only for a Shopify store that installed
250
250
  // them on everywhere with no edit here. Until then the docs, the counts and every roster leave them out.
251
251
  export const WHOP_DARK_TOOLS = Object.freeze(['post_to_whop', 'list_whop_destinations', 'list_whop_products', 'edit_whop_post', 'delete_whop_post', 'get_whop_post']);
252
252
  export const FLAGGED_OFF_TOOLS = new Set(['reangle_video', ...WHOP_DARK_TOOLS]);
253
+ // THE PRODUCTION FLAG THAT LIFTS A DARK TOOL (2026-11-03, owner: count and document the Whop tools whenever their gate
254
+ // is open in prod, keep the automatic gate). A tool named here is dark only while that CONNECTOR_GATES flag is off in
255
+ // production, as recorded in docs-site/prod-gate-flags.json, the file the docs build already reads. The counts and the
256
+ // docs reference read it through darkToolsUnder(); the per-caller roster still follows `flagsOn` from the server.
257
+ // reangle_video has no entry: it follows its own REANGLE_VIDEO switch, which no production record carries.
258
+ export const DARK_TOOL_GATE_FLAG = Object.freeze(Object.fromEntries(WHOP_DARK_TOOLS.map((n) => [n, 'WHOP_API_KEY'])));
259
+ export function darkToolsUnder(flags) {
260
+ const f = flags && typeof flags === 'object' ? flags : {};
261
+ return new Set([...FLAGGED_OFF_TOOLS].filter((n) => !(DARK_TOOL_GATE_FLAG[n] && f[DARK_TOOL_GATE_FLAG[n]] === true)));
262
+ }
253
263
  export function toolFlaggedOff(name, conn) { const n = String(name || ''); return FLAGGED_OFF_TOOLS.has(n) && !(conn && conn.flagsOn instanceof Set && conn.flagsOn.has(n)); }
254
264
  export function toolWithheldByServer(name, conn) { return toolFlaggedOff(name, conn) || !!(conn && ((conn.withheld instanceof Set && conn.withheld.has(String(name || ''))) || toolHeldForNoShopifyStore(name, conn))); }
255
265
  export function toolHeldBackByConnectors(name, conn) {
package/mcp/tools.mjs CHANGED
@@ -3207,7 +3207,7 @@ export const WIDGET_HOST_DESCRIPTIONS = Object.freeze({
3207
3207
  list_linkedin_events: "The events a LinkedIn company Page organizes: name, link, start and end times, online or in person, visibility and state. Only Pages shared with this profile can be read. It needs LinkedIn's Events permission; if a connection made before that permission was enabled is refused, the user reconnects LinkedIn once. Read-only, 0 credits. Needs LinkedIn connected.",
3208
3208
  edit_timeline: "Render one edited video from a timeline the caller describes: segments placed on an output timeline (later segments are drawn on top, and overlapping segments form the transition), each with keyframed position, scale, rotation, speed and opacity, plus optional HTML overlays and masks. Sources are a videoUrl, a Library or render URL, a direct file, or a public TikTok, Reel, Facebook, X or YouTube post link; in and out are source seconds. Audio per segment: gain, mute, and J-cut or L-cut timing. video_frames shows exact frames for free, to find a cut point and to check the result. Costs the flat post_edit price (about 2 credits); a 'generate' segment, which renders a new transition shot with a video model, costs more and is used only when the user asks for one.",
3209
3209
  list_openai_ads_feeds: "The product feeds on this ChatGPT Ads account: id, name, countries, currencies and the campaigns using each. A product-feed campaign advertises the products in one of these feeds. Read-only, 0 credits. Needs ChatGPT Ads connected.",
3210
- create_openai_ads_feed: "Create an empty product feed on ChatGPT Ads, with a name and the countries it serves (ISO codes). Products are added afterwards with update_openai_ads_feed_products. Needs ChatGPT Ads connected.",
3210
+ create_openai_ads_feed: "Create an empty feed on ChatGPT Ads, with a name and the countries it serves (ISO codes). By default it is a product feed, and products are added afterwards with update_openai_ads_feed_products. inventoryType 'hotel' asks for a hotel property feed instead, which OpenAI accepts only for advertisers in its hotel beta and refuses for everyone else. Needs ChatGPT Ads connected.",
3211
3211
  update_reddit_ads_ad_group: "Edit an existing Reddit ad group: its name, budget, goal type, bid, schedule dates or targeting. Budget and bid are amounts in the ad account's currency. Targeting is updated per key: each targeting key sent replaces that whole list, keys left out stay as they are, and an empty list clears a key. It does not start or pause delivery (set_reddit_ads_status does). The saved ad group is read back from Reddit. Needs Reddit Ads connected.",
3212
3212
  update_reddit_ads_saved_audience: "Edit a Reddit saved audience: its name or its targeting. Targeting is updated per key: each targeting key sent replaces that whole list, keys left out stay as they are, and an empty list clears a key. Changing the targeting of an audience that live ad groups use changes who those ad groups reach right away, so tell the user how many ad groups use it and get a yes first. The saved audience is read back from Reddit. Needs Reddit Ads connected.",
3213
3213
  });
@@ -5099,14 +5099,16 @@ function buildTools(rawServer, opts = {}, sink = null) {
5099
5099
 
5100
5100
  server.registerTool('get_whop_post', {
5101
5101
  title: 'Read a Whop post',
5102
- description: "Read back a Whop forum post or chat message Hermoso posted: its text and the numbers Whop reports (views, likes and comments on a forum post; views and reactions on a chat message). A count Whop does not report is listed as unavailable, never zero. Free.",
5103
- inputSchema: { ...MANAGE_BRAND, id: z.string(), kind: z.enum(['forum', 'chat']) },
5104
- outputSchema: { id: z.string().optional(), kind: z.string().optional(), title: z.string().nullable().optional(), content: z.string().optional(), metrics: z.record(z.number()).optional(), absent: z.record(z.string()).optional(), note: z.string().optional() },
5102
+ description: "Read back a Whop forum post or chat message Hermoso posted: its text, the numbers Whop reports (views, likes and comments on a forum post; views and reactions on a chat message), and whether it is still up — state live, deleted or unconfirmed. Whop's read also returns a DELETED chat message, so pass the chat's experienceId (from list_whop_destinations) to confirm a message is still in the chat; one deleted through Hermoso always reads deleted. A count Whop does not report is listed as unavailable, never zero. Free.",
5103
+ inputSchema: { ...MANAGE_BRAND, id: z.string(), kind: z.enum(['forum', 'chat']), experienceId: z.string().optional().describe('the chat it was posted in: confirms a chat message is still up') },
5104
+ outputSchema: { id: z.string().optional(), kind: z.string().optional(), title: z.string().nullable().optional(), content: z.string().optional(), deleted: z.boolean().optional(), state: z.string().optional(), deletedAt: z.string().optional(), metrics: z.record(z.number()).optional(), absent: z.record(z.string()).optional(), note: z.string().optional() },
5105
5105
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
5106
5106
  }, wrap(async (a) => {
5107
- const d = await apiGet('/api/whop/post', queryBrand(a, { id: a.id, kind: a.kind }));
5107
+ const d = await apiGet('/api/whop/post', queryBrand(a, { id: a.id, kind: a.kind, ...(a.experienceId ? { experienceId: a.experienceId } : {}) }));
5108
+ const what = d.kind === 'chat' ? 'chat message' : 'forum post';
5109
+ if (d.deleted) return ok(`Whop ${what} ${d.id}: DELETED — ${d.note || 'no longer up'}`, d);
5108
5110
  const m = d.metrics || {};
5109
- return ok(`Whop ${d.kind === 'chat' ? 'chat message' : 'forum post'} ${d.id}: ${Object.keys(m).length ? Object.entries(m).map(([k, v]) => `${v} ${k}`).join(', ') : 'no counts'}.`, d);
5111
+ return ok(`Whop ${what} ${d.id}${d.state === 'unconfirmed' ? ' (still up: unconfirmed — pass experienceId to check the chat)' : ''}: ${Object.keys(m).length ? Object.entries(m).map(([k, v]) => `${v} ${k}`).join(', ') : 'no counts'}.`, d);
5110
5112
  }));
5111
5113
 
5112
5114
  server.registerTool('list_inbox', {
@@ -17380,7 +17382,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17380
17382
  savedAudienceId: z.string().optional().describe('point this ad group at a saved audience instead'),
17381
17383
  productSetId: z.string().optional().describe('CATALOG SALES — rebind the ad group to another product set (a UUID from Reddit Ads Manager > Catalogs). Reddit stores an edit without checking the set exists or that the campaign uses a catalog, so pass the right id; the note says if it was not kept'),
17382
17384
  shoppingType: z.enum(['DYNAMIC', 'STATIC']).optional().describe('CATALOG SALES — DYNAMIC or STATIC product ads. Reddit has been measured keeping the value an ad group was created with, and the note says when it did not change'),
17383
- targeting: z.record(z.any()).optional().describe('same shape as create_reddit_ads_ad_group — REPLACES the existing targeting'),
17385
+ targeting: z.record(z.any()).optional().describe('the targeting keys to change, in the same shape as create_reddit_ads_ad_group: each key sent replaces that whole list, keys left out are kept, and an empty list clears one'),
17384
17386
  schedule: z.array(z.any()).optional().describe('weekly delivery windows: startDay, endDay, startHour, endHour (0 = Sunday)'),
17385
17387
  },
17386
17388
  outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), budget: z.number().nullable().optional(), productSetId: z.string().nullable().optional(), shoppingType: z.string().nullable().optional(), note: z.string().optional() },
@@ -17997,7 +17999,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17997
17999
  adAccountId: z.string().optional().describe('Reddit ad account id (a2_...); omit when only one is shared'),
17998
18000
  savedAudienceId: z.string().describe('id of the saved audience to edit'),
17999
18001
  name: z.string().optional().describe('new saved audience name'),
18000
- targeting: z.record(z.any()).optional().describe('REPLACES the existing targeting'),
18002
+ targeting: z.record(z.any()).optional().describe('the targeting keys to change: each key sent replaces that whole list, keys left out are kept, and an empty list clears one'),
18001
18003
  },
18002
18004
  outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), activeAdGroups: z.number().nullable().optional(), note: z.string().optional() },
18003
18005
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.502",
3
+ "version": "0.1.504",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI marketing agent and AI ad generator, run from your own AI agent. 1176 tools. No markup on models on Starter and up. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on any plan; Free includes 10 posts a month and every paid plan posts without a limit. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram, WordPress, Discord, Slack and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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 marketing agent and AI ad generator, run from your own AI agent. 1182 tools. No markup on models on Starter and up. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on any plan; Free includes 10 posts a month and every paid plan posts without a limit. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram, WordPress, Discord, Slack, Whop and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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"