hermoso 0.1.37 → 0.1.41
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 +22 -6
- package/package.json +2 -2
package/mcp/tools.mjs
CHANGED
|
@@ -3656,7 +3656,7 @@ export function registerTools(server) {
|
|
|
3656
3656
|
unitCost: z.number().optional().describe('the bid'),
|
|
3657
3657
|
currencyCode: z.string().optional(),
|
|
3658
3658
|
locale: z.record(z.any()).optional(),
|
|
3659
|
-
country: z.string().optional().describe('campaign locale country, default US'),
|
|
3659
|
+
country: z.string().optional().describe('campaign locale country, default US — this is the ad UI language market, NOT geo targeting. NEVER derive it from where the ad should run: targeting Canada still uses the US/en locale (LinkedIn refuses en_CA). Leave it alone unless the user explicitly asks for a different interface language.'),
|
|
3660
3660
|
language: z.string().optional().describe('campaign locale language, default en'),
|
|
3661
3661
|
locations: z.array(z.string()).optional().describe('REQUIRED unless targetingCriteria is given — geo URNs or bare geo ids from search_linkedin_ads_targeting'),
|
|
3662
3662
|
include: z.record(z.any()).optional().describe('further targeting facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]}'),
|
|
@@ -4445,7 +4445,7 @@ export function registerTools(server) {
|
|
|
4445
4445
|
|
|
4446
4446
|
server.registerTool('make_explainer', {
|
|
4447
4447
|
title: 'Make an explainer video',
|
|
4448
|
-
description: "Turn a TOPIC into a finished narrated explainer video. Writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS,
|
|
4448
|
+
description: "Turn a TOPIC into a finished narrated explainer video. Writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE, exactly as Higgsfield's stills pipeline does it — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; `captions:true` adds held key points and `subtitles:true` adds narration-timed CAPS (see both). It is an image film WITH motion, not N video-model renders — that's what keeps it affordable. `style` picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; `frameDensity:'lean'` roughly halves it and `'minimal'` (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.",
|
|
4449
4449
|
inputSchema: {
|
|
4450
4450
|
topic: z.string().describe('what the explainer should teach or explain — a topic or a short brief'),
|
|
4451
4451
|
durationSeconds: z.number().optional().describe('target length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections'),
|
|
@@ -5129,8 +5129,14 @@ export function registerTools(server) {
|
|
|
5129
5129
|
// workspace was revoked. The route now 403s that case by name instead of quietly scoping the read to the
|
|
5130
5130
|
// caller's own empty account (R6-6), so it surfaces as an error through wrap() rather than as "0 connected".
|
|
5131
5131
|
const empty = '\nIf this brand should have connected accounts, check WHICH workspace this connection is on: list_brands names every brand on this account plus every one shared with you, and use_brand switches. Otherwise connect one in the app — Workspace ▸ Connectors (linking needs a browser).';
|
|
5132
|
-
|
|
5133
|
-
|
|
5132
|
+
// AN AGENT NEVER SEES A PICKER PROVIDER'S RAW `accountLabel` (2026-08-04). It names whoever AUTHORIZED the
|
|
5133
|
+
// connection, and on LinkedIn that is a tickable, postable identity the user has almost certainly NOT ticked
|
|
5134
|
+
// (config.liMember is off by default) — printing it told Studio a personal profile was postable when it was not.
|
|
5135
|
+
// The SERVER decides what may be said (`agentLabel`, from lib/shared-identities.mjs); this only renders it, and
|
|
5136
|
+
// `accountLabel` is stripped from structuredContent too, or the model reads it there instead.
|
|
5137
|
+
const lines = on.map(c => ` • ${c.provider}${c.agentLabel ? ` — ${c.agentLabel}` : ''} (${c.status || 'active'})`);
|
|
5138
|
+
const safe = { ...d, connectors: (d.connectors || []).map(({ accountLabel, ...c }) => c) };
|
|
5139
|
+
return ok(`${on.length} connected:\n${lines.join('\n') || ' (none)'}\nAvailable to connect: ${(d.providers || []).join(', ') || '(none configured)'}.${on.length ? '' : empty}`, safe);
|
|
5134
5140
|
}));
|
|
5135
5141
|
// ── CONNECTOR WRITES. Connecting needs a browser (OAuth consent) and is correctly NOT headless — but the other two
|
|
5136
5142
|
// halves of connector management are, and were web-only: DISCONNECTING, and choosing WHICH accounts a brand may
|
|
@@ -5181,6 +5187,15 @@ export function registerTools(server) {
|
|
|
5181
5187
|
identities: (d) => (d.accounts || []).map(a => ({ id: String(a.id), name: a.name || a.id, type: 'ad_account', selected: !!a.selected, detail: [a.currency, a.test ? 'TEST account' : '', a.role].filter(Boolean).join(', ') })),
|
|
5182
5188
|
write: (ids) => ['/api/linkedin/ads-scope', { adAccountIds: ids }],
|
|
5183
5189
|
},
|
|
5190
|
+
// REDDIT ADS — the same law-4-with-no-path-to-tick gap the linkedin_ads row above was added to close (2026-08-04).
|
|
5191
|
+
// Its tools already say "ask the user to tick the ones it may use (or call set_connector_accounts)", and
|
|
5192
|
+
// set_connector_accounts' enum is DERIVED from this table, so without a row here that instruction named a tool
|
|
5193
|
+
// that would reject the provider.
|
|
5194
|
+
reddit_ads: {
|
|
5195
|
+
label: 'Reddit Ads', read: '/api/reddit-ads/accounts',
|
|
5196
|
+
identities: (d) => (d.accounts || []).map(a => ({ id: String(a.id), name: a.name || a.id, type: 'ad_account', selected: !!a.selected, detail: a.currency || '' })),
|
|
5197
|
+
write: (ids) => ['/api/reddit-ads/scope', { adAccountIds: ids }],
|
|
5198
|
+
},
|
|
5184
5199
|
microsoft_ads: {
|
|
5185
5200
|
label: 'Microsoft Ads', read: '/api/microsoft-ads/accounts',
|
|
5186
5201
|
identities: (d) => (d.accounts || []).map(a => ({ id: String(a.accountId), name: a.name, type: 'ad_account', selected: !!a.selected, detail: a.customerId ? `customer ${a.customerId}` : '' })),
|
|
@@ -5245,11 +5260,12 @@ export function registerTools(server) {
|
|
|
5245
5260
|
const live = ((await apiGet('/api/connectors')).connectors || []).filter(c => c && c.status !== 'revoked');
|
|
5246
5261
|
const hit = live.find(c => c.provider === provider);
|
|
5247
5262
|
if (!hit) return { content: [{ type: 'text', text: `Nothing connected for "${provider}". Connected: ${live.map(c => c.provider).join(', ') || '(none)'}.` }], isError: true };
|
|
5248
|
-
|
|
5263
|
+
// agentLabel, never accountLabel — the confirm sentence is model-visible text like any other (2026-08-04).
|
|
5264
|
+
if (a.confirm !== true) return ok(`This disconnects ${hit.provider}${hit.agentLabel ? ` (${hit.agentLabel})` : ''} from this workspace: our access is revoked at the provider, the stored credentials are deleted, and every ${hit.provider} tool stops working until someone reconnects it IN A BROWSER — I can't do that step. Already-published posts and running campaigns are untouched. Confirm with the user, then call again with confirm:true.`, { ok: false, provider });
|
|
5249
5265
|
// Straight through the app's own disconnect route → Connectors.remove(), which carries the shared-grant guard
|
|
5250
5266
|
// (all six google_* connectors ride ONE OAuth client id, so a naive revoke would silently kill the siblings).
|
|
5251
5267
|
await apiPost(`/api/connectors/${encodeURIComponent(provider)}/disconnect`, {});
|
|
5252
|
-
return ok(`Disconnected ${provider}${hit.
|
|
5268
|
+
return ok(`Disconnected ${provider}${hit.agentLabel ? ` (${hit.agentLabel})` : ''}. Reconnect from the app: Workspace ▸ Connectors ▸ ${provider}.`, { ok: true, provider, disconnected: true });
|
|
5253
5269
|
}));
|
|
5254
5270
|
server.registerTool('list_team', {
|
|
5255
5271
|
title: 'List team members',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI
|
|
5
|
+
"description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI \u2014 spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus TikTok/Instagram/YouTube organic \u2014 then publish to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest and build & manage the ad campaigns behind them on Meta, Google Ads, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads. MCP server (262 tools), CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|