hermoso 0.1.405 → 0.1.407
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 +16 -10
- package/mcp/openai-extensions.mjs +1 -1
- package/mcp/roster-scope.mjs +2 -0
- package/mcp/tool-hints.mjs +2 -1
- package/mcp/tools.mjs +13 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,9 +21,9 @@ charges per API request.
|
|
|
21
21
|
|
|
22
22
|
**What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
|
|
23
23
|
Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus
|
|
24
|
-
product feeds in Google Merchant Center. Publishing and scheduling — **
|
|
25
|
-
Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and
|
|
26
|
-
person, so it is not
|
|
24
|
+
product feeds in Google Merchant Center. Publishing and scheduling — **eleven** channels: Facebook, Instagram,
|
|
25
|
+
Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. Messaging: WhatsApp (you message a
|
|
26
|
+
person, so it is not a twelfth publishing channel) and DM automations on Instagram, Facebook Pages and
|
|
27
27
|
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,
|
|
28
28
|
Instagram, YouTube, Threads and Reddit. Analytics: Google Analytics 4, Google Search Console and every
|
|
29
29
|
connected platform's own post and campaign insights. Files: Google Drive, Sheets, Docs and OneDrive.
|
|
@@ -271,21 +271,27 @@ Real faces from your own photos need a paid plan; AI people work on every plan.
|
|
|
271
271
|
per-render credit cost) with no ad framing: `generate_image` / `generate_video` with `useBrand:false`,
|
|
272
272
|
`generate_voice`, `generate_text`. The image default is Nano Banana 2.1.
|
|
273
273
|
|
|
274
|
-
**Publish to your own channels** — **
|
|
274
|
+
**Publish to your own channels** — **eleven** of them: Facebook, Instagram and Threads (`post_to_meta`), TikTok
|
|
275
275
|
(`post_to_tiktok`), YouTube (`post_to_youtube` + `update_youtube_video`, `youtube_video_insights`, comments
|
|
276
276
|
read/reply), X (`post_to_x`, `x_post_metrics`, `x_post_insights`, `x_mentions`, `list_x_dms`, `send_x_dm`),
|
|
277
277
|
LinkedIn profile **and** company Pages (`post_to_linkedin`, `post_to_linkedin_page`), Pinterest
|
|
278
278
|
(`post_to_pinterest` + boards), Bluesky (`post_to_bluesky`, `delete_bluesky_post`, `bluesky_post_metrics`, plus
|
|
279
279
|
`list_bluesky_convos` / `read_bluesky_dm` / `send_bluesky_dm`) and Telegram (`post_to_telegram`,
|
|
280
|
-
`delete_telegram_message`, `list_telegram_chats`). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
|
|
280
|
+
`delete_telegram_message`, `list_telegram_chats`) and Google Business Profile (below). `schedule_post` / `list_scheduled` / `cancel_scheduled` give
|
|
281
281
|
you one content calendar over exactly that set. `upload_file` brings in any external or local media, not just
|
|
282
282
|
Hermoso renders.
|
|
283
283
|
*X posting bills credits per API call (X charges per request); a post containing a link costs 13× one without.*
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
**Google Business Profile** is the panel a business shows on Google Search and Maps. `post_to_google_business`
|
|
285
|
+
publishes a Post with text, optionally one photo, and a call-to-action button (Google takes no video in Posts, so
|
|
286
|
+
a schedule with only a video is refused for that channel); `list_google_business_posts` and
|
|
287
|
+
`delete_google_business_post` manage what is showing. `list_google_business_reviews` finds the reviews with no
|
|
288
|
+
answer yet and `reply_to_google_business_review` answers one as the business. `google_business_insights` reports
|
|
289
|
+
search and Maps impressions, calls, website clicks and direction requests, and `google_business_search_keywords`
|
|
290
|
+
the words people searched to find the listing. `list_business_locations`, `get_business_location` and
|
|
291
|
+
`update_business_location` read and edit the listing itself: hours, phone, website, description, categories.
|
|
292
|
+
Business Profile Q&A is not offered: Google retired it on 2025-11-03.
|
|
293
|
+
|
|
294
|
+
**Message customers on WhatsApp** — messaging, not a twelfth publishing channel: you message a person, and
|
|
289
295
|
nothing here posts to a feed. `list_whatsapp_accounts` finds the Business Account and its
|
|
290
296
|
numbers, `list_whatsapp_templates` / `create_whatsapp_template` / `delete_whatsapp_template` manage the templates
|
|
291
297
|
Meta reviews, and `send_whatsapp_message` sends one — confirm-gated, because it reaches a real phone and Meta
|
|
@@ -1256,7 +1256,7 @@ export const neutralNote = (t) => {
|
|
|
1256
1256
|
// sweeps and drift watches queued for later (measured on a fresh local account: one `agentidle` "nudge", queued for
|
|
1257
1257
|
// tomorrow, would have shown as "1 render in progress"). Mirrors server.js JOB_TYPES_HIDDEN_FROM_V1, and the check
|
|
1258
1258
|
// fails if that list gains a type this one lacks. A job queued for LATER is not in progress either.
|
|
1259
|
-
export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['purge', 'agentidle', 'agentsetupmail', 'uploadretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth', 'chargednothing', 'emailfeedback']);
|
|
1259
|
+
export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['purge', 'agentidle', 'agentsetupmail', 'uploadretention', 'miniappretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth', 'chargednothing', 'emailfeedback']);
|
|
1260
1260
|
const ageText = (h) => (h == null || !Number.isFinite(+h) ? '' : +h < 1 ? 'just now' : +h < 48 ? `${Math.round(+h)}h ago` : `${Math.round(+h / 24)}d ago`);
|
|
1261
1261
|
const VIDEO_RE = /\.(mp4|webm|mov|m4v)([?#]|$)/i;
|
|
1262
1262
|
const clip = (s, n) => { const t = String(s ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -95,6 +95,8 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
95
95
|
// collide with Hermoso billing, which is buy_credits / billing_status / upgrade_plan and never says "stripe".
|
|
96
96
|
[/stripe/, 'stripe'],
|
|
97
97
|
[/hubspot/, 'hubspot'],
|
|
98
|
+
// Intercom (2026-10-12): the brand's OWN support inbox, offered only to a workspace that connected it.
|
|
99
|
+
[/intercom/, 'intercom'],
|
|
98
100
|
// ── posting-only channels ──
|
|
99
101
|
[/youtube/, 'youtube'],
|
|
100
102
|
[/google_business|business_location|^list_business_(categories|attributes)$|^business_google_updated$/, 'google_business'],
|
package/mcp/tool-hints.mjs
CHANGED
|
@@ -92,7 +92,8 @@ export function videoChoiceText(tool, choice, opts = {}) {
|
|
|
92
92
|
const c = choice && typeof choice === 'object' ? choice : {}, o = c.options || {};
|
|
93
93
|
const d = o.draft && o.draft.model ? o.draft : null;
|
|
94
94
|
const lines = [
|
|
95
|
-
|
|
95
|
+
// A clone names which length it priced (lengthBasis, 2026-10-07) and any post lookup reading that length cost.
|
|
96
|
+
`${c.seconds ? `A ${c.seconds}s video${c.lengthBasis === 'source' ? ' (the original’s length)' : ''}` : 'This video'} would cost about ${c.videoCredits ?? '?'} credits and the account has ${c.balance ?? '?'} (${c.short ?? '?'} short).${c.lengthBasis === 'estimate' && c.seconds ? ` That price ASSUMES ${c.seconds}s: the original’s length could not be read before planning, and the clone will match its real length, so say it is an estimate.` : ''} ${Number(c.lookupCredits) > 0 ? `Nothing was planned or rendered; reading the original’s length cost ${Math.round(Number(c.lookupCredits))} credit${Math.round(Number(c.lookupCredits)) === 1 ? '' : 's'} (the post lookup the clone reuses).` : 'Nothing was planned, rendered or charged.'} Tell the user and let them choose — never switch the format for them:`,
|
|
96
97
|
` 1. Make it as an image instead (~${o.image?.credits ?? '?'} credits): ${videoChoiceImageCall(tool)}.`,
|
|
97
98
|
// A WIDGET HOST (ChatGPT) GETS NO PURCHASE ROUTE (2026-09-30): its plugin guidelines let a plugin explain that a
|
|
98
99
|
// feature needs more credits and link an informational page, never a checkout. `o.topup.url` is a checkout link.
|
package/mcp/tools.mjs
CHANGED
|
@@ -4389,7 +4389,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4389
4389
|
// workspace outright, so no argument here can point the write at somebody else's account.
|
|
4390
4390
|
server.registerTool('create_brand', {
|
|
4391
4391
|
title: 'Create a profile',
|
|
4392
|
-
description: 'Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate.
|
|
4392
|
+
description: 'Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate. Creating it is free. Drafting a NEW brand into it (draft_brand) needs the credits for its research on the balance, about 100; with fewer, draft_brand refuses before anything runs or is charged and says how many it needs and how many the account has.',
|
|
4393
4393
|
inputSchema: {
|
|
4394
4394
|
name: z.string().describe('the name for the new profile (a brand, client or creator name)'),
|
|
4395
4395
|
activate: z.boolean().optional().describe('switch this connection to the new profile (default true); everything you do next scopes to it'),
|
|
@@ -4988,15 +4988,16 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4988
4988
|
// it is a caller with two different intentions and picking one would upload a file they did not mean to send.
|
|
4989
4989
|
if (a.getUploadUrl) {
|
|
4990
4990
|
const conflict = ['url', 'path', 'dataUri'].filter(k => String(a[k] || '').trim());
|
|
4991
|
-
if (conflict.length) throw new Error(`\`getUploadUrl\` asks for a link to send bytes to; \`${conflict.join('` and `')}\` is a file to upload right now. Do one or the other.`);
|
|
4991
|
+
if (conflict.length) throw Object.assign(new Error(`\`getUploadUrl\` asks for a link to send bytes to; \`${conflict.join('` and `')}\` is a file to upload right now. Do one or the other.`), { status: 400, _userInput: true });
|
|
4992
4992
|
const t = await apiPost('/api/upload/ticket', {});
|
|
4993
4993
|
return ok(`PUT the file's raw bytes to this url and it answers with the durable Hermoso url:\n\n${t.uploadUrl}\n\n${t.howto}`, { uploadUrl: t.uploadUrl, expiresAt: t.expiresAt, maxBytes: t.maxBytes, ...(t.partMaxBytes ? { partMaxBytes: t.partMaxBytes } : {}) });
|
|
4994
4994
|
}
|
|
4995
4995
|
// EXACTLY ONE SOURCE. Two is an ERROR: a caller who passes both has two different files in mind, and quietly
|
|
4996
4996
|
// preferring one of them ingests the wrong file and reports success.
|
|
4997
4997
|
const given = ['url', 'path', 'dataUri'].filter(k => String(a[k] || '').trim());
|
|
4998
|
-
if (given.length > 1) throw new Error(`Give exactly ONE source — you passed \`${given.join('` and `')}\`. Two sources mean two different files, so I won't pick one for you.`);
|
|
4999
|
-
|
|
4998
|
+
if (given.length > 1) throw Object.assign(new Error(`Give exactly ONE source — you passed \`${given.join('` and `')}\`. Two sources mean two different files, so I won't pick one for you.`), { status: 400, _userInput: true });
|
|
4999
|
+
// AN AUTHORED REFUSAL CARRIES ITS MARK (2026-10-07): bare, the error ledger filed it as "no status and no marker".
|
|
5000
|
+
if (!given.length) throw Object.assign(new Error('Provide exactly one source: `url` (a public http(s) link — works everywhere, including the hosted connector), `path` (a local file, stdio/CLI only) or `dataUri` (base64).'), { status: 400, _userInput: true });
|
|
5000
5001
|
const fileName0 = a.name || '';
|
|
5001
5002
|
if (a.url) {
|
|
5002
5003
|
// The SERVER fetches it, so the bytes never cross this transport and the SSRF guard runs on our side, on
|
|
@@ -8079,7 +8080,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8079
8080
|
description: 'READ the product tags on a post the brand has already published, or ADD/MOVE tags on it. Pass `tags` to update: Meta\'s own behaviour is "updates coordinates if the product is already tagged; otherwise adds new tag" — so it is ADD-OR-MOVE, never replace-all, and it CANNOT be used to take a tag off. THERE IS NO WAY TO REMOVE A PRODUCT TAG: Meta documents Creating, Reading and Updating on this edge and no delete at all, so Hermoso will not guess at one — deleting the post is the only thing that removes its tags, and the reply says so rather than implying otherwise. The answer is always READ BACK from Instagram, and it separates tags that are STORED from tags that will actually be SHOWN — only an "approved" product ever appears on a published post. Read is free; the update costs 0 credits too. Whether Meta has approved Hermoso\'s app for the permission this needs is read LIVE from Meta: when it has not, list_connectors says so under Meta and this tool\'s refusal says Meta hasn\'t approved Hermoso for it yet. That is Meta\'s decision about Hermoso, not the user\'s connection, so never tell them to reconnect for it.',
|
|
8080
8081
|
inputSchema: {
|
|
8081
8082
|
mediaId: z.string().describe('the numeric Instagram media id, from list_instagram_media'),
|
|
8082
|
-
tags: z.array(z.object({ product_id: z.string(), x: z.number(), y: z.number() })).optional().describe('ADD or MOVE these tags. x and y are FRACTIONS of the image, 0.0 (left/top) to 1.0 (right/bottom)
|
|
8083
|
+
tags: z.array(z.object({ product_id: z.string(), x: z.number().optional(), y: z.number().optional() })).optional().describe('ADD or MOVE these tags. Omit to just read. ON A PHOTO each tag is {product_id, x, y}: x and y are FRACTIONS of the image, 0.0 (left/top) to 1.0 (right/bottom), 0.5,0.5 is the middle, and both are required. ON A REEL OR VIDEO it is {product_id} ALONE: Instagram takes no position on a video tag, so coordinates sent for one are not used and the reply says so. Max 20 on a photo or feed video, 30 on a Reel. A carousel is tagged on its SLIDES: pass the slide’s own media id.'),
|
|
8083
8084
|
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
8084
8085
|
},
|
|
8085
8086
|
outputSchema: { mediaId: z.string().optional(), updated: z.boolean().optional(), count: z.number().nullable().optional(), tags: z.array(z.any()).optional(), summary: z.string().optional(), removal: z.string().optional() },
|
|
@@ -17986,6 +17987,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17986
17987
|
}
|
|
17987
17988
|
const brief = [`Recreate the reference video for this brand${product ? ` — advertising ${product}` : ''}: keep its hook device, structure, cuts and pacing, but make every word, face, setting and product this brand's own`, changes ? `What the user wants changed or kept: ${changes}` : ''].filter(Boolean).join('. ');
|
|
17988
17989
|
const d = await apiSSE('/api/create', { stream: true, brand: brandObj, product: brief, format: 'video', reference: { url: link }, matchReferenceLength: !_len, ...(_len ? { durationSeconds: _len } : {}), language: language || '', userAsk: brief });
|
|
17990
|
+
// A clone the balance cannot cover streams back {choice} instead of a creative (priced on the ORIGINAL's length
|
|
17991
|
+
// since 2026-10-07): raise it as the 402 wrap() already spells, never as an empty "Clone plan".
|
|
17992
|
+
if (d?.data?.choice && typeof d.data.choice === 'object') throw Object.assign(new Error(String(d.data.choice.message || 'This video does not fit your balance.')), { status: 402, videoChoice: d.data.choice, _viaApi: true });
|
|
17989
17993
|
const c = d?.data?.creative || d?.creative || d;
|
|
17990
17994
|
if (brandObj && !c.brand) c.brand = { name: brandObj.name || '', domain: brandObj.domain || '', logo: brandObj.logo || '', sells: brandObj.sells || '', palette: (brandObj.palette || []).slice(0, 4), productImages: (brandObj.productImages || []).slice(0, 4) };
|
|
17991
17995
|
const planned = Math.round(+c.render_plan?.duration_seconds || (c.video_storyboard?.scenes || []).reduce((s, x) => s + (+x.seconds || 0), 0) || 0);
|
|
@@ -18791,8 +18795,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18791
18795
|
}, wrap(async (a) => {
|
|
18792
18796
|
// A PROMPT OR SHOTS, EITHER ONE (2026-10-05). `shots` replaces `prompt` (Kling sends multi_prompt and no prompt at all),
|
|
18793
18797
|
// so the schema leaves prompt optional and the one refusal lives here: neither is nothing to render. Free, no network.
|
|
18798
|
+
// `speak` alone is a whole request too (2026-10-08): the talk-to-camera lane writes its own staging when prompt is
|
|
18799
|
+
// empty (lib/talking-video.mjs talkingVideoPrompt), and this refusal had been turning every speak-only call away.
|
|
18794
18800
|
const _hasShots = Array.isArray(a.shots) && a.shots.length > 0;
|
|
18795
|
-
|
|
18801
|
+
const _hasSpeak = !!String(a.speak || '').trim();
|
|
18802
|
+
if (!String(a.prompt || '').trim() && !_hasShots && !_hasSpeak) return ok('NOTHING WAS RENDERED and nothing was charged. Say what the clip should show: send `prompt`, or `shots` (a list of {prompt, seconds}) for a multi-shot clip on a model with multiShot true in hermoso_capabilities.', { refused: 'missing_prompt' });
|
|
18796
18803
|
// NEVER SILENTLY TRUNCATE A LENGTH ASK. One generation = ONE continuous clip, and workVideo resolves an
|
|
18797
18804
|
// over-long ask by picking the longest clip the model has — so a 40s generate_video call used to render 15s and
|
|
18798
18805
|
// report success. A refusal that names the real ceiling and the real alternative is the honest answer, and it
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.407",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 906 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. 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 and
|
|
5
|
+
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 906 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. 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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|