hermoso 0.1.481 → 0.1.483
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 +13 -7
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -5400,12 +5400,15 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5400
5400
|
// silent "not hidden" back one layer up, which is exactly the defect the server-side table closed.
|
|
5401
5401
|
// replyCount is part of the ANSWER: without it a comment with replies looks identical to one without, so a
|
|
5402
5402
|
// caller asked to find a reply can only guess which parent to open — and a reply's id lives nowhere else.
|
|
5403
|
-
|
|
5403
|
+
// Replies Instagram already sent with the comment come back INLINE (2026-10-31): the follow-up call is only for a comment whose
|
|
5404
|
+
// replies are not all here (Facebook, or `repliesPartial`). Each follow-up used to cost the account a failed Meta call first.
|
|
5405
|
+
const lines = (d.comments || []).flatMap(c => [`• ${c.author || '(unknown)'}: ${String(c.text).replace(/\s+/g, ' ').slice(0, 90)} — ${c.id}${c.replyCount ? ` [${c.replyCount} repl${c.replyCount === 1 ? 'y' : 'ies'}${Array.isArray(c.replies) && c.replies.length && !c.repliesPartial ? ' shown below' : ` — call again with postId=${c.id}`}]` : ''}${c.hidden === true ? ' [hidden]' : c.hidden === null ? ' [hidden state UNKNOWN — could not read it back]' : ''}${c.likes ? ` · ${c.likes} likes` : ''}`,
|
|
5406
|
+
...(Array.isArray(c.replies) ? c.replies.map(r => ` ↳ ${r.author || '(unknown)'}: ${String(r.text).replace(/\s+/g, ' ').slice(0, 90)} — ${r.id}`) : [])]);
|
|
5404
5407
|
// THE TRUNCATION LEADS. `${d.count} comment(s)` is honest about what came back and still reads as the whole
|
|
5405
5408
|
// thread — Meta's own summary.total_count is what makes the difference visible, and it rides the same request.
|
|
5406
5409
|
const head = d.truncationNote ? `⚠ ${d.truncationNote}\n\n` : '';
|
|
5407
5410
|
const of = d.totalCount != null && d.totalCount !== d.count ? ` of ${d.totalCount} total` : '';
|
|
5408
|
-
return ok(`${head}${d.count} comment(s)${of} on ${d.postId}:\n${lines.join('\n') || '(none)'}`, d);
|
|
5411
|
+
return ok(`${head}${d.count} comment(s)${of} on ${d.postId}:\n${lines.join('\n') || '(none)'}${d.note ? `\n\n${d.note}` : ''}`, d);
|
|
5409
5412
|
}));
|
|
5410
5413
|
|
|
5411
5414
|
server.registerTool('comment_on_meta_post', {
|
|
@@ -6035,13 +6038,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6035
6038
|
target: z.enum(['facebook', 'instagram', 'threads']).optional().describe('default facebook; instagram -> the Page’s linked IG; threads -> the brand’s connected Threads account'),
|
|
6036
6039
|
account: z.string().optional().describe('WHICH Instagram account when target is instagram and the brand has several — Page-linked and Instagram Login accounts alike; an @username or id from list_connector_accounts("instagram"). Several and none named is refused by name; omit when there is one.'),
|
|
6037
6040
|
scheduleAt: z.string().optional().describe('FACEBOOK ONLY — schedule instead of posting now. ISO timestamp (2026-08-01T09:00:00Z) or unix seconds; must be 10 minutes to 30 days ahead. Facebook holds the post and publishes it at that time, so nothing has to stay running on our side. Instagram and Threads have NO scheduling in Meta’s API — passing this for them is refused rather than silently posted immediately.'),
|
|
6038
|
-
locationId: z.string().optional().describe('TAG A PLACE. THREADS: a place id from search_threads_locations. INSTAGRAM: the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.'),
|
|
6041
|
+
locationId: z.string().optional().describe('TAG A PLACE. THREADS: a place id from search_threads_locations. INSTAGRAM: the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.'),
|
|
6039
6042
|
// ── THE FACEBOOK PAGE FEED, SAME 2026-09-16 AUDIT ──────────────────────────────────────────────────────────
|
|
6040
6043
|
// Meta's /{page-id}/feed table lists ~24 parameters and we sent four. `call_to_action` was the instructive
|
|
6041
6044
|
// one: its value builder already existed in this codebase, inside the ADS creative path, and the organic
|
|
6042
6045
|
// post next door could not use it.
|
|
6043
6046
|
audience: z.object({ countries: z.array(z.string()).optional().describe('two-letter codes, e.g. ["CA","US"]'), regions: z.array(z.string()).optional().describe('Meta location keys for regions/states'), cities: z.array(z.string()).optional().describe('Meta location keys for cities'), minAge: z.union([z.literal(13), z.literal(15), z.literal(18), z.literal(21), z.literal(25)]).optional().describe('Facebook Page post audience: minimum age, one of 13, 15, 18, 21 or 25') }).optional().describe('FACEBOOK PAGE POST ONLY — limit who can see this organic post to people in these places and/or over this age (Facebook allows 13, 15, 18, 21 or 25). Not on Instagram, Threads or a Facebook Reel (a vertical clip becomes a Reel): those are refused. Region and city keys come from the Meta ads location search.'),
|
|
6044
|
-
place: z.string().optional().describe('FACEBOOK — tag a location on the Page post. The NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.'),
|
|
6047
|
+
place: z.string().optional().describe('FACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.'),
|
|
6048
|
+
placeName: z.string().optional().describe('the name the user saw for that place; a different business is refused'),
|
|
6045
6049
|
callToAction: z.enum(['BOOK_TRAVEL', 'BUY_NOW', 'CALL_NOW', 'DOWNLOAD', 'GET_DIRECTIONS', 'LEARN_MORE', 'LIKE_PAGE', 'MESSAGE_PAGE', 'NO_BUTTON', 'OPEN_LINK', 'SHOP_NOW', 'SIGN_UP', 'WATCH_MORE']).optional().describe('FACEBOOK — a real call-to-action BUTTON on the Page post. The types that open a destination (SHOP_NOW, LEARN_MORE, SIGN_UP, BUY_NOW, DOWNLOAD, OPEN_LINK, WATCH_MORE, BOOK_TRAVEL) need somewhere to go: the post’s own `link`, or `callToActionLink`. CALL_NOW, MESSAGE_PAGE, LIKE_PAGE and GET_DIRECTIONS act on the Page itself and take none.'),
|
|
6046
6050
|
callToActionLink: z.string().optional().describe('FACEBOOK — where the button goes, when that is not the post’s own `link`.'),
|
|
6047
6051
|
linkName: z.string().optional().describe('FACEBOOK — override the HEADLINE of the link preview. Without it the post shows whatever Open Graph title the destination carries, which on a bare landing page is often nothing. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.'),
|
|
@@ -6202,7 +6206,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6202
6206
|
// post next door could not use it.
|
|
6203
6207
|
audience: z.object({ countries: z.array(z.string()).optional().describe('two-letter codes, e.g. ["CA","US"]'), regions: z.array(z.string()).optional().describe('Meta location keys for regions/states'), cities: z.array(z.string()).optional().describe('Meta location keys for cities'), minAge: z.union([z.literal(13), z.literal(15), z.literal(18), z.literal(21), z.literal(25)]).optional().describe('Facebook Page post audience: minimum age, one of 13, 15, 18, 21 or 25') }).optional().describe('FACEBOOK PAGE POST ONLY — limit who can see this organic post to people in these places and/or over this age (Facebook allows 13, 15, 18, 21 or 25). Not on Instagram, Threads or a Facebook Reel (a vertical clip becomes a Reel): those are refused. Region and city keys come from the Meta ads location search.'),
|
|
6204
6208
|
targetAudience: z.object({ geoLocations: z.array(z.string()).optional().describe('LinkedIn geo facet: numeric id or urn:li:geo value from search_linkedin_ads_targeting'), industries: z.array(z.string()).optional().describe('LinkedIn industry facet: numeric id or urn:li:industry value from search_linkedin_ads_targeting'), seniorities: z.array(z.string()).optional().describe('LinkedIn seniority facet: numeric id or urn:li:seniority value from search_linkedin_ads_targeting'), jobFunctions: z.array(z.string()).optional().describe('LinkedIn job function facet: numeric id or urn:li:function value from search_linkedin_ads_targeting'), staffCountRanges: z.array(z.enum(['SIZE_1', 'SIZE_2_TO_10', 'SIZE_11_TO_50', 'SIZE_51_TO_200', 'SIZE_201_TO_500', 'SIZE_501_TO_1000', 'SIZE_1001_TO_5000', 'SIZE_5001_TO_10000', 'SIZE_10001_OR_MORE'])).optional().describe('LinkedIn company size bands, e.g. SIZE_11_TO_50'), degrees: z.array(z.string()).optional().describe('LinkedIn degree facet: numeric id or urn:li:degree value from search_linkedin_ads_targeting'), fieldsOfStudy: z.array(z.string()).optional().describe('LinkedIn field of study facet: numeric id or urn:li:fieldOfStudy value from search_linkedin_ads_targeting'), organizations: z.array(z.string()).optional().describe('LinkedIn school or organization facet: numeric id or urn:li:organization value from search_linkedin_ads_targeting') }).optional().describe('LINKEDIN COMPANY PAGE POST ONLY — show the post only to Page followers matching these facets (URNs or bare numeric ids; search_linkedin_ads_targeting finds them). LinkedIn requires the matching audience to be over 300 followers and refuses a smaller one. Personal-profile posts cannot be targeted.'),
|
|
6205
|
-
place: z.string().optional().describe('FACEBOOK — tag a location on the Page post. The NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.'),
|
|
6209
|
+
place: z.string().optional().describe('FACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.'),
|
|
6210
|
+
placeName: z.string().optional().describe('the name the user saw for that place; a different business is refused'),
|
|
6206
6211
|
callToAction: z.enum(['BOOK_TRAVEL', 'BUY_NOW', 'CALL_NOW', 'DOWNLOAD', 'GET_DIRECTIONS', 'LEARN_MORE', 'LIKE_PAGE', 'MESSAGE_PAGE', 'NO_BUTTON', 'OPEN_LINK', 'SHOP_NOW', 'SIGN_UP', 'WATCH_MORE']).optional().describe('FACEBOOK — a real call-to-action BUTTON on the Page post. The types that open a destination (SHOP_NOW, LEARN_MORE, SIGN_UP, BUY_NOW, DOWNLOAD, OPEN_LINK, WATCH_MORE, BOOK_TRAVEL) need somewhere to go: the post’s own `link`, or `callToActionLink`. CALL_NOW, MESSAGE_PAGE, LIKE_PAGE and GET_DIRECTIONS act on the Page itself and take none.'),
|
|
6207
6212
|
callToActionLink: z.string().optional().describe('FACEBOOK — where the button goes, when that is not the post’s own `link`.'),
|
|
6208
6213
|
linkName: z.string().optional().describe('FACEBOOK — override the HEADLINE of the link preview. Without it the post shows whatever Open Graph title the destination carries, which on a bare landing page is often nothing. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.'),
|
|
@@ -6227,7 +6232,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6227
6232
|
audioId: z.string().optional().describe('INSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.'),
|
|
6228
6233
|
audioVolume: z.number().int().min(0).max(100).optional().describe('INSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.'),
|
|
6229
6234
|
videoVolume: z.number().int().min(0).max(100).optional().describe('INSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.'),
|
|
6230
|
-
instagramLocationId: z.string().optional().describe('INSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.'),
|
|
6235
|
+
instagramLocationId: z.string().optional().describe('INSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.'),
|
|
6231
6236
|
brandedContentSponsorIds: z.array(z.string()).optional().describe('INSTAGRAM — the numeric Instagram USER IDS of the brands behind that label (at most 2, and ids rather than @handles). Naming sponsors IS asking for the label, so setting these with `paidPartnership:false` is refused instead of publishing brand credits with no disclosure.'),
|
|
6232
6237
|
trialReel: z.enum(['MANUAL', 'SS_PERFORMANCE']).optional().describe('INSTAGRAM TRIAL REEL \u2014 publish this Reel to NON-FOLLOWERS ONLY at first (Instagram allows trials only on accounts above its follower threshold — about 1,000 followers; an ineligible account is refused by name and nothing is posted), so a hook can be tested on a cold audience without spending it on the people who already follow the brand; Instagram shows it to followers only if it graduates. MANUAL = the creator graduates it by hand in the Instagram app; SS_PERFORMANCE = Instagram graduates it automatically if it performs. REELS ONLY and INSTAGRAM ONLY: an image, a carousel, or a Facebook/Threads channel is REFUSED BY NAME rather than quietly published as an ordinary post \u2014 a trial that silently goes to every follower is the exact opposite of what was asked for, so Instagram must be one of the `channels` and the item must carry a video. Omit it for a normal Reel.'),
|
|
6233
6238
|
story: z.boolean().optional().describe('INSTAGRAM OR FACEBOOK PAGE STORY \u2014 publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel \u2014 passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.'),
|
|
@@ -6409,6 +6414,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6409
6414
|
instagramLocationId: z.string().optional().describe('INSTAGRAM — replaces the tagged place (the numeric id of its Facebook Page); an empty string removes it. Not locationId, which is the Google Business listing.'),
|
|
6410
6415
|
brandedContentSponsorIds: z.array(z.string()).optional().describe('INSTAGRAM — replaces the sponsor user ids behind the paid-partnership label (at most 2); [] removes them.'),
|
|
6411
6416
|
place: z.string().optional().describe('FACEBOOK — replaces the tagged place (the numeric id of its Facebook Page); an empty string removes it.'),
|
|
6417
|
+
placeName: z.string().optional().describe('the name the user saw for that place; a different business is refused'),
|
|
6412
6418
|
callToAction: z.enum(['BOOK_TRAVEL', 'BUY_NOW', 'CALL_NOW', 'DOWNLOAD', 'GET_DIRECTIONS', 'LEARN_MORE', 'LIKE_PAGE', 'MESSAGE_PAGE', 'NO_BUTTON', 'OPEN_LINK', 'SHOP_NOW', 'SIGN_UP', 'WATCH_MORE', '']).optional().describe('FACEBOOK — replaces the button on the Page post; "" removes it.'),
|
|
6413
6419
|
callToActionLink: z.string().optional().describe('FACEBOOK — replaces where the button goes; an empty string falls back to the post link.'),
|
|
6414
6420
|
linkName: z.string().optional().describe('FACEBOOK — replaces the link preview headline; an empty string removes the override.'),
|
|
@@ -24338,7 +24344,7 @@ function memoryNoteVerdict(text) {
|
|
|
24338
24344
|
// needs no connected account, which is why it sits in research, and it is billed in credits like its siblings.
|
|
24339
24345
|
server.registerTool('search_meta_places', {
|
|
24340
24346
|
title: 'Find a place to tag on Instagram or Facebook',
|
|
24341
|
-
description: 'Find a PLACE to tag on an Instagram or Facebook post: a shop, cafe, restaurant, venue or landmark. Give its name with the city ("Blue Bottle Coffee Oakland"), or paste its Facebook Page link, an Instagram place link (instagram.com/explore/locations/...) or its numeric id. Returns a handful of matching Facebook Pages that have an address: pageId, name, address, category, link. Pass the chosen pageId as locationId (Instagram) or place (Facebook) on post_to_meta, or instagramLocationId on schedule_post. These are matches to CONFIRM with the user (check the name and address; a place tag is public), and Instagram checks the place when the post is made. Needs no connected account. Spends about 1 credit per Page it checks (a name search checks up to 5 Pages, so about 5 to 7 credits); a pasted Facebook Page link is about 1 credit; an Instagram place link
|
|
24347
|
+
description: 'Find a PLACE to tag on an Instagram or Facebook post: a shop, cafe, restaurant, venue or landmark. Give its name with the city ("Blue Bottle Coffee Oakland"), or paste its Facebook Page link, an Instagram place link (instagram.com/explore/locations/...) or its numeric id. A pasted link or id is read and comes back as that Page\'s taggable pageId with its name and address, or a plain reason it cannot be tagged. Returns a handful of matching Facebook Pages that have an address: pageId, name, address, category, link. Pass the chosen pageId as locationId (Instagram) or place (Facebook) on post_to_meta, or instagramLocationId on schedule_post. These are matches to CONFIRM with the user (check the name and address; a place tag is public), and Instagram checks the place when the post is made. Needs no connected account. Spends about 1 credit per Page it checks (a name search checks up to 5 Pages, so about 5 to 7 credits); a pasted Facebook Page link or numeric id is about 1 credit; an Instagram place link costs nothing.',
|
|
24342
24348
|
inputSchema: {
|
|
24343
24349
|
query: z.string().describe('the place name with its city (e.g. "Blue Bottle Coffee Oakland"), or a Facebook Page link, an Instagram place link, or a numeric id'),
|
|
24344
24350
|
near: z.string().optional().describe('optional city or area to narrow a name search, e.g. "Paris"'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.483",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1091 tools. No markup on models. Publishing, scheduling, ad campaign management, comments, DMs and analytics are free on every plan. 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 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",
|