hermoso 0.1.473 → 0.1.475
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 +23 -9
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -2552,7 +2552,14 @@ const splitW = (destructiveHint, idempotentHint) => Object.freeze({ readOnlyHint
|
|
|
2552
2552
|
const splitOp = (name, set, params, ann, title, description, desc) => Object.freeze({ name, set: Object.freeze(set), params: Object.freeze(params), ann, title, description, ...(desc ? { desc: Object.freeze(desc) } : {}) });
|
|
2553
2553
|
const X_BILLED = ' Needs X connected; X bills per call, so it costs credits.';
|
|
2554
2554
|
const META_ACCT = ['account', 'pageId'];
|
|
2555
|
-
|
|
2555
|
+
// A MULTI-ACCOUNT CHANNEL'S OPERATIONS NAME THE ACCOUNT TOO (2026-10-30). withAccountArg gives every tool of a channel in
|
|
2556
|
+
// MULTI_ACCOUNT_CHANNELS an `account` argument at the registry, but an operation tool takes ONLY the params its op lists
|
|
2557
|
+
// (widgetSplitDef / widgetSplitHandler), so on a widget host every YouTube / X / Bluesky / Pinterest operation silently
|
|
2558
|
+
// lost it: a ChatGPT user with two channels connected could not say which one a playlist, caption or board change was
|
|
2559
|
+
// for. Appended here, once, for every op of an account-scoped tool, so a split added tomorrow inherits it.
|
|
2560
|
+
const withSplitAccounts = (m) => Object.fromEntries(Object.entries(m).map(([from, ops]) => [from, !accountScopedTool(from) ? ops
|
|
2561
|
+
: ops.map((o) => (o.params.some((p) => p.replace(/!$/, '') === 'account') ? o : splitOp(o.name, o.set, [...o.params, 'account'], o.ann, o.title, o.description, o.desc)))]));
|
|
2562
|
+
export const WIDGET_HOST_SPLITS = Object.freeze(withSplitAccounts({
|
|
2556
2563
|
moderate_meta_comment: [
|
|
2557
2564
|
splitOp('hide_meta_comment', { action: 'hide' }, ['commentId!', ...META_ACCT], splitW(true, true), 'Hide a Facebook or Instagram comment', 'Use this when a comment on the brand’s own Facebook or Instagram post should be hidden from the public. Reversible with unhide_meta_comment. Get commentId from list_meta_comments. Free.'),
|
|
2558
2565
|
splitOp('unhide_meta_comment', { action: 'unhide' }, ['commentId!', ...META_ACCT], splitW(true, true), 'Unhide a Facebook or Instagram comment', 'Use this when a comment the brand hid on its own Facebook or Instagram post should be visible again. Get commentId from list_meta_comments. Free.'),
|
|
@@ -2749,7 +2756,7 @@ export const WIDGET_HOST_SPLITS = Object.freeze({
|
|
|
2749
2756
|
splitOp('edit_pinterest_board_details', {}, ['boardId!', 'name', 'description'], splitW(true, true), 'Rename a Pinterest board or edit its description', 'Use this when a Pinterest board’s name or description should change. It overwrites the public text. Get boardId from list_pinterest_boards. 0 credits.', { name: 'the new board name' }),
|
|
2750
2757
|
splitOp('set_pinterest_board_visibility', {}, ['boardId!', 'privacy!'], splitW(true, true), 'Make a Pinterest board public or secret', 'Use this when a Pinterest board should become SECRET (hidden, with every Pin on it, from everyone but this account) or PUBLIC again. Say what SECRET hides and get a yes first. It is reversible, and the reply reports how many Pins were hidden. 0 credits.', { privacy: 'PUBLIC or SECRET; SECRET hides every Pin on the board from everyone but this account' }),
|
|
2751
2758
|
],
|
|
2752
|
-
});
|
|
2759
|
+
}));
|
|
2753
2760
|
// op tool name → the multi-op tool it comes from.
|
|
2754
2761
|
export const WIDGET_SPLIT_OF = Object.freeze(Object.fromEntries(Object.entries(WIDGET_HOST_SPLITS).flatMap(([from, ops]) => ops.map((o) => [o.name, from]))));
|
|
2755
2762
|
// Every name a widget host is never LISTED, whatever its scope. Pure, so the checks run it.
|
|
@@ -6296,6 +6303,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6296
6303
|
description: z.string().optional().describe('YOUTUBE — replace the video description; "" clears it and the caption is used.'),
|
|
6297
6304
|
tags: z.array(z.string()).optional().describe('YOUTUBE — replaces the WHOLE tag list; an empty array [] clears the tags.'),
|
|
6298
6305
|
thumbnailUrl: z.string().optional().describe('YOUTUBE: replace the custom thumbnail; "" goes back to a frame of the video, "auto" to YouTube’s pick.'),
|
|
6306
|
+
paidProductPlacement: z.boolean().optional().describe('YOUTUBE: paid product placement (shows the disclosure) — false removes it'),
|
|
6307
|
+
brandPartner: z.string().optional().describe('YOUTUBE: sponsoring brand channel (UC… or @handle); "" removes it'),
|
|
6299
6308
|
// The same six the CREATE path gained — a field you can set at create and not at edit is a one-way door,
|
|
6300
6309
|
// and reschedule_post is how an agent corrects a queued post it got wrong.
|
|
6301
6310
|
replyControl: z.enum(['everyone', 'accounts_you_follow', 'mentioned_only', 'parent_post_author_only', 'followers_only']).optional().describe('THREADS ONLY — who may reply.'),
|
|
@@ -18557,6 +18566,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18557
18566
|
title: 'TikTok post insights',
|
|
18558
18567
|
description: 'Per-POST analytics for the connected TikTok account: views, reach, likes, comments, shares, saves, average watch time, full-watch rate, the RETENTION CURVE (share still watching at each second — the hook read), traffic sources (For You, Follow, Search, Sound, profile…), new followers, profile views, website clicks, lead submissions and the audience by country, gender and new-vs-returning. Newest posts first, up to 20 a page (cursor for more), or name videoIds. Data lags 24-48 hours. Needs the TikTok ACCOUNT authorization; an older one says which permission to re-authorize for. Read-only, free.',
|
|
18559
18568
|
inputSchema: {
|
|
18569
|
+
...MANAGE_BRAND,
|
|
18560
18570
|
videoIds: z.array(z.string()).optional().describe('post ids to read; omit for the newest posts'),
|
|
18561
18571
|
maxCount: z.number().optional().describe('1-20, default 20'),
|
|
18562
18572
|
cursor: z.string().optional().describe('pagination cursor from the previous reply, to fetch the next page'),
|
|
@@ -18565,7 +18575,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18565
18575
|
outputSchema: { videos: z.array(z.any()).optional(), cursor: z.any().optional(), hasMore: z.boolean().optional(), note: z.string().optional() },
|
|
18566
18576
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
18567
18577
|
}, wrap(async (a) => {
|
|
18568
|
-
const
|
|
18578
|
+
const { brand, ...q } = a; // the brand scopes the call (?brandId=), it is not a query field of its own
|
|
18579
|
+
const d = await apiGet('/api/tiktok-account/videos', queryBrand(a, { ...q, videoIds: a.videoIds ? a.videoIds.join(',') : undefined }));
|
|
18569
18580
|
return ok(`${d.note}\n${(d.videos || []).map(v => `• ${v.itemId}: ${v.views ?? '?'} views, reach ${v.reach ?? '?'}, avg watch ${v.averageWatchSeconds ?? '?'}s, ${v.retentionAt3s ?? '?'}% still watching at 3s — ${(v.caption || '').slice(0, 60)}`).join('\n')}`, d);
|
|
18570
18581
|
}));
|
|
18571
18582
|
server.registerTool('tiktok_spark_recommendations', {
|
|
@@ -19885,7 +19896,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
19885
19896
|
// which is how "we have no data on that post" becomes a confident "it got no engagement".
|
|
19886
19897
|
server.registerTool('linkedin_profile_followers', {
|
|
19887
19898
|
title: 'Followers of a personal LinkedIn profile',
|
|
19888
|
-
description: 'Follower count of the PERSONAL LinkedIn profile shared with this brand — the lifetime total and, with startDate / endDate, the new followers per day (LinkedIn\u2019s memberFollowersCount). This is the creator / founder growth number; Company Page followers are in linkedin_page_analytics. It needs a personal profile ticked in Manage accounts (list_connector_accounts provider linkedin) and the LinkedIn analytics authorization on the LinkedIn card — if the reply says re-authorize, say exactly that. Read-only, free.',
|
|
19899
|
+
description: 'Follower count of the PERSONAL LinkedIn profile shared with this brand — the lifetime total and, with startDate / endDate, the new followers per day (LinkedIn\u2019s memberFollowersCount). This is the creator / founder growth number; Company Page followers are in linkedin_page_analytics. It needs a personal profile ticked in Manage accounts (list_connector_accounts provider linkedin, then set_connector_accounts with its id to tick it) and the LinkedIn analytics authorization on the LinkedIn card — if the reply says re-authorize, say exactly that. Read-only, free.',
|
|
19889
19900
|
inputSchema: {
|
|
19890
19901
|
startDate: z.string().optional().describe('YYYY-MM-DD — include for a daily series'),
|
|
19891
19902
|
endDate: z.string().optional().describe('YYYY-MM-DD, exclusive, default today'),
|
|
@@ -22079,7 +22090,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
22079
22090
|
const s = plan.style || {};
|
|
22080
22091
|
const how = [s.hookDevice ? `Open the way the reference opens: ${s.hookDevice}` : '', s.pacing ? `Pacing: ${s.pacing}` : '', s.narration ? `Narration: ${s.narration}` : ''].filter(Boolean).join('. ');
|
|
22081
22092
|
const captions = typeof a.captions === 'boolean' ? a.captions : !!(plan.reference && s.captions); // on only when the WATCHED reference burns subtitles (no-overlays law, 2026-10-26)
|
|
22082
|
-
|
|
22093
|
+
// female/male are shortcuts onto the default voices; any other name is a narration voice passed straight through,
|
|
22094
|
+
// exactly as make_explainer takes it (never rigid: presets are shortcuts, never the only door — enum-ledger.json).
|
|
22095
|
+
const _v = String(a.voice || s.voice || '').trim();
|
|
22096
|
+
const voice = /^male$/i.test(_v) ? (vo.male || 'George') : (!_v || /^female$/i.test(_v)) ? (vo.female || 'Sarah') : _v;
|
|
22083
22097
|
return {
|
|
22084
22098
|
topic: `Episode ${ep.episode} of ${n} in a faceless short-video series about "${topic}". This episode: ${ep.title}. ${ep.brief}${how ? ` ${how}.` : ''} No presenter on camera.`,
|
|
22085
22099
|
durationSeconds: plan.durationSeconds, aspectRatio: a.aspectRatio || s.aspectRatio || '9:16',
|
|
@@ -22106,7 +22120,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
22106
22120
|
aspectRatio: z.enum(['9:16', '16:9', '1:1']).optional().describe('default: the reference\'s shape, else 9:16'),
|
|
22107
22121
|
style: z.string().optional().describe('override the look in words (e.g. "flat 2D cartoon") instead of the reference\'s'),
|
|
22108
22122
|
captions: z.boolean().optional().describe('burn subtitles of every spoken word. Default: on only when the reference itself burns its spoken words as subtitles, otherwise off; nothing else is ever put on screen (no app screens, URLs, cards or stickers) unless the user asks'),
|
|
22109
|
-
voice: z.
|
|
22123
|
+
voice: z.string().optional().describe("narrator: 'female' or 'male' (default follows the reference), or any narration voice name make_explainer takes"),
|
|
22110
22124
|
format: z.enum(['explainer', 'faceless_channel']).optional().describe("renderer lane: 'explainer' (default; exact length) or 'faceless_channel' (2K, whole 10s blocks)"),
|
|
22111
22125
|
planOnly: z.boolean().optional().describe('write the episode plan and style card, render nothing (a small planning charge)'),
|
|
22112
22126
|
plan: z.any().optional().describe('the `plan` a planOnly call returned, to run it without planning again'),
|
|
@@ -24610,12 +24624,12 @@ function memoryNoteVerdict(text) {
|
|
|
24610
24624
|
|
|
24611
24625
|
server.registerTool('upscale_video', {
|
|
24612
24626
|
title: 'Upscale video',
|
|
24613
|
-
description: "Upscale a video to higher resolution (2x) for final delivery. Paid render; returns the served URL. A 480p Seedance 2.5 draft: finish_draft (same take, native 1080p).
|
|
24614
|
-
inputSchema: { video: z.string().describe('the source video URL'), engine: z.enum(['standard', 'flux']).optional().describe("default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler"), mode: z.enum(['precise', 'creative']).optional().describe("FLUX only — 'creative' turns on its detail-enhancement pass; default precise") },
|
|
24627
|
+
description: "Upscale a video to higher resolution (2x) for final delivery. Paid render; returns the served URL. A 480p Seedance 2.5 draft: finish_draft (same take, native 1080p). Three engines: the default ('standard') is the safe precision upscaler; engine:'flux' is the FLUX 3 video upscaler (1080p/2K/4K) with an optional mode:'creative' detail-enhancement pass; engine:'detail' rebuilds fine detail such as hair, skin and fabric, the sharpest result for a hero shot and the most expensive. Pick 'flux' or 'detail' when the user asks for added detail rather than a faithful enlargement.",
|
|
24628
|
+
inputSchema: { video: z.string().describe('the source video URL'), engine: z.enum(['standard', 'flux', 'detail']).optional().describe("default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler. 'detail' = rebuilds fine detail, costs the most"), mode: z.enum(['precise', 'creative']).optional().describe("FLUX only — 'creative' turns on its detail-enhancement pass; default precise") },
|
|
24615
24629
|
outputSchema: { ...JOB_OUT },
|
|
24616
24630
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
24617
24631
|
}, wrap(async ({ video, engine, mode }) => {
|
|
24618
|
-
const r = await renderJob('upscale', { video, factor: 2, ...(engine ? { engine } : {}), ...(mode ? { mode } : {}) }, engine === 'flux' ? 'Upscale 2x · FLUX' : 'Upscale 2x');
|
|
24632
|
+
const r = await renderJob('upscale', { video, factor: 2, ...(engine ? { engine } : {}), ...(mode ? { mode } : {}) }, engine === 'flux' ? 'Upscale 2x · FLUX' : engine === 'detail' ? 'Upscale 2x · Detail' : 'Upscale 2x');
|
|
24619
24633
|
return okVideo(`Upscaled video: ${r.url}`, r);
|
|
24620
24634
|
}));
|
|
24621
24635
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.475",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1089 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",
|