hermoso 0.1.491 → 0.1.494
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/client.mjs +1 -1
- package/mcp/tools.mjs +26 -3
- package/package.json +1 -1
package/mcp/client.mjs
CHANGED
|
@@ -159,7 +159,7 @@ async function unwrap(res, { read = false } = {}) {
|
|
|
159
159
|
// ledger with the tool name off x-hermoso-tool. wrap() reports ONLY the errors that lack this marker — a local
|
|
160
160
|
// throw, a schema rejection, a socket reset — which is what stops the twins double-counting every 4xx.
|
|
161
161
|
// `connectUrl` rides a not-connected 401 with the brand already in it, so a hint never rebuilds the link.
|
|
162
|
-
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(typeof body?.code === 'string' ? { apiCode: body.code } : {}), ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}), ...(body?.meta?.welcomeClaim && typeof body.meta.welcomeClaim === 'object' && body.meta.welcomeClaim.url ? { welcomeClaim: { credits: Number(body.meta.welcomeClaim.credits) || 0, url: String(body.meta.welcomeClaim.url) } } : {}), ...(body?.remedy && typeof body.remedy === 'object' && typeof body.remedy.code === 'string' ? { remedy: body.remedy } : {}) }); // `remedy` (2026-10-25): the server's structured next step (a publish outcome, Meta's security hold), handed on whole to wrap()'s _meta and /v1 // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
162
|
+
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(res.status === 429 ? { retryAfter: Number(body?.retryAfter) || Number(res.headers?.get?.('retry-after')) || 60 } : {}), ...(typeof body?.code === 'string' ? { apiCode: body.code } : {}), ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}), ...(body?.meta?.welcomeClaim && typeof body.meta.welcomeClaim === 'object' && body.meta.welcomeClaim.url ? { welcomeClaim: { credits: Number(body.meta.welcomeClaim.credits) || 0, url: String(body.meta.welcomeClaim.url) } } : {}), ...(body?.remedy && typeof body.remedy === 'object' && typeof body.remedy.code === 'string' ? { remedy: body.remedy } : {}) }); // `remedy` (2026-10-25): the server's structured next step (a publish outcome, Meta's security hold), handed on whole to wrap()'s _meta and /v1 // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
163
163
|
}
|
|
164
164
|
// WHAT THIS CALL WAS CHARGED (2026-10-19). A paid route answers {data, creditsUsed}, and the unwrap below drops the
|
|
165
165
|
// sibling, so a synchronous paid tool's reply could never say what it cost. It is added to the running tool call's
|
package/mcp/tools.mjs
CHANGED
|
@@ -596,6 +596,8 @@ const wrap = (fn) => {
|
|
|
596
596
|
// META'S SECURITY HOLD (code 31/3858385): the server's sentence already carries the steps; this names the move
|
|
597
597
|
// so an agent relays it instead of retrying or telling the user to reconnect. Read from the STRUCTURED field.
|
|
598
598
|
if (e?.metaAuthHold === true) _hints.push({ do: 'stop retrying; ask the user to clear Meta\u2019s security check: as the Facebook profile that connected Hermoso, on a computer (not the Ads Manager app), create a NEW draft ad in Ads Manager, click Start authentication (or Fix errors) on the error it shows and complete it, or follow a red banner if one is shown; then run the same call again', why: 'Meta refuses new or edited ads from that profile until it re-authenticates; the connection and permissions are fine and reconnecting with the same profile does not clear it', ...(e?.remedy?.login ? { who: e.remedy.who } : {}) });
|
|
599
|
+
// A RATE LIMIT SAYS HOW LONG, IN A NUMBER AND A SENTENCE (2026-11-02: an agent called set_youtube_thumbnail 73 times on "wait a few minutes"). Read from the structured field the client carried.
|
|
600
|
+
if (Number(e?.status) === 429 && e?.retryAfter > 0) { const _w = Number(e.retryAfter), _ww = _w < 90 ? `${Math.round(_w)} seconds` : `${Math.round(_w / 60)} minutes`; msg = `${msg} Do not call ${_tool} again yet: wait ${_ww} first. Calling it sooner is refused the same way and can make the limit last longer.`; _hints.push({ do: `wait ${_ww}, then call ${_tool} once`, why: 'the platform is rate-limiting this call, so an immediate retry cannot succeed' }); }
|
|
599
601
|
if (Number(e?.status) === 401 && e?.connector) _hints.push({ do: `have the user connect "${e.connector}" (Settings \u25b8 Connectors, or the one-click link in this message)`, why: `${e.connector} is not connected in this workspace, so this tool can only answer 401 until it is` });
|
|
600
602
|
// A PUBLISH OUTCOME IS DATA (2026-10-25, fb_679fd3573ff0): "nothing was posted" and "could not confirm" both arrive
|
|
601
603
|
// as 502, so the hint follows the server's `remedy.posted`, never the status or the prose.
|
|
@@ -614,7 +616,7 @@ const wrap = (fn) => {
|
|
|
614
616
|
// so its ambiguous-publish advice keeps the marker rather than dropping it.
|
|
615
617
|
// `remedy` (2026-10-25): the server's structured next step, whole, plus its `code` — Meta's security hold (code +
|
|
616
618
|
// steps[]) or a publish outcome (posted:false | 'unknown'). /v1 makes `code` its error code and returns `remedy`.
|
|
617
|
-
return withHints({ content: [{ type: 'text', text: msg }], isError: true, _meta: { 'hermoso.ai/error': { status: Number(e?.status) || 0, connector: e?.connector ? String(e.connector) : '', ...(e?.videoChoice?.options && typeof e.videoChoice.options === 'object' ? { options: e.videoChoice.options } : {}), ...(e?.remedy && typeof e.remedy === 'object' && typeof e.remedy.code === 'string' ? { code: e.remedy.code, remedy: e.remedy } : {}) } } }, _hints); // `options` = the video choice's three ways forward, structured, so /v1 answers its 402 with them
|
|
619
|
+
return withHints({ content: [{ type: 'text', text: msg }], isError: true, _meta: { 'hermoso.ai/error': { status: Number(e?.status) || 0, ...(Number(e?.status) === 429 && e?.retryAfter > 0 ? { retryAfterSeconds: Number(e.retryAfter) } : {}), connector: e?.connector ? String(e.connector) : '', ...(e?.videoChoice?.options && typeof e.videoChoice.options === 'object' ? { options: e.videoChoice.options } : {}), ...(e?.remedy && typeof e.remedy === 'object' && typeof e.remedy.code === 'string' ? { code: e.remedy.code, remedy: e.remedy } : {}) } } }, _hints); // `options` = the video choice's three ways forward, structured, so /v1 answers its 402 with them
|
|
618
620
|
}
|
|
619
621
|
};
|
|
620
622
|
return outer;
|
|
@@ -21983,7 +21985,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
21983
21985
|
};
|
|
21984
21986
|
server.registerTool('make_motion_film', {
|
|
21985
21987
|
title: 'Make a motion graphics video',
|
|
21986
|
-
description: "MOTION GRAPHICS and ANIMATED VIDEO in one call
|
|
21988
|
+
description: "MOTION GRAPHICS and ANIMATED VIDEO in one call: kinetic typography (animated text), a logo animation or intro, a product launch film, an animated social cut. MOVING PICTURES are the default for launch, product and social: in each scene with a picture, one thing physically happens (a seed splits open, an eye opens, the view travels through a forest), filmed by an image-to-video model, with the type, transitions and the music's beat put on top; a few minutes, priced per shot, and with no pictures given each scene paints its own. moving:false, and always for type and logo, makes it in code with no video model: type, pictures and drawn graphics filmed by a moving virtual camera, a 1080p MP4 in about a minute, billed for its compute only. Pick it for 'animated video', 'motion graphics', 'kinetic text', 'text animation', 'logo animation', 'logo intro', 'animated explainer' asks. recipe: 'type' (every line its own animated card, no picture needed), 'logo' (the logo assembles out of dots and settles, the name and tagline land under it), 'launch' (30s 16:9), 'product' (15s), 'social' (15s 9:16). Or `spec`, the whole film as JSON (help:true returns the language). STEPS: without confirm it is a FREE quote that gives both prices (length, the music's tempo; the spec in structuredContent): say both; preview:true is a 1-credit contact sheet of the still version; confirm:true renders. You may write `moves` yourself, or the film's own planner writes them for a few credits. Only the user's or the brand's words appear: never invent a claim, number or review. projectUrl = a past film's url returns its saved spec to edit and render again.",
|
|
21987
21989
|
inputSchema: {
|
|
21988
21990
|
recipe: z.enum(['type', 'logo', 'launch', 'product', 'social']).optional().describe('type, logo, launch, product or social; help:true lists the recipes'),
|
|
21989
21991
|
hook: z.string().optional().describe("opening line; '*word*' = italic accent"),
|
|
@@ -21998,6 +22000,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
21998
22000
|
music: z.string().optional().describe("'off' = silent"),
|
|
21999
22001
|
theme: z.enum(['light', 'dark']).optional().describe("logo: dark for a light logo"),
|
|
22000
22002
|
energy: z.number().optional().describe('how much the film keeps moving, 0 to 1; 0 films exactly what is written'),
|
|
22003
|
+
moving: z.boolean().optional().describe('true: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social'),
|
|
22004
|
+
moves: z.array(z.object({ scene: z.string().describe('scene id'), action: z.string().describe('the one thing that physically happens, in plain words'), still: z.string().optional().describe('a picture to animate, when not the one in the scene'), stillPrompt: z.string().optional().describe('a picture to paint first, for a scene with none'), layer: z.string().optional().describe("the picture layer's id, when the scene has several"), video: z.string().optional().describe('a shot already filmed: kept, and not paid for again'), matchNext: z.boolean().optional().describe("the shot ends on the next scene's picture") })).optional().describe('per scene, what happens in its picture; omit and the planner writes them'),
|
|
22005
|
+
brief: z.string().optional().describe('what the film is about, for the planner'),
|
|
22006
|
+
model: z.string().optional().describe('image-to-video model id from hermoso_capabilities; default is the best value for short shots'),
|
|
22007
|
+
resolution: z.string().optional().describe('the moving shots: 720p or 1080p (default) where the model has them'),
|
|
22001
22008
|
spec: z.object({}).passthrough().optional().describe('the whole film as JSON, from a quote or a saved film; help:true shows the spec language'),
|
|
22002
22009
|
projectUrl: z.string().optional().describe("a past film's url; returns its saved spec to edit and render again"),
|
|
22003
22010
|
help: z.boolean().optional().describe('true returns the spec language and recipes; renders nothing'),
|
|
@@ -22023,15 +22030,31 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
22023
22030
|
const name = String(a.name || b.name || '').slice(0, 40), brandLogo = logo || b.logo || '';
|
|
22024
22031
|
let spec = a.spec && typeof a.spec === 'object' ? a.spec : null, music = typeof a.music === 'string' && a.music.trim() ? a.music.trim().slice(0, 300) : '';
|
|
22025
22032
|
let credits = null, previewCredits = null, bpm = null;
|
|
22033
|
+
// the moving version is the default wherever a film has pictures to move; a spec already holding filmed shots stays as it is unless asked, so changing a word does not re-buy them
|
|
22034
|
+
const hasClips = Array.isArray(spec?.scenes) && spec.scenes.some((s) => Array.isArray(s?.layers) && s.layers.some((l) => l && l.type === 'clip'));
|
|
22035
|
+
const wantMoving = a.moving ?? (a.preview ? false : Array.isArray(a.moves) && a.moves.length ? true : spec ? !hasClips : a.recipe === 'launch' || a.recipe === 'product' || a.recipe === 'social');
|
|
22026
22036
|
if (!spec) {
|
|
22027
22037
|
if (!a.recipe) throw Object.assign(new Error("Pass a recipe ('type', 'logo', 'launch', 'product' or 'social') with words, or a whole spec."), { status: 400, _userInput: true });
|
|
22028
|
-
const d = await apiPost('/api/motion/build', { recipe: a.recipe, size: a.size || null, seconds: a.seconds ?? null, mood: a.mood || null, theme: a.theme || null, ...(a.energy != null ? { energy: a.energy } : {}), brand: { name, palette: pal, logo: brandLogo ? 'yes' : '' }, words: { hook: a.hook || '', lines: a.lines || [], tagline: a.tagline || '' }, images });
|
|
22038
|
+
const d = await apiPost('/api/motion/build', { ...(wantMoving ? { moving: true } : {}), recipe: a.recipe, size: a.size || null, seconds: a.seconds ?? null, mood: a.mood || null, theme: a.theme || null, ...(a.energy != null ? { energy: a.energy } : {}), brand: { name, palette: pal, logo: brandLogo ? 'yes' : '' }, words: { hook: a.hook || '', lines: a.lines || [], tagline: a.tagline || '' }, images });
|
|
22029
22039
|
spec = d.spec; if (!music) music = d.music || ''; credits = d.credits; previewCredits = d.previewCredits; bpm = d.bpm;
|
|
22030
22040
|
}
|
|
22031
22041
|
const film = { config: { template: 'motion', spec, ...(music ? { music } : {}) }, brand: { name, domain: b.domain || '', logo: brandLogo, palette: pal } };
|
|
22032
22042
|
if (credits == null) { const q = await quoteJob('templatead', film); credits = q?.credits ?? null; if (q?.refused) throw Object.assign(new Error(`The film was refused before anything rendered (free): ${q.refused}`), { status: 400, _userInput: true }); }
|
|
22033
22043
|
const secs = Array.isArray(spec?.scenes) ? +spec.scenes.reduce((x, s) => x + (+s.seconds || 0), 0).toFixed(2) : null;
|
|
22034
22044
|
const what = `${secs != null ? secs + 's, ' : ''}${spec?.size || '16:9'}, ${Array.isArray(spec?.scenes) ? spec.scenes.length : '?'} scenes${music && music !== 'off' ? `, music "${music}"${bpm ? ` at ${bpm} BPM` : ''}` : ''}`;
|
|
22045
|
+
if (wantMoving) {
|
|
22046
|
+
const mv = { ...(a.model ? { model: a.model } : {}), ...(a.resolution ? { resolution: a.resolution } : {}) };
|
|
22047
|
+
const q = await apiPost('/api/motion/moving-quote', { spec, music, ...(Array.isArray(a.moves) && a.moves.length ? { moves: a.moves } : {}), ...mv });
|
|
22048
|
+
if (q?.moving) {
|
|
22049
|
+
if (!a.confirm) return ok(`QUOTE (nothing rendered or charged): with moving pictures this film is about ${q.credits} credits (${what}; ${q.moving} moving shot${q.moving === 1 ? '' : 's'} on ${q.model} at ${q.resolution}). With still pictures moved by a virtual camera it is ${q.stillFilmCredits} credits (moving:false). ${PRICE_ESTIMATE_LINE} Tell the user both prices, then call again with the same arguments and confirm:true (pass structuredContent.spec as spec to keep this exact film).`, { credits: q.credits, stillFilmCredits: q.stillFilmCredits, moving: q.moving, shots: q.scenes, lines: q.lines, model: q.model, resolution: q.resolution, seconds: secs, music, bpm, spec });
|
|
22050
|
+
let moves = Array.isArray(a.moves) && a.moves.length ? a.moves : null, planned = 0;
|
|
22051
|
+
if (!moves) { const p = await apiPost('/api/motion/moves', { spec, brief: a.brief || [a.hook, ...(a.lines || []), a.tagline].filter(Boolean).join(' / '), brand: { name } }); moves = p?.moves; planned = +p?.planCreditsUsed || 0; if (!Array.isArray(moves) || !moves.length) throw new Error('The moves for this film could not be written. Nothing else was charged. Try again, or pass moves yourself.'); }
|
|
22052
|
+
const r = await renderJob('motionmoving', { ...film, moves, ...mv }, 'MCP moving motion film');
|
|
22053
|
+
const raw = r?.raw || {};
|
|
22054
|
+
const notes = Array.isArray(raw.notes) && raw.notes.length ? `\nNOTE: ${raw.notes.join('; ')}` : '';
|
|
22055
|
+
return okVideo(`Motion film ready (${what}, ${raw.movingShots ?? moves.length} moving shot${(raw.movingShots ?? moves.length) === 1 ? '' : 's'}): ${r.url} [job ${r.jobId}]${planned ? ` Writing the moves was ${planned} credits.` : ''}${notes}\nIts spec is saved with the shots in it: make_motion_film({projectUrl: this url}) opens it, and changing the words costs only the render.`, r);
|
|
22056
|
+
}
|
|
22057
|
+
}
|
|
22035
22058
|
if (a.preview) {
|
|
22036
22059
|
const r = await renderJob('templatead', { ...film, config: { ...film.config, preview: true } }, 'MCP motion contact sheet');
|
|
22037
22060
|
if (r?.stillRendering) return ok(stillMsg(r), r);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.494",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1108 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",
|