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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +23 -9
  2. 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
- export const WIDGET_HOST_SPLITS = Object.freeze({
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 d = await apiGet('/api/tiktok-account/videos', { ...a, videoIds: a.videoIds ? a.videoIds.join(',') : undefined });
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
- const voice = (a.voice || s.voice) === 'male' ? (vo.male || 'George') : (vo.female || 'Sarah');
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.enum(['female', 'male']).optional().describe('narrator; default follows the reference'),
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). Two 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 — pick it when the user asks for the FLUX upscaler or wants added detail rather than a faithful enlargement.",
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.473",
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",