hermoso 0.1.377 → 0.1.379

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 +18 -15
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -5694,7 +5694,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5694
5694
  // never re-posts the Library; in 'review' mode each batch waits as drafts until approved.
5695
5695
  server.registerTool('get_post_refill', {
5696
5696
  title: 'Autoposting status',
5697
- description: 'Show autoposting (the posting refill) for this profile: whether it is on, its mode (review = each batch of fresh posts waits as drafts for approval; auto = fresh posts are scheduled straight away), its render budget, when it next runs, how many posts are queued, and the DRAFTS waiting for approval (id, time, channels, caption, the new image or video). It also names the channels that CANNOT be posted to and why. Read-only, free.',
5697
+ description: 'Show autoposting (the posting refill) for this profile: whether it is on, its mode (review = each batch of fresh posts waits as drafts for approval; auto = fresh posts are scheduled straight away), WHICH CHANNELS it posts to with each one’s posts per day and posting times, roughly what it costs a day, when it next runs, how many posts are queued, and the DRAFTS waiting for approval (id, time, channels, caption, the new image or video). It also names the channels that CANNOT be posted to and why. Read-only, free.',
5698
5698
  inputSchema: {},
5699
5699
  outputSchema: {
5700
5700
  enabled: z.boolean().optional(), mode: z.string().optional(), daysAhead: z.number().optional(),
@@ -5705,14 +5705,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
5705
5705
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
5706
5706
  }, wrap(async () => {
5707
5707
  const d = await apiGet('/api/schedule/refill', {});
5708
- const cadence = d.postsPerDay > 0 ? d.postsPerDay : (d.postingSchedule?.slots || []).length;
5709
5708
  const state = !d.enabled ? 'OFF' : (d.mode === 'auto' ? 'ON, AUTO-PUBLISH — each run makes fresh posts and schedules them' : 'ON, REVIEW — each run makes fresh posts and keeps them as drafts until you approve them');
5710
5709
  const drafts = Array.isArray(d.drafts) ? d.drafts : [];
5711
5710
  const draftLines = drafts.slice(0, 20).map((x, i) => ` ${i + 1}. ${x.id} · ${x.at} → ${(x.channels || []).join(', ')} · ${x.kind}\n ${String(x.message || '').replace(/\n+/g, ' / ').slice(0, 200)}\n ${x.media || '(no file)'}`);
5712
5711
  return ok([
5713
5712
  `Autoposting: ${state}.${d.note ? `\n${d.note}` : ''}`,
5714
- `${cadence} post(s) a day at ${(d.postingSchedule?.slots || []).join(', ') || 'no posting times set'} ${d.postingSchedule?.timezone || 'UTC'}, filling ${d.daysAhead} day(s) ahead. ${d.queuedPosts} post(s) queued now.`,
5715
- `Render budget: ${d.maxImagesPerDay || 0} image(s) + ${d.maxVideosPerDay || 0} video(s) a day, ${d.maxCreditsPerDay || 0} credits a day${d.maxCreditsPerWeek ? ` and ${d.maxCreditsPerWeek} a week` : ''} at most. Every post is a new render made for it.${d.zeroBudget ? ` ${d.budgetNote}` : ''} Used: ${d.spend?.today || 0} credits today, ${d.spend?.week || 0} this week.`,
5713
+ `Posting times: ${(d.postingTimes || []).join(', ') || 'none set (add some on the Schedule tab)'} ${d.postingSchedule?.timezone || 'UTC'}. It fills ${d.daysAhead} day(s) ahead; ${d.queuedPosts} post(s) queued now.`,
5714
+ (d.channelsView || []).length ? `Channels:\n${d.channelsView.map((c) => ` ${c.on ? '[on] ' : '[off]'} ${c.label} (${c.channel}): ${c.on ? `${c.postsPerDay} a day${c.times.length ? ` at ${c.times.join(', ')}` : ''}` : 'not posting'}${c.note ? ` · ${c.note}` : ''}`).join('\n')}` : (d.connectedUnreadable ? 'Channels: could not be read just now.' : 'Channels: none connected — autopilot has nowhere to post. Connect one under Workspace ▸ Connectors.'),
5715
+ `Makes ${d.postsPerDayMade || 0} new post(s) a day, every one a fresh render made for it${d.estCreditsPerDay != null && d.postsPerDayMade ? `, about ${d.estCreditsPerDay} credits a day at today's prices` : ''}.${d.zeroBudget ? ` ${d.budgetNote}` : ''} Used: ${d.spend?.today || 0} credits today, ${d.spend?.week || 0} this week.`,
5716
5716
  `Brief: goal ${d.brief?.goal || 'not set (required to switch it on)'}${d.brief?.goalNote ? ` (${d.brief.goalNote})` : ''} · format ${d.brief?.formats || 'mix'}${(d.brief?.pillars || []).length ? ` · pillars: ${d.brief.pillars.join('; ')}` : (d.suggestedPillars || []).length ? ` · no pillars yet; suggested from the brand: ${d.suggestedPillars.join('; ')}` : ''}${(d.brief?.avoid || []).length ? ` · avoid: ${d.brief.avoid.join('; ')}` : ''}`,
5717
5717
  d.suggestAuto ? `${d.approvedBatches} review batches approved so far: auto-publish (mode "auto") is available if the user wants posts to go out without a review step. Ask; never switch it yourself.` : '',
5718
5718
  drafts.length ? `Drafts waiting for approval (${drafts.length}):\n${draftLines.join('\n')}${drafts.length > 20 ? `\n …and ${drafts.length - 20} more` : ''}\nApprove, edit or discard them with run_post_refill (approve / edit / discard).` : 'No drafts waiting for approval.',
@@ -5721,20 +5721,17 @@ function buildTools(rawServer, opts = {}, sink = null) {
5721
5721
  }));
5722
5722
  server.registerTool('set_post_refill', {
5723
5723
  title: 'Configure autoposting',
5724
- description: 'Turn autoposting (the posting refill) on or off and set how it behaves. PASS ONLY WHAT CHANGES. It always makes NEW posts (a fresh image, carousel or video for each) within the render budget, and never re-posts the Library, so turning it on needs a budget: maxImagesPerDay and/or maxVideosPerDay above 0 plus a maxCreditsPerDay or maxCreditsPerWeek ceiling — with a 0 budget it is refused. SWITCHING IT ON ASKS THE BRIEF ONCE: a goal (followers / sales / launch / other) is required; content pillars (get_post_refill suggests some from the brand), formats (mix / video / carousel / image) and an avoid list are optional. Posts are written from the brief, the brand profile, what has worked on each channel (compared only within a channel), the brand’s skills and playbooks and the ads it saved. Ask the user for anything missing (goal, mode, budget) rather than guessing. `mode` picks what happens to each batch: "review" (the default) keeps the fresh posts as drafts until a human approves them (run_post_refill approve), "auto" schedules them straight away. `enabled:false` is the PAUSE — it removes the recurring job outright, and posts already queued are left alone (cancel those with cancel_scheduled). THE CADENCE IS THE BRAND’S POSTING TIMES, not a number here: three posting times means three posts a day.',
5724
+ description: 'Turn autoposting (the posting refill) on or off and set how it behaves. PASS ONLY WHAT CHANGES. It always makes NEW posts (a fresh image, carousel or video for each) and never re-posts the Library. WHERE: every connected channel by default (channels narrows it); HOW OFTEN: the brand’s posting times, and channelPostsPerDay sets fewer on a channel (e.g. {"x":1}). There are no credit limits to set — get_post_refill shows roughly what it costs a day. Switching it on is refused when no channel is connected or nothing a day would be made. SWITCHING IT ON ASKS THE BRIEF ONCE: a goal (followers / sales / launch / other) is required; content pillars (get_post_refill suggests some from the brand), formats (mix / video / carousel / image) and an avoid list are optional. Posts are written from the brief, the brand profile, what has worked on each channel (compared only within a channel), the brand’s skills and playbooks and the ads it saved. Ask the user for anything missing (goal, mode, channels) rather than guessing. `mode` picks what happens to each batch: "review" (the default) keeps the fresh posts as drafts until a human approves them (run_post_refill approve), "auto" schedules them straight away. `enabled:false` is the PAUSE — it removes the recurring job outright, and posts already queued are left alone (cancel those with cancel_scheduled). Three posting times means up to three posts a day; to post MORE per day, add posting times on the Schedule tab.',
5725
5725
  inputSchema: {
5726
- enabled: z.boolean().optional().describe('on/off. true needs a render budget (refused at 0). false PAUSES it: the recurring job is deleted and nothing new is made. Already-queued posts are untouched.'),
5726
+ enabled: z.boolean().optional().describe('on/off. true needs a connected channel and a goal (refused otherwise). false PAUSES it: the recurring job is deleted and nothing new is made. Already-queued posts are untouched.'),
5727
5727
  mode: z.enum(['review', 'auto']).optional().describe('"review" (default): each batch of fresh posts waits as drafts for approval. "auto": fresh posts are scheduled to publish automatically.'),
5728
5728
  daysAhead: z.number().optional().describe('how far ahead to keep the queue full, 1–30 (default 7)'),
5729
5729
  postsPerDay: z.number().optional().describe('cap the posts per day BELOW the number of posting times. 0 (default) = use every posting time, which is where "3 a day" comes from. To post MORE per day, add posting times instead.'),
5730
- maxImagesPerDay: z.number().optional().describe('how many NEW images a day it may render, one per post. 0 = none.'),
5731
- maxVideosPerDay: z.number().optional().describe('how many NEW short videos a day it may render. 0 = none. Video is the expensive one.'),
5732
- maxCreditsPerDay: z.number().optional().describe('a hard credit ceiling per day, checked BEFORE any render starts. It binds independently of the counts above, and 0 means it makes nothing.'),
5733
- maxCreditsPerWeek: z.number().optional().describe('a credit ceiling per week (0 = none). With it, each day may use what the week has left, within any daily cap.'),
5730
+ channelPostsPerDay: z.record(z.number()).optional().describe('per channel, how many posts a day (the first N of the posting times), e.g. {"x":1,"instagram":3}. 0 turns a channel off; null resets it to every posting time. Merged into what is saved.'),
5734
5731
  goal: z.enum(['followers', 'sales', 'launch', 'other']).optional().describe('THE BRIEF: what the posts are for — grow followers, drive sales, promote a launch, or other (say what in goalNote). Required to switch it on.'),
5735
5732
  goalNote: z.string().optional().describe('THE BRIEF: a line about the goal (e.g. the launch date and what is launching).'),
5736
5733
  pillars: z.array(z.string()).optional().describe('THE BRIEF: content pillars / topics, up to 8 short lines. Every post belongs to one. get_post_refill suggests some from the brand profile.'),
5737
- formats: z.enum(['mix', 'video', 'carousel', 'image']).optional().describe('THE BRIEF: what to make — mix (images with a short video now and then, the default), video (short videos only; needs maxVideosPerDay), carousel (3 new images per post; needs maxImagesPerDay ≥ 3), image.'),
5734
+ formats: z.enum(['mix', 'video', 'carousel', 'image']).optional().describe('THE BRIEF: what to make — mix (images with a short video now and then, the default), video (short videos only), carousel (3 new images per post), image.'),
5738
5735
  avoid: z.array(z.string()).optional().describe('THE BRIEF: never mention or show these — topics, claims, competitors, faces.'),
5739
5736
  channels: z.array(z.enum(['facebook', 'instagram', 'threads', 'tiktok', 'youtube', 'linkedin', 'x', 'pinterest', 'google_business', 'bluesky', 'telegram'])).optional().describe('restrict it to these channels. Omit (or send an empty list) to use every connected channel that can carry each post.'),
5740
5737
  boardId: z.string().optional().describe('PINTEREST — which board Pins go on (list_pinterest_boards). Without one, Pinterest is skipped: a Pin on the wrong board is a public mistake, so it is never guessed.'),
@@ -5749,7 +5746,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5749
5746
  const brief = Object.fromEntries(Object.entries({ goal, goalNote, pillars, formats, avoid }).filter(([, v]) => v !== undefined));
5750
5747
  const d = await apiPut('/api/schedule/refill', { ...rest, ...(Object.keys(brief).length ? { brief } : {}) });
5751
5748
  return ok(d.enabled
5752
- ? `Autoposting is ON in ${d.mode === 'auto' ? 'AUTO-PUBLISH mode — each run makes fresh posts and schedules them to go out on their own.' : 'REVIEW mode — each run makes fresh posts and keeps them as drafts; nothing is scheduled until you approve them with run_post_refill.'} Budget: ${d.maxImagesPerDay || 0} image(s) + ${d.maxVideosPerDay || 0} video(s) a day, ${d.maxCreditsPerDay || 0} credits at most. Run run_post_refill (a free preview by default) to see a plan.`
5749
+ ? `Autoposting is ON in ${d.mode === 'auto' ? 'AUTO-PUBLISH mode — each run makes fresh posts and schedules them to go out on their own.' : 'REVIEW mode — each run makes fresh posts and keeps them as drafts; nothing is scheduled until you approve them with run_post_refill.'} get_post_refill lists the channels it posts to and what it costs a day. Run run_post_refill (a free preview by default) to see a plan.`
5753
5750
  : 'Autoposting is OFF. The recurring job is gone and nothing new will be made. Posts already in the calendar are untouched — cancel those with cancel_scheduled if you want them gone.', d);
5754
5751
  }));
5755
5752
  server.registerTool('run_post_refill', {
@@ -7636,6 +7633,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
7636
7633
  endTime: z.string().optional().describe('REQUIRED with lifetimeBudgetUsd'),
7637
7634
  adsetSchedule: z.array(z.object({ startMinute: z.number(), endMinute: z.number(), days: z.array(z.number()) })).optional().describe('dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday'),
7638
7635
  attributionSpec: z.array(z.any()).optional().describe('e.g. [{event_type:"CLICK_THROUGH",window_days:7}]'),
7636
+ dsaBeneficiary: z.string().optional().describe("EU AD TRANSPARENCY: the person or organisation this ad promotes (Meta shows it on the ad). Meta requires it whenever the ad set reaches the EU; omit it and Hermoso uses the ad account's own default, else this profile's brand name, else the Page name, and the summary says which was used"),
7637
+ dsaPayor: z.string().optional().describe('EU AD TRANSPARENCY: who pays for the ads (Meta shows it on the ad). Omit it and it matches dsaBeneficiary'),
7639
7638
  };
7640
7639
  server.registerTool('create_meta_ad', {
7641
7640
  title: 'Build a full Meta ad (campaign -> ad set -> ad, paused)',
@@ -17182,13 +17181,15 @@ function buildTools(rawServer, opts = {}, sink = null) {
17182
17181
  status: z.enum(['ACTIVE', 'PAUSED', 'ARCHIVED']).optional().describe('ACTIVE starts spend (needs confirm:true); PAUSED / ARCHIVED are safe'),
17183
17182
  dailyBudgetUsd: z.number().optional().describe('new daily budget in USD (1–10000; ad-set or campaign level)'),
17184
17183
  targeting: z.record(z.any()).optional().describe('replacement targeting spec (ad sets) — a Meta targeting object'),
17184
+ dsaBeneficiary: z.string().optional().describe("ad sets: EU ad transparency, who the ad promotes. Meta requires it once targeting reaches the EU; retarget into the EU without it and the ad account default, else the profile's brand name, else the Page name is used"),
17185
+ dsaPayor: z.string().optional().describe('ad sets: EU ad transparency, who pays (defaults to dsaBeneficiary)'),
17185
17186
  confirm: z.boolean().optional().describe('REQUIRED true ONLY to set status ACTIVE (real spend)'),
17186
17187
  },
17187
- outputSchema: { ok: z.boolean().optional(), objectId: z.string().optional(), updated: z.array(z.string()).optional() },
17188
+ outputSchema: { ok: z.boolean().optional(), objectId: z.string().optional(), updated: z.array(z.string()).optional(), euTransparency: z.string().optional().describe('the EU advertiser / payer names set on this edit, and where they came from') },
17188
17189
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
17189
17190
  }, wrap(async (a) => {
17190
17191
  const d = await apiPost('/api/meta/object/update', a);
17191
- return ok(`Updated ${a.objectId} (${(d.updated || []).join(', ')}).`, d);
17192
+ return ok(`Updated ${a.objectId} (${(d.updated || []).join(', ')}).${d.euTransparency ? ` ${d.euTransparency}` : ''}`, d);
17192
17193
  }));
17193
17194
  // ── LINKEDIN LEAD SYNC (2026-09-04): forms + leads read, notification webhooks managed. Standard Tier granted
17194
17195
  // to app 257800418 that day; the consent dialog was probed for the scope BEFORE a line was written. Needs
@@ -18180,11 +18181,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
18180
18181
  text: z.string().optional().describe('the generated text'),
18181
18182
  model: z.string().optional().describe('the writing model label'),
18182
18183
  creditsUsed: z.number().optional().describe('credits billed for this generation'),
18184
+ modelRequested: z.string().optional().describe('present only when the model you named is no longer offered: the id you asked for'),
18185
+ modelNote: z.string().optional().describe('present only when the model you named is no longer offered: says the default ran instead'),
18183
18186
  },
18184
18187
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
18185
18188
  }, wrap(async ({ prompt, model, raw }) => {
18186
18189
  const d = await apiPost('/api/models/llm', { prompt, ...(model ? { model } : {}), ...(raw === true ? { raw: true } : {}) });
18187
- return ok(`${d.text}${d.model ? `\n\n— ${d.model}` : ''}`, d);
18190
+ return ok(`${d.text}${d.model ? `\n\n— ${d.model}` : ''}${d.modelNote ? `\n\n${d.modelNote}` : ''}`, d);
18188
18191
  }));
18189
18192
 
18190
18193
  // ---------- video / avatar / stitch (job-based, polled to completion) ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.377",
3
+ "version": "0.1.379",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Marketing on autopilot, run from your own AI agent. 897 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. 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 and Telegram. 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",