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 +8 -7
- package/mcp/client.mjs +14 -2
- package/mcp/openai-extensions.mjs +5 -5
- package/mcp/roster-scope.mjs +10 -0
- package/mcp/tools.mjs +10 -8
- package/package.json +2 -2
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
|
-
**
|
|
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 — **
|
|
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
|
|
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
|
|
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** — **
|
|
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`)
|
|
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
|
-
|
|
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
|
|
1300
|
-
// while
|
|
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
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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('
|
|
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.
|
|
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.
|
|
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"
|