hermoso 0.1.245 → 0.1.247

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/README.md CHANGED
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
5
5
  composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
6
6
  campaigns behind them — all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
7
7
 
8
- **838 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **841 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
9
9
  catalog with exact per-render credit costs plus the full capability map.
10
10
 
11
11
  **What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
171
171
 
172
172
  Then ask your agent: *“Generate an image ad with Hermoso.”*
173
173
 
174
- ### What the 838 tools cover
174
+ ### What the 841 tools cover
175
175
 
176
176
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
177
177
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
@@ -102,7 +102,7 @@ export const TOOL_PROVIDER_RULES = [
102
102
  [/bluesky/, 'bluesky'],
103
103
  [/telegram/, 'telegram'],
104
104
  [/^post_to_tiktok$|^tiktok_|_tiktok_/, 'tiktok'],
105
- [/^post_to_x$|^delete_x_post$|^edit_x_post$|^post_x_article$|^send_x_dm$|^list_x_dms$|^search_x$|^x_(mentions|post|account|trends|user|search_counts|follows)/, 'x'],
105
+ [/^post_to_x$|^delete_x_post$|^edit_x_post$|^post_x_article$|^send_x_dm$|^list_x_dms$|^search_x$|^block_x_user$|^unblock_x_user$|^list_x_blocks$|^x_(mentions|post|account|trends|user|search_counts|follows)/, 'x'],
106
106
  [/pinterest/, 'pinterest'],
107
107
  [/reddit/, 'reddit'],
108
108
  [/snapchat/, 'snapchat'],
package/mcp/tools.mjs CHANGED
@@ -4713,6 +4713,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
4713
4713
  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.'),
4714
4714
  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.'),
4715
4715
  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'),
4716
+ // A MEDIA URL DROPPED WITHOUT AN ERROR IS A TEXT-ONLY POST (2026-09-15, hit dogfooding): the in-app agent's schema says
4717
+ // imageUrl/videoUrl and the server reads all three names, but this schema knew only mediaUrl, so a caller passing
4718
+ // videoUrl published text with no media and no warning. Accept the two aliases here; xPost already prefers video.
4719
+ videoUrl: z.string().optional().describe('alias of mediaUrl for a VIDEO — same as passing it as mediaUrl'),
4720
+ imageUrl: z.string().optional().describe('alias of mediaUrl for an IMAGE — same as passing it as mediaUrl'),
4716
4721
  mediaUrls: z.array(z.string()).optional().describe('UP TO FOUR Hermoso-hosted media attached to ONE post — X\u2019s own schema caps media_ids at 4. X renders them as a GRID: every image visible at once, nothing to swipe to. That is NOT a carousel, and a numbered "1/6 \u00b7 SWIPE" slide deck must still not be sent here — it would publish as a grid and the "swipe" instruction would make no sense. Order decides the layout. On a thread the media rides the FIRST post; give each later part its own post to attach more. Mutually exclusive with mediaUrl, and more than 4 is refused before anything is uploaded.'),
4717
4722
  altText: z.union([z.string(), z.array(z.string())]).optional().describe('accessibility description of the attached media, max 1000 characters \u2014 write one whenever you attach a render. Costs a small extra amount: X bills one metadata write PER media. With SEVERAL media, pass an ARRAY aligned to their order \u2014 X attaches alt text per media id, and a single string describes only the FIRST one (X renders up to four media as a GRID, not a carousel, so one sentence would be wrong for the other three).'),
4718
4723
  poll: z.object({
@@ -5820,6 +5825,36 @@ function buildTools(rawServer, opts = {}, sink = null) {
5820
5825
  const top = (d.users || []).slice(0, 10).map((u) => `\u2022 ${u.username}${u.name ? ` (${u.name})` : ''}${u.followers != null ? ` \u2014 ${Number(u.followers).toLocaleString()} followers` : ''}`).join('\n');
5821
5826
  return ok(`${d.count} of ${d.account}\u2019s ${d.direction}${d.total != null ? ` (of ${Number(d.total).toLocaleString()})` : ''}:\n${top}${d.nextToken ? '\n\nMore available \u2014 pass paginationToken.' : ''}`, d);
5822
5827
  }));
5828
+ server.registerTool('block_x_user', {
5829
+ title: 'Block an account on X',
5830
+ description: 'BLOCK an account on the connected X account: they can no longer see, reply to, follow or message the brand. Reversible with unblock_x_user, but confirm the exact handle with the user first, X shows the block to the person blocked. NEEDS THE BLOCK PERMISSION: an X connection made before 2026-09-15 does not carry it and must be reconnected once under Settings ▸ Connectors ▸ X (same account, one click); the tool says so if that is the case and changes nothing. Costs credits (X bills per API call).',
5831
+ inputSchema: { username: z.string().describe('the handle to block, with or without the @') },
5832
+ outputSchema: { ok: z.boolean().optional(), account: z.string().optional(), target: z.string().optional(), targetId: z.string().optional(), blocking: z.boolean().optional(), costCredits: z.number().optional() },
5833
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
5834
+ }, wrap(async (a) => {
5835
+ const d = await apiPost('/api/x/block', a);
5836
+ return ok(d.blocking ? `Blocked ${d.target} on ${d.account}. Cost ${d.costCredits ?? '?'} credits.` : `X did not confirm the block of ${d.target}.`, d);
5837
+ }));
5838
+ server.registerTool('unblock_x_user', {
5839
+ title: 'Unblock an account on X',
5840
+ description: 'UNBLOCK an account the connected X account has blocked. Same permission note as block_x_user (a pre-2026-09-15 connection reconnects once). Costs credits (X bills per API call).',
5841
+ inputSchema: { username: z.string().describe('the handle to unblock, with or without the @') },
5842
+ outputSchema: { ok: z.boolean().optional(), account: z.string().optional(), target: z.string().optional(), targetId: z.string().optional(), blocking: z.boolean().optional(), costCredits: z.number().optional() },
5843
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
5844
+ }, wrap(async (a) => {
5845
+ const d = await apiPost('/api/x/unblock', a);
5846
+ return ok(d.blocking ? `X still reports ${d.target} as blocked by ${d.account}.` : `Unblocked ${d.target} on ${d.account}. Cost ${d.costCredits ?? '?'} credits.`, d);
5847
+ }));
5848
+ server.registerTool('list_x_blocks', {
5849
+ title: 'Who the connected X account has blocked',
5850
+ description: 'The accounts the connected X account has BLOCKED, with handle, name, bio and follower count: the read-back after block_x_user / unblock_x_user. Same permission note as block_x_user. Costs credits (X bills per API call).',
5851
+ inputSchema: { maxResults: z.number().optional().describe('1–1000, default 100'), paginationToken: z.string().optional() },
5852
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
5853
+ }, wrap(async (a = {}) => {
5854
+ const d = await apiGet('/api/x/blocks', a);
5855
+ const top = (d.users || []).slice(0, 20).map((u) => `• ${u.username}${u.name ? ` (${u.name})` : ''}${u.followers != null ? ` — ${Number(u.followers).toLocaleString()} followers` : ''}`).join('\n');
5856
+ return ok(`${d.account} blocks ${d.count} account(s)${d.nextToken ? ' (more available — pass paginationToken)' : ''}:\n${top || '(none)'}`, d);
5857
+ }));
5823
5858
  server.registerTool('x_user', {
5824
5859
  title: 'Look up any public X account',
5825
5860
  description: 'ACCOUNT-LEVEL numbers for ANY public X account by handle \u2014 followers, following, posts, listed and media counts, plus bio, location, verified status and account age. The research twin of `x_account` (which reads YOUR connected account): use this to size a competitor or vet a creator before working with them. Returns the identical shape as x_account, so the two are never described differently, and a counter X does not return is reported as unknown rather than zero. Needs X connected.',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.245",
3
+ "version": "0.1.247",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI ad studio and marketing MCP server with 838 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. 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.",
5
+ "description": "AI ad studio and marketing MCP server with 841 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. 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",
7
7
  "bin": {
8
8
  "hermoso": "bin/hermoso.mjs"