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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +6 -6
  2. 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(['ads'])`. Counted, not silently skipped: the reply
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.208",
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",