hermoso 0.1.468 → 0.1.470

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 +5 -5
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -6609,13 +6609,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
6609
6609
  // below say so explicitly, because an agent that fires ten posts to "see what sticks" is spending the user's money.
6610
6610
  server.registerTool('post_to_x', {
6611
6611
  title: 'Publish a post to X (Twitter)',
6612
- description: 'Use this when the user wants to post to X (Twitter) now. Publish to the user’s connected X (Twitter) account — a single post, a post with an image or video render attached, a reply to an existing post, or a whole THREAD (pass `thread` as an array and each part is posted as a reply to the one before). This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. Can also run a POLL (2-4 options) instead of media, and restrict who may reply. LENGTH IS THE POSTING ACCOUNT’S, NOT A FLAT 280: 280 characters on an ordinary account, but UP TO 25,000 if that account has X Premium. Hermoso reads the account’s own subscription from X and sends the post rather than refusing something it may be entitled to; if X declines it on length, the reply says so and names the subscription X reported. Over 25,000 is refused for free — that is X’s own ceiling on every account. Text is NEVER truncated. AND A LONG POST IS CHEAPER THAN A THREAD: it is ONE billed X call where the same words split across five posts are five, so prefer one long post over threading when the account has Premium. Only the first ~280 characters show in the timeline; the rest sits behind \u201cShow more\u201d. Write altText whenever you attach a render. COSTS CREDITS at X’s per-post price, every post in a thread billed: a post that contains a link (any web address, and every quote post) is X’s link rate, far higher than one without (hermoso_credits gives both figures). Before you ask the user’s yes, call this once with dryRun:true: it posts NOTHING and returns this exact post’s costCredits and their live balance. Say that number when you show the text, and when the balance cannot cover it, say both numbers instead of posting. Each profile also has a rolling 24-hour ceiling on what it can spend at X, and a request that would cross it is refused WHOLE before anything publishes, so a batch of posts is bounded rather than open-ended. THIS TOOL POSTS ORGANICALLY — it does not create an ad campaign. X ads are built with create_x_ads_campaign -> create_x_ads_line_item -> create_x_ads_promoted_tweet, and a promoted post needs a post id, so publish here first and promote that post. Needs X connected (Settings > Connectors > X).',
6612
+ description: 'Use this when the user wants to post to X (Twitter) now. Publish to the user’s connected X (Twitter) account — a single post, a post with an image or video render attached, a reply to an existing post, or a whole THREAD (pass `thread` as an array and each part is posted as a reply to the one before). This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. Can also run a POLL (2-4 options) instead of media, and restrict who may reply. LENGTH IS THE POSTING ACCOUNT’S, NOT A FLAT 280: 280 characters on an ordinary account, but UP TO 25,000 if that account has X Premium. Hermoso reads the account’s own subscription from X and sends the post rather than refusing something it may be entitled to; if X declines it on length, the reply says so and names the subscription X reported. Over 25,000 is refused for free — that is X’s own ceiling on every account. Text is NEVER truncated. AND A LONG POST IS CHEAPER THAN A THREAD: it is ONE billed X call where the same words split across five posts are five, so prefer one long post over threading when the account has Premium. Only the first ~280 characters show in the timeline; the rest sits behind \u201cShow more\u201d. Write altText whenever you attach a render. COSTS CREDITS at X’s per-post price, every post in a thread billed: a post that contains a link (any web address, and every quote post) is X’s link rate, far higher than one without (hermoso_credits gives both figures). When the user asks what a post will cost, call this with dryRun:true: it posts NOTHING and returns that exact post’s costCredits. Each profile also has a rolling 24-hour ceiling on what it can spend at X, and a request that would cross it is refused WHOLE before anything publishes, so a batch of posts is bounded rather than open-ended. THIS TOOL POSTS ORGANICALLY — it does not create an ad campaign. X ads are built with create_x_ads_campaign -> create_x_ads_line_item -> create_x_ads_promoted_tweet, and a promoted post needs a post id, so publish here first and promote that post. Needs X connected (Settings > Connectors > X).',
6613
6613
  inputSchema: {
6614
6614
  account: z.string().optional().describe("WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one x account connected (several and none named is refused by name, never guessed); omit when there is one."),
6615
6615
  ...HOOK_ATTR,
6616
- text: z.string().optional().describe('the post text. 280 characters without X Premium, up to 25,000 with it \u2014 write the full thing, it is never truncated. Use this OR thread, not both.'),
6617
- thread: z.array(z.string()).optional().describe('a thread: each string is one post, published in order, each replying to the previous. Max 25. Each part follows the same length rule as `text`, and on an X Premium account ONE long post is usually both better reading and cheaper than a thread.'),
6618
- dryRun: z.boolean().optional().describe('true = post NOTHING and return what this exact post would cost (costCredits) and the live balance. Free.'),
6616
+ text: z.string().optional().describe('the post text. 280 characters without X Premium, up to 25,000 with it \u2014 write the full thing, it is never truncated. With `thread` also set, this is the FIRST post and the thread entries follow it (a thread whose first entry is already this text is not posted twice).'),
6617
+ thread: z.array(z.string()).optional().describe('a thread: each string is one post, published in order, each replying to the previous (if `text` is also set it is posted first and these follow). Max 25 including `text`. Each part follows the same length rule as `text`, and on an X Premium account ONE long post is usually both better reading and cheaper than a thread.'),
6618
+ dryRun: z.boolean().optional().describe('true = post NOTHING and return what this exact post would cost (costCredits). Free.'),
6619
6619
  mediaUrl: z.string().optional().describe('a Hermoso render (image or video) to attach to the first post — pass its served URL, or an upload_file url for external media'),
6620
6620
  // A MEDIA URL DROPPED WITHOUT AN ERROR IS A TEXT-ONLY POST (2026-09-15, hit dogfooding): the in-app agent's schema says
6621
6621
  // imageUrl/videoUrl and the server reads all three names, but this schema knew only mediaUrl, so a caller passing
@@ -6645,7 +6645,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
6645
6645
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
6646
6646
  }, wrap(async (a) => {
6647
6647
  const d = await apiPost('/api/x/post', a);
6648
- if (d && d.dryRun === true) return ok(`Not posted (price check). This ${Array.isArray(a.thread) && a.thread.length > 1 ? `${a.thread.length}-post thread` : 'post'} costs ${d.costCredits} credits at X's per-post price${d.balance != null ? `; the balance is ${d.balance}, so it ${d.covers ? 'fits' : `does NOT fit (${d.costCredits - d.balance} short)`}` : ''}. ${d.note || ''}`.trim(), d);
6648
+ if (d && d.dryRun === true) return ok(`Not posted (price check). This ${Array.isArray(a.thread) && a.thread.length > 1 ? `${a.thread.length}-post thread` : 'post'} costs ${d.costCredits} credits at X's per-post price. ${d.note || ''}`.trim(), d);
6649
6649
  const extra = d.media ? (d.altText ? ' with the render + alt text' : ' with the render attached (no alt text was written)') : d.poll ? ' with a poll' : '';
6650
6650
  return ok(`Published to X${d.thread ? ` — a ${(d.posts || []).length}-post thread` : ''}${extra}: ${d.url}. Cost ${d.costCredits ?? '?'} credits.`, d);
6651
6651
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.468",
3
+ "version": "0.1.470",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI marketing agent and AI ad generator, run from your own AI agent. 1088 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",