hermoso 0.1.86 → 0.1.87
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 +14 -11
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -1525,7 +1525,7 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
1525
1525
|
// empty description box and no tags. The caption cannot stand in for them — it becomes the video TITLE.
|
|
1526
1526
|
description: z.string().optional().describe('YOUTUBE — the video DESCRIPTION, max 5000 characters: the box under the video carrying the links, the CTA and everything YouTube search reads. It is NOT the caption — a scheduled YouTube item’s text becomes its TITLE — so omitting this publishes the video with an empty description.'),
|
|
1527
1527
|
tags: z.array(z.string()).optional().describe('YOUTUBE — up to 30 search tags for the video (plain words, no #).'),
|
|
1528
|
-
altText: z.string().optional().describe('ACCESSIBILITY
|
|
1528
|
+
altText: z.string().optional().describe('ACCESSIBILITY \u2014 the screen-reader description of the attached image. X (max 1000 characters), Pinterest (max 500), LinkedIn COMPANY PAGE posts (max 4086) and INSTAGRAM (max 1000) each attach it to the picture; the schedule is REFUSED if it is longer than the tightest of the channels on it, rather than truncated on the way out. Write one whenever the post carries an image: describe what is IN the picture, never a repeat of the caption, which a screen reader already reads. TWO CHANNELS CARRY NONE, and neither is refused \u2014 the post still publishes, just undescribed there: a LinkedIn post to a PERSONAL profile (LinkedIn\u2019s member posting API has no alt-text field at all), and an INSTAGRAM VIDEO/Reel (Meta support alt_text on image posts only). On an Instagram CAROUSEL the one description is applied to every image slide.'),
|
|
1529
1529
|
link: z.string().optional().describe('a link to attach (Facebook)'),
|
|
1530
1530
|
// ── PER-CHANNEL PUBLISH SETTINGS (server-side SCHED_META_FIELDS, 2026-08-03). Every one of these has been on
|
|
1531
1531
|
// the immediate publish helper since the day it was written — tiktokPost's disclosures and interaction
|
|
@@ -1602,7 +1602,7 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
1602
1602
|
title: z.string().optional().describe('PINTEREST / YOUTUBE — replace the headline; "" clears it and goes back to deriving one from the caption'),
|
|
1603
1603
|
description: z.string().optional().describe('YOUTUBE — replace the video description; "" clears it. Remember the caption is the TITLE, not the description.'),
|
|
1604
1604
|
tags: z.array(z.string()).optional().describe('YOUTUBE — replaces the WHOLE tag list; an empty array [] clears the tags.'),
|
|
1605
|
-
altText: z.string().optional().describe('ACCESSIBILITY
|
|
1605
|
+
altText: z.string().optional().describe('ACCESSIBILITY \u2014 replace the image\u2019s screen-reader description (used by X, Pinterest, LinkedIn company Pages and Instagram image posts); "" clears it.'),
|
|
1606
1606
|
link: z.string().optional(),
|
|
1607
1607
|
// The per-channel publish settings (SCHED_META_FIELDS), on the same omitted-means-keep rule as everything else
|
|
1608
1608
|
// here: send only what changes. A whole-record field (`event`, `offer`, `poll`, `thread`) REPLACES the record.
|
|
@@ -7341,7 +7341,7 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
7341
7341
|
// leg priced by the same videoCostUsd the Models catalog quotes. Every lane SETTLES to the exact cost afterwards.
|
|
7342
7342
|
server.registerTool('clip_video', {
|
|
7343
7343
|
title: 'Clip a long video',
|
|
7344
|
-
description: "Cut ONE long video into several RANKED, ready-to-post short clips (podcast, webinar, interview, conference talk, long ad cut → Reels/Shorts/TikTok). Transcribes the source with timestamps, picks the strongest SELF-CONTAINED moments, then cuts + reframes each with ffmpeg — no video model renders anything, which is why it's fast and cheap. ACCEPTS: (a) a YouTube link (or Vimeo / Loom / Dailymotion / Streamable / Rumble / Wistia / Twitch / TED) — the server pulls the video down itself; (b) a direct https .mp4/.mov/.webm; (c) a Hermoso /generated/ URL (upload_file turns a local file into one). NOT supported: TikTok / Instagram / Facebook links, and anything age-restricted, private, members-only, geo-blocked or still LIVE — those fail fast with the real reason and are fully refunded, so ask for a direct file or an upload rather than retrying. Source must be at least ~15s and under ~600MB; only the first ~40 minutes is analysed (the result reports truncated:true when it hits that). Cost: a ~7-credit hold, settled to the exact transcription + encode cost, plus the clip-selection model's tokens billed as their own small event. RETURNS clips[] — each with its OWN served mp4 URL, title, hook, ready-to-post caption, 0-100 score and source timecode — not a single video. SUBTITLES ARE BURNED IN BY DEFAULT — slim white CAPS, thin black outline, bottom safe band, no box and no plate — because short-form is watched on mute; pass captions:false for clean footage. TIMING IS APPROXIMATE, NOT WORD-LEVEL: each cue is anchored to the transcript's own per-sentence timestamp and split inside a sentence by character count, so it tracks the speech closely but is not frame-accurate sync — never promise that. Read captionsBurned back off the result: it counts the clips that actually carry a burned track, and captionNote says why any are bare.",
|
|
7344
|
+
description: "Cut ONE long video into several RANKED, ready-to-post short clips (podcast, webinar, interview, conference talk, long ad cut → Reels/Shorts/TikTok). Transcribes the source with timestamps, picks the strongest SELF-CONTAINED moments, then cuts + reframes each with ffmpeg — no video model renders anything, which is why it's fast and cheap. THE VERTICAL REFRAME IS SUBJECT-AWARE: a few stills per clip go to ONE cheap vision call, which decides a SINGLE crop offset that is held for that clip's whole length — so a speaker sitting camera-left is not cropped out of their own clip, while the framing still never drifts INSIDE a clip (a per-frame crop truncates to whole pixels and shimmers, so it is deliberately not tracked). It costs one small vision call per clip, billed as its own event. When nothing is being discarded, or no single subject can be located, the crop stays dead centre exactly as before — read `reframedToSubject` and each clip's `reframeWhy` back off the result rather than assuming either way. ACCEPTS: (a) a YouTube link (or Vimeo / Loom / Dailymotion / Streamable / Rumble / Wistia / Twitch / TED) — the server pulls the video down itself; (b) a direct https .mp4/.mov/.webm; (c) a Hermoso /generated/ URL (upload_file turns a local file into one). NOT supported: TikTok / Instagram / Facebook links, and anything age-restricted, private, members-only, geo-blocked or still LIVE — those fail fast with the real reason and are fully refunded, so ask for a direct file or an upload rather than retrying. Source must be at least ~15s and under ~600MB; only the first ~40 minutes is analysed (the result reports truncated:true when it hits that). Cost: a ~7-credit hold, settled to the exact transcription + encode cost, plus the clip-selection model's tokens billed as their own small event. RETURNS clips[] — each with its OWN served mp4 URL, title, hook, ready-to-post caption, 0-100 score and source timecode — not a single video. SUBTITLES ARE BURNED IN BY DEFAULT — slim white CAPS, thin black outline, bottom safe band, no box and no plate — because short-form is watched on mute; pass captions:false for clean footage. TIMING IS APPROXIMATE, NOT WORD-LEVEL: each cue is anchored to the transcript's own per-sentence timestamp and split inside a sentence by character count, so it tracks the speech closely but is not frame-accurate sync — never promise that. Read captionsBurned back off the result: it counts the clips that actually carry a burned track, and captionNote says why any are bare.",
|
|
7345
7345
|
inputSchema: {
|
|
7346
7346
|
video: z.string().describe('the long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL'),
|
|
7347
7347
|
count: z.number().optional().describe('how many clips to cut, 1-8 (default 4)'),
|
|
@@ -8193,16 +8193,19 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
8193
8193
|
const hit = live.find(c => c.provider === provider);
|
|
8194
8194
|
if (!hit) return { content: [{ type: 'text', text: `Nothing connected for "${provider}". Connected: ${live.map(c => c.provider).join(', ') || '(none)'}.` }], isError: true };
|
|
8195
8195
|
// agentLabel, never accountLabel — the confirm sentence is model-visible text like any other (2026-08-04).
|
|
8196
|
-
// THE REVOKE CLAIM IS THE SERVER'S
|
|
8197
|
-
//
|
|
8198
|
-
//
|
|
8199
|
-
//
|
|
8200
|
-
//
|
|
8196
|
+
// THE REVOKE CLAIM IS THE SERVER'S, AND SINCE 2026-08-13 IT IS THE SAME FOR EVERY PROVIDER: a disconnect
|
|
8197
|
+
// removes THIS BRAND'S row and calls nothing at the provider. That is not a limitation to apologise for, it is
|
|
8198
|
+
// the point — an OAuth grant belongs to the (user, app) pair, so revoking from one brand would have killed the
|
|
8199
|
+
// same person's connection on every OTHER brand they had connected the account to. `removalNote` comes from
|
|
8200
|
+
// disconnectClaim() and names where the user can really revoke us. It is printed UNCONDITIONALLY on both the
|
|
8201
|
+
// confirm prompt and the result, because "this is brand-scoped" is a capability limit that stays true forever
|
|
8202
|
+
// rather than a per-provider quirk — the connector-card rule about desc-vs-setup copy, applied to an agent.
|
|
8201
8203
|
if (a.confirm !== true) return ok(`This disconnects ${hit.provider}${hit.agentLabel ? ` (${hit.agentLabel})` : ''} from this workspace: 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. ${hit.removalNote || ''} Already-published posts and running campaigns are untouched. Confirm with the user, then call again with confirm:true.`, { ok: false, provider, revokesAtProvider: hit.revokesAtProvider || null });
|
|
8202
|
-
// Straight through the app's own disconnect route → Connectors.remove()
|
|
8203
|
-
// (all
|
|
8204
|
+
// Straight through the app's own disconnect route → Connectors.remove(). The shared-grant guard that used to
|
|
8205
|
+
// matter here (all seven google_* connectors ride ONE OAuth client id, so a naive revoke killed the siblings)
|
|
8206
|
+
// is moot now that nothing revokes — the hazard is closed rather than guarded.
|
|
8204
8207
|
await apiPost(`/api/connectors/${encodeURIComponent(provider)}/disconnect`, {});
|
|
8205
|
-
return ok(`Disconnected ${provider}${hit.agentLabel ? ` (${hit.agentLabel})` : ''}
|
|
8208
|
+
return ok(`Disconnected ${provider}${hit.agentLabel ? ` (${hit.agentLabel})` : ''} from this brand.${hit.removalNote ? ` ${hit.removalNote}` : ''} Reconnect from the app: Workspace ▸ Connectors ▸ ${provider}.`, { ok: true, provider, disconnected: true, revokesAtProvider: hit.revokesAtProvider || null });
|
|
8206
8209
|
}));
|
|
8207
8210
|
// ── REMOVE ONLY WHAT I CONTRIBUTED (2026-08-11) ────────────────────────────────────────────────────────────────
|
|
8208
8211
|
// The web gained a "Remove my account" control the same day, and a WEB-ONLY capability is a defect
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI ad studio + marketing MCP (415 tools): build and manage ad campaigns on Meta, Google Ads, Reddit, X, TikTok, Snapchat, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads — generate finished video, image and UGC avatar ads, publish them to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest, spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus organic TikTok/Instagram/YouTube/Reddit, and read what they achieved in Google Analytics 4. 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",
|