hermoso 0.1.500 → 0.1.502
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 +7 -8
- package/mcp/openai-extensions.mjs +6 -3
- package/mcp/roster-scope.mjs +7 -1
- package/mcp/tools.mjs +31 -16
- 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
|
+
**1176 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
|
|
28
|
-
person, so it is not a
|
|
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
|
|
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 1176 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** — **fourteen** 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,8 +286,7 @@ 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`)
|
|
290
|
-
`list_whop_destinations`) and Google Business Profile (below). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
|
|
289
|
+
`delete_discord_message`, `post_to_slack`) and Google Business Profile (below). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
|
|
291
290
|
you one content calendar over exactly that set. `upload_file` brings in any external or local media, not just
|
|
292
291
|
Hermoso renders.
|
|
293
292
|
*X posting bills credits per API call (X charges per request); a post containing a link costs 13× one without.*
|
|
@@ -1281,11 +1281,12 @@ 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
1285
|
google_ads: ['Google Ads', 'ads'], tiktok_ads: ['TikTok Ads', 'ads'], linkedin_ads: ['LinkedIn Ads', 'ads'], microsoft_ads: ['Microsoft Advertising', 'ads'],
|
|
1285
1286
|
reddit_ads: ['Reddit Ads', 'ads'], pinterest_ads: ['Pinterest Ads', 'ads'], snapchat_ads: ['Snapchat Ads', 'ads'], x_ads: ['X Ads', 'ads'],
|
|
1286
1287
|
openai_ads: ['ChatGPT Ads', 'ads'], apple_ads: ['Apple Ads', 'ads'], applovin_ads: ['AppLovin Ads', 'ads'],
|
|
1287
1288
|
google_drive: ['Google Drive, Sheets and Docs', 'data'], onedrive: ['OneDrive', 'data'], google_analytics: ['Google Analytics', 'data'],
|
|
1288
|
-
search_console: ['Google Search Console', 'data'], merchant_center: ['Google Merchant Center', 'data'],
|
|
1289
|
+
search_console: ['Google Search Console', 'data'], merchant_center: ['Google Merchant Center', 'data'],
|
|
1289
1290
|
shopify: ['Shopify', 'data'], stripe: ['Stripe', 'data'], hubspot: ['HubSpot', 'data'], notion: ['Notion', 'data'], canva: ['Canva', 'data'],
|
|
1290
1291
|
};
|
|
1291
1292
|
export const providerInfo = (p) => {
|
|
@@ -1295,8 +1296,10 @@ export const providerInfo = (p) => {
|
|
|
1295
1296
|
return { name, group: /_ads$/.test(String(p)) ? 'ads' : 'data' };
|
|
1296
1297
|
};
|
|
1297
1298
|
// The publishing channels a connector carries, for the "post here by default" settings. The ids are schedule_post's.
|
|
1298
|
-
|
|
1299
|
-
|
|
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' };
|
|
1300
1303
|
export const ALL_CHANNELS = Object.freeze(Object.keys(CHANNEL_LABEL));
|
|
1301
1304
|
|
|
1302
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
|
@@ -243,7 +243,13 @@ export const SHOPIFY_STORE_NOTE = 'works only for a Shopify store that installed
|
|
|
243
243
|
// (server.js reangleOffered(), env REANGLE_VIDEO=on). FAIL CLOSED: no read, a failed read or an older server all keep
|
|
244
244
|
// it hidden, so the docs build, a CLI with no session and an unreadable store never offer a capability that is not
|
|
245
245
|
// ready. The route and its job types refuse it for free as well, so a stale roster cannot spend.
|
|
246
|
-
|
|
246
|
+
// THE WHOP TOOLS SHIP DARK WITH THE WHOP CONNECTOR (2026-11-03, owner: "nothing public until it works"). Whop has no
|
|
247
|
+
// app key in production and the App Store listing is not approved, so CONNECTOR_GATES.whop is shut, and a tool for a
|
|
248
|
+
// connector nobody can link is not a capability. They lift with the gate itself: server.js flagsOnNow() names them in
|
|
249
|
+
// `flagsOn` exactly when CONNECTOR_GATES.whop.open() (WHOP_API_KEY present), so putting the key in production turns
|
|
250
|
+
// them on everywhere with no edit here. Until then the docs, the counts and every roster leave them out.
|
|
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
|
+
export const FLAGGED_OFF_TOOLS = new Set(['reangle_video', ...WHOP_DARK_TOOLS]);
|
|
247
253
|
export function toolFlaggedOff(name, conn) { const n = String(name || ''); return FLAGGED_OFF_TOOLS.has(n) && !(conn && conn.flagsOn instanceof Set && conn.flagsOn.has(n)); }
|
|
248
254
|
export function toolWithheldByServer(name, conn) { return toolFlaggedOff(name, conn) || !!(conn && ((conn.withheld instanceof Set && conn.withheld.has(String(name || ''))) || toolHeldForNoShopifyStore(name, conn))); }
|
|
249
255
|
export function toolHeldBackByConnectors(name, conn) {
|
package/mcp/tools.mjs
CHANGED
|
@@ -6348,9 +6348,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6348
6348
|
// durable public URL to feed post_to_meta / upload_meta_asset / create_meta_ad. Makes the publishing tools work on the
|
|
6349
6349
|
// user's OWN files, not just generated ones.
|
|
6350
6350
|
const EXT_MIME = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', m4v: 'video/mp4' };
|
|
6351
|
+
// WHERE THE UPLOAD WAS FILED (2026-11-03). The server keeps every uploaded image, video and PDF in the brand's Library
|
|
6352
|
+
// and answers `library: {saved, id}`; the sentence says so, so the user hears they will not need to upload it again.
|
|
6353
|
+
const uploadLibraryNote = (d) => (d && d.library && d.library.saved ? ` It is also kept in this brand's Library as ${d.library.id || 'its newest item'} (list_library), so it can be reused later without uploading it again.` : '');
|
|
6351
6354
|
server.registerTool('upload_file', {
|
|
6352
6355
|
title: 'Upload a local file -> durable public URL',
|
|
6353
|
-
description: 'Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts — post_to_meta / post_to_linkedin / post_to_linkedin_page / post_to_youtube / post_to_tiktok / post_to_pinterest / post_to_x / post_to_google_business / schedule_post / upload_meta_asset / upload_google_ads_asset / create_meta_ad / create_linkedin_ads_creative / set_youtube_thumbnail / save_to_drive / save_to_onedrive. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user\'s desktop, an agency\'s finished ad, a photo they shot), and it means you can publish, schedule and run ads through Hermoso without generating anything here. Provide exactly ONE source — passing two is an error, never a silent preference: `url` (ANY public http(s) link — Hermoso fetches it server-side, so nothing crosses this connection and there is no practical size limit; THIS IS THE ONE THAT ALWAYS WORKS, including on the hosted connector), ' + (opts.hosted ? '' : '`path` (a local file, ONLY when Hermoso runs on the user\'s own machine over stdio/CLI; the hosted connector cannot see their disk), or ') + '`dataUri` (a base64 data: URI — keep it under ~15MB, since the bytes travel over this connection). If the file is already at a public https URL, the Meta, Reddit and ChatGPT-Ads tools take it directly and re-host it safely — but LinkedIn (posts and ad creatives), Pinterest, the YouTube thumbnail and upload_google_ads_asset upload the BYTES themselves and therefore refuse an external host, so run it through here first and pass the URL this returns. When in doubt, use this: its URL works everywhere. (Calling the underlying HTTP route directly? POST /api/upload takes the file\'s RAW BYTES as the request body with its own content-type — NOT multipart/form-data — or ?url=<link> with no body.) Returns {url, kind, bytes}.',
|
|
6356
|
+
description: 'Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts — post_to_meta / post_to_linkedin / post_to_linkedin_page / post_to_youtube / post_to_tiktok / post_to_pinterest / post_to_x / post_to_google_business / schedule_post / upload_meta_asset / upload_google_ads_asset / create_meta_ad / create_linkedin_ads_creative / set_youtube_thumbnail / save_to_drive / save_to_onedrive. Kept in the Library. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user\'s desktop, an agency\'s finished ad, a photo they shot), and it means you can publish, schedule and run ads through Hermoso without generating anything here. Provide exactly ONE source — passing two is an error, never a silent preference: `url` (ANY public http(s) link — Hermoso fetches it server-side, so nothing crosses this connection and there is no practical size limit; THIS IS THE ONE THAT ALWAYS WORKS, including on the hosted connector), ' + (opts.hosted ? '' : '`path` (a local file, ONLY when Hermoso runs on the user\'s own machine over stdio/CLI; the hosted connector cannot see their disk), or ') + '`dataUri` (a base64 data: URI — keep it under ~15MB, since the bytes travel over this connection). If the file is already at a public https URL, the Meta, Reddit and ChatGPT-Ads tools take it directly and re-host it safely — but LinkedIn (posts and ad creatives), Pinterest, the YouTube thumbnail and upload_google_ads_asset upload the BYTES themselves and therefore refuse an external host, so run it through here first and pass the URL this returns. When in doubt, use this: its URL works everywhere. (Calling the underlying HTTP route directly? POST /api/upload takes the file\'s RAW BYTES as the request body with its own content-type — NOT multipart/form-data — or ?url=<link> with no body.) Returns {url, kind, bytes}.',
|
|
6354
6357
|
inputSchema: {
|
|
6355
6358
|
// THE THIRD SOURCE, AND THE ONLY ONE THAT CARRIES BYTES WITHOUT SPENDING THEM AS TOKENS (2026-09-16). Asked
|
|
6356
6359
|
// for by the first real user of the connector: `path` cannot work on a hosted surface and a data: URI puts
|
|
@@ -6389,7 +6392,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6389
6392
|
// The SERVER fetches it, so the bytes never cross this transport and the SSRF guard runs on our side, on
|
|
6390
6393
|
// EVERY redirect hop. This is what makes bring-your-own-creative work on the hosted connector at all.
|
|
6391
6394
|
const d = await apiUploadUrl('/api/upload', String(a.url).trim(), { fileName: fileName0 });
|
|
6392
|
-
return ok(`Ingested ${d.kind || 'file'} (${d.bytes} bytes) from that URL → ${d.url}. Pass this url to any publish, schedule or ad-build tool
|
|
6395
|
+
return ok(`Ingested ${d.kind || 'file'} (${d.bytes} bytes) from that URL → ${d.url}. Pass this url to any publish, schedule or ad-build tool.${uploadLibraryNote(d)}`, { url: d.url, kind: d.kind, bytes: d.bytes, ...(d.library?.saved && d.library.id ? { libraryId: d.library.id } : {}) });
|
|
6393
6396
|
}
|
|
6394
6397
|
let buf, contentType = 'application/octet-stream', fileName = fileName0;
|
|
6395
6398
|
if (a.dataUri) {
|
|
@@ -6416,7 +6419,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6416
6419
|
contentType = EXT_MIME[(fileName.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
|
|
6417
6420
|
}
|
|
6418
6421
|
const d = await apiUpload('/api/upload', buf, { contentType, fileName });
|
|
6419
|
-
return ok(`Uploaded ${d.kind || 'file'} (${d.bytes || buf.length} bytes) → ${d.url}. Pass this url to post_to_meta / upload_meta_asset / create_meta_ad
|
|
6422
|
+
return ok(`Uploaded ${d.kind || 'file'} (${d.bytes || buf.length} bytes) → ${d.url}. Pass this url to post_to_meta / upload_meta_asset / create_meta_ad.${uploadLibraryNote(d)}`, { url: d.url, kind: d.kind, bytes: d.bytes, ...(d.library?.saved && d.library.id ? { libraryId: d.library.id } : {}) });
|
|
6420
6423
|
}));
|
|
6421
6424
|
|
|
6422
6425
|
server.group('channel_admin');
|
|
@@ -6734,7 +6737,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6734
6737
|
// no need for one edge case just for Facebook."
|
|
6735
6738
|
server.registerTool('schedule_post', {
|
|
6736
6739
|
title: 'Schedule a post for later',
|
|
6737
|
-
description: 'Use this when the user wants to schedule a social media post for later, on one channel or several at once. Scheduling and publishing cost no credits on any Hermoso plan (X alone bills per call: hermoso_credits gives the exact credits per X post). THE FREE PLAN INCLUDES 10 POSTS A MONTH ON UP TO 3 CHANNELS (a post counts once, however many channels it goes to; counted in the UTC month the post goes out); every paid plan posts without a limit. A post over that allowance is refused here and nothing is queued; the refusal says how many are used and when the count resets. Relay it as written, never retry it and never split the post to fit. billing_status and list_scheduled show how many are used. Queue a post for a future time on one or more connected channels at once: facebook, instagram, threads, tiktok, youtube, linkedin, x, pinterest, bluesky, telegram, wordpress, discord, slack, whop
|
|
6740
|
+
description: 'Use this when the user wants to schedule a social media post for later, on one channel or several at once. Scheduling and publishing cost no credits on any Hermoso plan (X alone bills per call: hermoso_credits gives the exact credits per X post). THE FREE PLAN INCLUDES 10 POSTS A MONTH ON UP TO 3 CHANNELS (a post counts once, however many channels it goes to; counted in the UTC month the post goes out); every paid plan posts without a limit. A post over that allowance is refused here and nothing is queued; the refusal says how many are used and when the count resets. Relay it as written, never retry it and never split the post to fit. billing_status and list_scheduled show how many are used. Queue a post for a future time on one or more connected channels at once: facebook, instagram, threads, tiktok, youtube, linkedin, x, pinterest, bluesky, telegram, wordpress, discord, slack — every one live. whop is held back until its connector is available, so a whop post is refused, not queued. A MULTI-SLIDE creative goes in imageUrls[] as a CAROUSEL, in order — never schedule just its first slide. Name the time in `at`, or pass `useQueue:true` for the brand’s next free POSTING SLOT (what “just queue it” means). imageUrl/videoUrl take a Hermoso render URL or an upload_file URL. `captions` gives a channel its own wording; the rest use `message`. PINTEREST AND YOUTUBE SHOW A TITLE: `title` (max 100 chars), derived from the caption when omitted; YOUTUBE also takes `description`, `tags`, `thumbnailUrl`. Every PER-CHANNEL SETTING is a parameter below, carried straight to the real publisher — TikTok’s `brandedContent` / `yourBrand` disclosures (set them whenever the post is commercial), Google Business `topicType` / `event` / `offer` / `actionType`, an X `thread` / `poll`, Instagram `collaborators`, and the rest. Channels are attempted INDEPENDENTLY: one failing never blocks the others. SOME CHANNELS MUST BE TOLD WHICH ACCOUNT, and Hermoso never guesses: Pinterest needs `boardId` (list_pinterest_boards) or is refused; a LinkedIn COMPANY PAGE post needs `linkedinOrganizationId` (list_linkedin_pages) or it goes to the person’s own profile; several Facebook Pages need `pageId` (list_meta_pages), several Google Business listings `locationId` (list_business_locations) — resolve those FIRST and let the user pick, or the post is refused when it fires. A scheduled post GOES LIVE PUBLICLY by default on every channel, never quietly downgraded. Only if the user asks, set `visibility` (or `visibilityByChannel`): ‘unlisted’ (YouTube) · ‘private’ (YouTube, or TikTok SELF_ONLY) · ‘draft’ (TikTok, or an unpublished Facebook Page post). A visibility a channel cannot do is REFUSED now, never posted weaker later.',
|
|
6738
6741
|
inputSchema: {
|
|
6739
6742
|
...HOOK_ATTR,
|
|
6740
6743
|
channels: z.array(z.enum(['facebook', 'instagram', 'threads', 'tiktok', 'youtube', 'linkedin', 'x', 'pinterest', 'google_business', 'bluesky', 'telegram', 'wordpress', 'discord', 'slack', 'whop'])).describe('one or more channels to post to at that time'),
|
|
@@ -6864,6 +6867,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6864
6867
|
whopPinned: z.boolean().optional().describe('WHOP FORUM POSTS ONLY — pin it to the top of the forum.'),
|
|
6865
6868
|
whopMention: z.boolean().optional().describe('WHOP FORUM POSTS ONLY — notify every member (off by default).'),
|
|
6866
6869
|
whopVisibility: z.enum(['members_only', 'globally_visible']).optional().describe('WHOP FORUM POSTS ONLY — who can see it; unset is the forum’s own default.'),
|
|
6870
|
+
whopPaywallCents: z.number().int().min(0).optional().describe('WHOP FORUM POSTS ONLY — a paywall in cents (500 = $5.00): readers pay to see the body; needs a title.'),
|
|
6871
|
+
whopPaywallCurrency: z.string().optional().describe('WHOP — the paywall currency, a 3-letter code (default usd).'),
|
|
6872
|
+
whopPoll: z.array(z.string()).optional().describe('WHOP FORUM POSTS ONLY — a poll: 2 to 10 options, in order.'),
|
|
6867
6873
|
buttons: z.array(z.union([z.object({ text: z.string(), url: z.string(), style: z.enum(['danger', 'success', 'primary']).optional() }), z.array(z.object({ text: z.string(), url: z.string(), style: z.enum(['danger', 'success', 'primary']).optional() }))])).optional().describe('TELEGRAM ONLY — URL buttons under the post, as post_to_telegram takes them; never on an album.'),
|
|
6868
6874
|
parseMode: z.enum(['HTML']).optional().describe("TELEGRAM ONLY — 'HTML' for Telegram's HTML tags; omit for plain text."),
|
|
6869
6875
|
silent: z.boolean().optional().describe('TELEGRAM / DISCORD — no notification.'),
|
|
@@ -7075,6 +7081,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7075
7081
|
discordStartThread: z.string().optional().describe('DISCORD bot: open a thread under the post, this title'),
|
|
7076
7082
|
discordMentions: z.boolean().optional().describe('DISCORD: let mentions ping'),
|
|
7077
7083
|
threadName: z.string().optional().describe('DISCORD: forum thread title'),
|
|
7084
|
+
whopPinned: z.boolean().optional().describe('WHOP: pin the forum post'),
|
|
7085
|
+
whopMention: z.boolean().optional().describe('WHOP: notify every member'),
|
|
7086
|
+
whopVisibility: z.enum(['members_only', 'globally_visible']).optional().describe('WHOP: who can see the forum post'),
|
|
7087
|
+
whopPaywallCents: z.number().int().min(0).optional().describe('WHOP: forum-post paywall in cents (needs a title)'),
|
|
7088
|
+
whopPaywallCurrency: z.string().optional().describe('WHOP: paywall currency'),
|
|
7089
|
+
whopPoll: z.array(z.string()).optional().describe('WHOP: poll options, 2 to 10'),
|
|
7078
7090
|
wpCategories: z.array(z.string()).optional().describe('WORDPRESS — categories (names or ids)'),
|
|
7079
7091
|
wpTags: z.array(z.string()).optional().describe('WORDPRESS — tags (names or ids)'),
|
|
7080
7092
|
excerpt: z.string().optional().describe('WORDPRESS — the excerpt under the title'),
|
|
@@ -26000,9 +26012,10 @@ function memoryNoteVerdict(text) {
|
|
|
26000
26012
|
|
|
26001
26013
|
server.registerTool('list_library', {
|
|
26002
26014
|
title: 'List library',
|
|
26003
|
-
description: "Browse this workspace's Library — every image/video generated in the Studio, newest first (the same Library the web app shows). Returns served URLs you can open directly or hand to fetch_asset for a download link, plus each asset's id (L0, L1…, which plan_ad and render_ad `inserts` take), kind, model, and age. Free, read-only.",
|
|
26015
|
+
description: "Browse this workspace's Library — every image/video generated in the Studio, and uploads (source:'upload'), newest first (the same Library the web app shows). Returns served URLs you can open directly or hand to fetch_asset for a download link, plus each asset's id (L0, L1…, which plan_ad and render_ad `inserts` take), kind, model, and age. Free, read-only.",
|
|
26004
26016
|
inputSchema: {
|
|
26005
|
-
kind: z.enum(['image', 'video', 'all']).optional().describe("filter by asset kind (default 'all')"),
|
|
26017
|
+
kind: z.enum(['image', 'video', 'document', 'all']).optional().describe("filter by asset kind (default 'all')"),
|
|
26018
|
+
uploads: z.boolean().optional(),
|
|
26006
26019
|
limit: z.number().optional().describe('max assets to return (default 20, max 60)'),
|
|
26007
26020
|
},
|
|
26008
26021
|
outputSchema: { assets: z.array(z.object({ id: z.string().optional(), url: z.string(), kind: z.string().optional(), model: z.string().optional(), ageHours: z.number().optional() })).optional() },
|
|
@@ -26013,10 +26026,11 @@ function memoryNoteVerdict(text) {
|
|
|
26013
26026
|
const kind = a.kind && a.kind !== 'all' ? a.kind : null;
|
|
26014
26027
|
const lim = Math.min(60, Math.max(1, +a.limit || 20));
|
|
26015
26028
|
// id: L0, L1… over the whole Library newest first (a kind filter does not renumber), the id plan_ad / render_ad `inserts` take
|
|
26016
|
-
const
|
|
26017
|
-
|
|
26018
|
-
|
|
26019
|
-
|
|
26029
|
+
const up = a.uploads === true;
|
|
26030
|
+
const assets = list.filter(x => x && x.url).map((x, i) => ({ x, id: 'L' + i })).filter(({ x }) => (!kind || x.kind === kind) && (!up || x.source === 'upload')).slice(0, lim)
|
|
26031
|
+
.map(({ x, id }) => ({ id, url: abs(x.url), kind: x.kind || '', model: x.model || '', ...(x.source === 'upload' ? { source: 'upload', ...(x.name ? { name: String(x.name).slice(0, 120) } : {}) } : {}), ageHours: x.at ? Math.round((Date.now() - x.at) / 36e5) : undefined }));
|
|
26032
|
+
if (!assets.length) return ok(up ? 'Nothing uploaded to this workspace\'s Library yet — upload_file keeps an image, video or PDF there.' : 'The Library is empty for this workspace — render something or upload_file a file first.', { assets: [] });
|
|
26033
|
+
return ok(`${assets.length} asset${assets.length === 1 ? '' : 's'} (newest first):\n` + assets.map((x) => ` ${x.id}. [${x.kind || '?'}${x.source === 'upload' ? ' · your upload' + (x.name ? ' “' + x.name + '”' : '') : x.model ? ' · ' + x.model : ''}${x.ageHours != null ? ' · ' + x.ageHours + 'h ago' : ''}] ${x.url}`).join('\n'), { assets });
|
|
26020
26034
|
}));
|
|
26021
26035
|
|
|
26022
26036
|
// DELETE FROM THE LIBRARY (2026-10-23). A customer was told it could not be done and sent to support. The server
|
|
@@ -26785,10 +26799,11 @@ function memoryNoteVerdict(text) {
|
|
|
26785
26799
|
}));
|
|
26786
26800
|
|
|
26787
26801
|
// RE-ANGLE A VIDEO (2026-11-03): POST /api/video/reangle, the same engine as the Studio's card. Two steps, both priced
|
|
26788
|
-
// before anything is spent: the takes at 720p (beats × angles × takes),
|
|
26802
|
+
// before anything is spent: the takes at 720p (beats × angles × takes), each anchored by a still of the new camera, then
|
|
26803
|
+
// the picked take of each beat delivered as itself at 1080p (upscaled, never re-rendered).
|
|
26789
26804
|
server.registerTool('reangle_video', {
|
|
26790
26805
|
title: 'Re-shoot a video from new camera angles',
|
|
26791
|
-
description: "RE-SHOOT an existing video from a NEW CAMERA ANGLE while keeping who and what is in it, the set, the action, the timing and the colour grade: top shot, worms_eye, three_quarter_left / three_quarter_right, left_profile / right_profile, over_the_shoulder, back, macro, wide, or ANY angle in your own words. The clip is split at its hard cuts and every beat is re-shot on its own, from that beat's footage. STEP 1 (video + angles + takes 1-4, default 2): renders every beat × angle × take at 720p, so you or the user can PICK the best take of each beat by looking at it (get_job, then video_frames):
|
|
26806
|
+
description: "RE-SHOOT an existing video from a NEW CAMERA ANGLE while keeping who and what is in it, the set, the action, the timing and the colour grade: top shot, worms_eye, three_quarter_left / three_quarter_right, left_profile / right_profile, over_the_shoulder, back, macro, wide, or ANY angle in your own words. The clip is split at its hard cuts and every beat is re-shot on its own, from that beat's footage; for a named shot each take first draws a still of the beat's first frame from the new camera and opens on it. STEP 1 (video + angles + takes 1-4, default 2): renders every beat × angle × take at 720p, so you or the user can PICK the best take of each beat by looking at it (get_job, then video_frames): a take can still drift back toward the source's camera, so check every take. STEP 2 (winners = the job id of the picked take for each beat): delivers each pick AS ITSELF at 1080p (upscaled, not re-rendered, so the angle is the one picked) and joins them into one video with the source's own sound. dryRun:true on either step returns the plan and the exact credits of both steps, nothing rendered or charged: ALWAYS dryRun first and tell the user beats × angles × takes and both prices. A clip showing a real person's face is usually refused by the video model for free, with choices. Results land in the Library.",
|
|
26792
26807
|
inputSchema: {
|
|
26793
26808
|
video: z.string().optional().describe('the clip to re-shoot (step 1): a render or Library URL, an upload_file URL or a direct video link, up to 60 s'),
|
|
26794
26809
|
angles: z.array(z.string()).optional().describe("step 1: 1-4 camera angles — top, worms_eye, three_quarter_left, three_quarter_right, left_profile, right_profile, over_the_shoulder, back, macro, wide — or any angle in your own words"),
|
|
@@ -26811,8 +26826,8 @@ function memoryNoteVerdict(text) {
|
|
|
26811
26826
|
takes: z.number().optional(),
|
|
26812
26827
|
renders: z.number().optional().describe('how many takes step 1 renders'),
|
|
26813
26828
|
takesCredits: z.number().optional().describe('the exact credits of step 1'),
|
|
26814
|
-
finalCredits: z.number().nullable().optional().describe('the credits of step 2 (the picks
|
|
26815
|
-
totalCredits: z.number().optional().describe('step 2: the exact credits of the 1080p
|
|
26829
|
+
finalCredits: z.number().nullable().optional().describe('the credits of step 2 (the picks upscaled to 1080p); its own dryRun prices the real takes exactly'),
|
|
26830
|
+
totalCredits: z.number().optional().describe('step 2: the exact credits of the 1080p finish'),
|
|
26816
26831
|
jobs: z.array(z.any()).optional().describe('step 1: one queued job per take: {id, beat, angle, take, credits}'),
|
|
26817
26832
|
job: z.any().optional().describe('step 2: the queued 1080p job {id, credits}'),
|
|
26818
26833
|
winners: z.array(z.any()).optional(),
|
|
@@ -26826,8 +26841,8 @@ function memoryNoteVerdict(text) {
|
|
|
26826
26841
|
const nl = notes.length ? '\n' + notes.join('\n') : '';
|
|
26827
26842
|
if (d?.phase === 'final') {
|
|
26828
26843
|
const w = (d.winners || []).map((x) => `beat ${x.beat}: ${x.angle} take ${x.take} (job ${x.job})`).join('; ');
|
|
26829
|
-
if (d.dryRun) return { content: [{ type: 'text', text: `RE-ANGLE 1080p
|
|
26830
|
-
return { content: [{ type: 'text', text: `
|
|
26844
|
+
if (d.dryRun) return { content: [{ type: 'text', text: `RE-ANGLE 1080p FINISH (nothing rendered, nothing charged): ${d.beats} beat${d.beats === 1 ? '' : 's'}, ${w}.\nPrice: ${d.totalCredits} credits. ${PRICE_ESTIMATE_LINE}${nl}\nTo run it, call reangle_video again with the same winners and no dryRun.` }], structuredContent: { reangleId: d.reangleId, phase: 'final', winners: d.winners, totalCredits: d.totalCredits, dryRun: true, ...(notes.length ? { notes } : {}) } };
|
|
26845
|
+
return { content: [{ type: 'text', text: `Delivering the picks at ${d.resolution} (upscaled, not re-rendered) and joining them (${d.totalCredits} credits): ${w}. Job ${d.job?.id}: poll it with get_job; the finished video lands in the Library.${nl}` }], structuredContent: { reangleId: d.reangleId, phase: 'final', winners: d.winners, totalCredits: d.totalCredits, job: d.job, ...(notes.length ? { notes } : {}) } };
|
|
26831
26846
|
}
|
|
26832
26847
|
const rows = Array.isArray(d?.plan) ? d.plan : [];
|
|
26833
26848
|
const ang = (d?.angles || []).map((a) => a.label).join(', ');
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.502",
|
|
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. 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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|