hermoso 0.1.208 → 0.1.210
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/mcp/tools.mjs +6 -6
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -2103,7 +2103,7 @@ const makeEnableToolsHandler = (ctx) => async ({ groups }) => {
|
|
|
2103
2103
|
// THE GROUP FLIP MUST NOT UNDO THE CONNECTOR GATE (2026-08-26). Enabling a group is a statement about SIZE —
|
|
2104
2104
|
// "I am willing to carry these schemas" — not a claim that the workspace has connected eleven ad platforms.
|
|
2105
2105
|
// A blind `h.enable()` here would have re-listed every tool applyToolGates had just held back, so the saving
|
|
2106
|
-
// would survive exactly until the first `enable_tools([
|
|
2106
|
+
// would survive exactly until the first `enable_tools(["ads"])`. Counted, not silently skipped: the reply
|
|
2107
2107
|
// says how many and why, because a group that turns on "8 tools" when the agent expected 240 with no
|
|
2108
2108
|
// explanation is the [[prompt-rosters-go-stale]] failure — the agent concludes the capability is missing.
|
|
2109
2109
|
if (toolHeldBackByConnectors(name, ctx.conn)) { heldBack++; continue; }
|
|
@@ -6132,7 +6132,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6132
6132
|
}));
|
|
6133
6133
|
server.registerTool('reply_to_meta_message', {
|
|
6134
6134
|
title: 'Reply to a Messenger / Instagram DM',
|
|
6135
|
-
description: 'Send a text reply to someone who has messaged the brand. THIS REACHES A REAL PERSON — show the user the exact text and who it goes to, get a yes, then send. Pass `conversationId` as well as `recipientId` and Hermoso checks Meta’s 24-hour window for free BEFORE sending, and refuses with the real reason instead of letting Meta refuse it; without one it sends and discloses that the window could not be checked. HERMOSO SENDS REPLIES ONLY: messaging_type is always RESPONSE, and proactive messages and message tags are not offered at all — that is a deliberate product boundary, not a gap, and a closed window is a RULE that only the person writing again reopens. `recipientId` is a PAGE-SCOPED ID on Messenger and an INSTAGRAM-SCOPED ID on Instagram (read_meta_conversation returns it as `replyTo`); a username or a handle is not one and cannot be turned into one. ACCEPTED IS NOT DELIVERED — never report it as delivered or read. 0 credits.',
|
|
6135
|
+
description: 'Send a text reply to someone who has messaged the brand. THIS REACHES A REAL PERSON — show the user the exact text and who it goes to, get a yes, then send. Pass `conversationId` as well as `recipientId` and Hermoso checks Meta’s 24-hour window for free BEFORE sending, and refuses with the real reason instead of letting Meta refuse it; without one it sends and discloses that the window could not be checked. HERMOSO SENDS REPLIES ONLY: messaging_type is always RESPONSE, and proactive messages and message tags are not offered at all — that is a deliberate product boundary, not a gap. THE ONE PROACTIVE THING MESSENGER ALLOWS IS A MARKETING-MESSAGES SUBSCRIPTION: to ask a person in the inbox to SUBSCRIBE to marketing messages, call request_messenger_optin (Meta’s own opt-in template, in the ads group — enable_tools(["ads"]) or find_tools if it is not in your list). Never answer that with a plain-text reply asking them to say YES, and never say the capability does not exist. And a closed window is a RULE that only the person writing again reopens. `recipientId` is a PAGE-SCOPED ID on Messenger and an INSTAGRAM-SCOPED ID on Instagram (read_meta_conversation returns it as `replyTo`); a username or a handle is not one and cannot be turned into one. ACCEPTED IS NOT DELIVERED — never report it as delivered or read. 0 credits.',
|
|
6136
6136
|
inputSchema: {
|
|
6137
6137
|
recipientId: z.string().describe('the PSID (Messenger) or IGSID (Instagram) to reply to — `replyTo` from read_meta_conversation'),
|
|
6138
6138
|
text: z.string().describe('the reply'),
|
|
@@ -9498,7 +9498,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
9498
9498
|
title: z.string().describe('the headline — 3 to 50 characters, enforced'),
|
|
9499
9499
|
body: z.string().describe('the description under the headline — 100 characters maximum, enforced'),
|
|
9500
9500
|
targetUrl: z.string().describe('the landing page (must not block OAI-AdsBot / OAI-SearchBot in robots.txt)'),
|
|
9501
|
-
imageUrl: z.string().optional().describe('public https URL of a STILL image — a video URL is refused, this channel has no video format'),
|
|
9501
|
+
imageUrl: z.string().optional().describe('public https URL of a STILL image — REQUIRED on every text ad (ChatGPT Ads refuses an ad with no creative.file_id unless the campaign is a product feed; measured 2026-09-06); a video URL is refused, this channel has no video format'),
|
|
9502
9502
|
price: z.string().optional().describe('optional price string shown on the card'),
|
|
9503
9503
|
});
|
|
9504
9504
|
server.registerTool('list_openai_ads_campaigns', {
|
|
@@ -16109,7 +16109,7 @@ function memoryNoteVerdict(text) {
|
|
|
16109
16109
|
},
|
|
16110
16110
|
outputSchema: {
|
|
16111
16111
|
collections: z.array(z.any()).optional().describe('the named collections ({id, name, ads} counts)'),
|
|
16112
|
-
ads: z.array(z.any()).optional().describe('the saved ads ({key, collection, advertiser, title, body, image, video, link, platform, savedAt, tags}); a saved CREATOR (platform "creator") also carries `creator` — handle, platform, email, phone, link, category, followers, posts, medianPlays, engagementRate, score, profileUrl'),
|
|
16112
|
+
ads: z.array(z.any()).optional().describe('the saved ads ({key, collection, advertiser, title, body, image, video, link, platform, savedAt, tags}); a saved CREATOR (platform "creator") also carries `creator` — handle, platform, email, phone, link, category, followers, posts, medianPlays, engagementRate, score, profileUrl, outreach {status new|contacted|replied|booked|passed, note} (set with update_saved_creator)'),
|
|
16113
16113
|
total: z.number().optional().describe('how many ads the board holds in total, before any collection filter or limit'),
|
|
16114
16114
|
},
|
|
16115
16115
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
@@ -16121,7 +16121,7 @@ function memoryNoteVerdict(text) {
|
|
|
16121
16121
|
if (want && !hit) return { content: [{ type: 'text', text: `No swipefile collection called "${a.collection}". This board has: ${s.collections.map(c => c.name).join(', ') || '(none)'}.` }], isError: true };
|
|
16122
16122
|
const nameOf = (id) => (s.collections.find(c => c.id === id) || {}).name || 'My Swipefile';
|
|
16123
16123
|
const rows = s.ads.filter(x => x && (!hit || x.collectionId === hit.id));
|
|
16124
|
-
const ads = rows.slice(0, lim).map(x => ({ key: x.key, collection: nameOf(x.collectionId), advertiser: x.advertiser || '', title: x.title || '', body: String(x.body || '').slice(0, 400), image: abs(x.image || ''), video: abs(x.video || ''), link: x.link || '', platform: x.platform || '', savedAt: x.savedAt || 0, tags: x.tags || null, ...(x.platform === 'creator' && x.creator ? { creator: { handle: x.creator.handle, platform: x.creator.platform, name: x.creator.name, email: x.creator.email || null, phone: x.creator.phone || null, link: x.creator.link || null, category: x.creator.category || null, followers: x.creator.followers ?? null, posts: x.creator.posts, medianPlays: x.creator.medianPlays, engagementRate: x.creator.engagementRate ?? null, score: x.creator.score, profileUrl: x.creator.profileUrl || '' } } : {}) }));
|
|
16124
|
+
const ads = rows.slice(0, lim).map(x => ({ key: x.key, collection: nameOf(x.collectionId), advertiser: x.advertiser || '', title: x.title || '', body: String(x.body || '').slice(0, 400), image: abs(x.image || ''), video: abs(x.video || ''), link: x.link || '', platform: x.platform || '', savedAt: x.savedAt || 0, tags: x.tags || null, ...(x.platform === 'creator' && x.creator ? { creator: { handle: x.creator.handle, platform: x.creator.platform, name: x.creator.name, email: x.creator.email || null, phone: x.creator.phone || null, link: x.creator.link || null, category: x.creator.category || null, followers: x.creator.followers ?? null, posts: x.creator.posts, medianPlays: x.creator.medianPlays, engagementRate: x.creator.engagementRate ?? null, score: x.creator.score, profileUrl: x.creator.profileUrl || '', outreach: x.creator.outreach || { status: 'new', note: '' } } } : {}) }));
|
|
16125
16125
|
const collections = s.collections.map(c => ({ id: c.id, name: c.name, ads: s.ads.filter(x => x && x.collectionId === c.id).length }));
|
|
16126
16126
|
if (!ads.length) return ok(`The swipefile is empty${hit ? ` in "${hit.name}"` : ''}. Collections: ${collections.map(c => `${c.name} (${c.ads})`).join(', ')}.`, { collections, ads: [], total: rows.length });
|
|
16127
16127
|
const text = `${rows.length} saved ad(s)${hit ? ` in "${hit.name}"` : ''}${ads.length < rows.length ? ` — showing ${ads.length}` : ''}:\n`
|
|
@@ -18111,7 +18111,7 @@ function memoryNoteVerdict(text) {
|
|
|
18111
18111
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
18112
18112
|
}, wrap(async (a) => {
|
|
18113
18113
|
const d = await apiPost('/api/posts/collect', { ...(a.includeMetered ? { includeMetered: true } : {}), ...(a.max ? { max: a.max } : {}) });
|
|
18114
|
-
const bits = [`Read ${d.collected} post(s)`, d.couldNotTell ? `${d.couldNotTell} could NOT be read (that is "could not tell", not zero engagement)` : null, d.remaining ? `${d.remaining} still due — call again` : null, d.meteredNote || null].filter(Boolean);
|
|
18114
|
+
const bits = [`Read ${d.collected} post(s)`, d.couldNotTell ? `${d.couldNotTell} could NOT be read (that is "could not tell", not zero engagement)` : null, d.gone ? `${d.gone} no longer exist at the platform (deleted or taken down) and will not be read again` : null, d.remaining ? `${d.remaining} still due — call again` : null, d.meteredNote || null].filter(Boolean);
|
|
18115
18115
|
return ok(`${bits.join('. ')}.${d.collected ? ' Ask post_performance which hooks are winning.' : ''}`, d);
|
|
18116
18116
|
}));
|
|
18117
18117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.210",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI ad studio and marketing MCP server with 791 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",
|