hermoso 0.1.254 → 0.1.256
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 +49 -56
- package/mcp/tools.mjs +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Run your whole marketing operation from **any AI agent**: Claude Code, Claude.ai, Cursor, Codex, or your own
|
|
4
4
|
scripts. Research the ads already winning in a market, generate finished image & video ads (your real product
|
|
5
5
|
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
|
-
campaigns behind them
|
|
6
|
+
campaigns behind them, all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
7
|
|
|
8
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.
|
|
@@ -81,40 +81,26 @@ Two shapes, and the right one is decided by **what your client can do**, not by
|
|
|
81
81
|
|
|
82
82
|
| Your client | Use | Why |
|
|
83
83
|
| --- | --- | --- |
|
|
84
|
-
| **Runs in a browser
|
|
85
|
-
| **Can run a shell
|
|
84
|
+
| **Runs in a browser**: Claude.ai, ChatGPT, Claude Desktop | the hosted connector `https://app.hermoso.ai/mcp` | It cannot spawn a local process, so a URL is the only shape it has. Nothing to install, no key to paste, and the full toolset arrives with your saved brand context. This is the right answer for these clients, not a lesser one. |
|
|
85
|
+
| **Can run a shell**: Claude Code, Codex, Gemini CLI, Cursor, Cline, OpenClaw, Hermes, your own scripts | the one-command install above, or the CLI itself (`npm install -g hermoso`) | A tool manifest is loaded into every session whether or not a tool is called. A shell command costs nothing until it runs, and it reaches **every** tool rather than the default roster. |
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| | tools in range | loaded per session |
|
|
90
|
-
| --- | --- | --- |
|
|
91
|
-
| Hosted connector, default roster | 306 | **181,713 tokens** |
|
|
92
|
-
| Hosted connector, `?tools=all` | 718 | **472,062 tokens** |
|
|
93
|
-
| stdio server (`npx -y hermoso mcp`) | 306 | **181,713 tokens** |
|
|
94
|
-
| **CLI** | **all 718** | **0** |
|
|
95
|
-
|
|
96
|
-
The CLI answers the same questions on demand instead, and only when asked:
|
|
87
|
+
The CLI answers the questions a tool manifest would, on demand and only when asked:
|
|
97
88
|
|
|
98
89
|
```bash
|
|
99
|
-
npx -y hermoso tools --search reddit # every matching tool, name + one line
|
|
100
|
-
npx -y hermoso tools plan_ad # one tool's full argument schema
|
|
90
|
+
npx -y hermoso tools --search reddit # every matching tool, name + one line
|
|
91
|
+
npx -y hermoso tools plan_ad # one tool's full argument schema
|
|
101
92
|
npx -y hermoso call plan_ad --json '{"product":"…"}' # run it
|
|
102
93
|
```
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
only that needs `hermoso auth login` once.
|
|
108
|
-
|
|
109
|
-
**Both at once is fine, and is what we suggest for Claude Code.** One `hermoso auth login` covers the CLI *and*
|
|
110
|
-
lets `claude mcp add hermoso -- npx -y hermoso mcp` pick the key up with no `env` block, so the agent can reach for
|
|
111
|
-
a native tool when it wants structured results and shell out when it wants breadth. If you only want one, take the
|
|
112
|
-
CLI: it covers strictly more.
|
|
95
|
+
`tools` and `tools <name>` read a registry bundled in the package (no key, no network, no sign-in), so an agent
|
|
96
|
+
can browse the entire product before anyone signs in. Only `call` spends, and only that needs
|
|
97
|
+
`hermoso auth login` once.
|
|
113
98
|
|
|
114
|
-
**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
**Want the tools in your coding agent's own list as well?** That is the MCP server, and it is optional. One
|
|
100
|
+
`hermoso auth login` covers the CLI *and* lets `claude mcp add hermoso -- npx -y hermoso mcp` pick the key up with
|
|
101
|
+
no `env` block, so the agent can reach for a native tool when it wants structured results and shell out when it
|
|
102
|
+
wants breadth. It costs context in every session, so add it when a session settles into one area and makes many
|
|
103
|
+
calls there; `enable_tools({groups:['ads']})` then turns campaign management on in a single free call.
|
|
118
104
|
|
|
119
105
|
## Your agent can sign itself up
|
|
120
106
|
|
|
@@ -170,50 +156,56 @@ the routes.
|
|
|
170
156
|
|
|
171
157
|
Paste **`https://app.hermoso.ai/mcp?src=readme`** into Claude → Settings → Connectors → *Add custom connector*, pick
|
|
172
158
|
**Always required** when Claude asks about authentication (its detector suggests "None" because our discovery
|
|
173
|
-
handshake is open; "None" would leave every tool call unauthenticated), approve with your Hermoso account, done
|
|
159
|
+
handshake is open; "None" would leave every tool call unauthenticated), approve with your Hermoso account, and you are done: the full toolset with your saved brand context, billed to your plan.
|
|
174
160
|
|
|
175
|
-
## Quickstart for Claude Code (one
|
|
161
|
+
## Quickstart for Claude Code (one command)
|
|
176
162
|
|
|
177
|
-
1. **Get an account** at [app.hermoso.ai](https://app.hermoso.ai)
|
|
178
|
-
same ones the web Studio uses. Or skip the browser entirely and let your agent sign itself up on a paid
|
|
179
|
-
with `POST /v1/signup` (above).
|
|
180
|
-
2. **
|
|
163
|
+
1. **Get an account** at [app.hermoso.ai](https://app.hermoso.ai). The free tier is included; plans and credits
|
|
164
|
+
are the same ones the web Studio uses. Or skip the browser entirely and let your agent sign itself up on a paid
|
|
165
|
+
plan with `POST /v1/signup` (above).
|
|
166
|
+
2. **Install the plugin.** It adds the four Hermoso skills, which drive the `hermoso` CLI through `npx`:
|
|
181
167
|
|
|
182
168
|
```bash
|
|
183
|
-
|
|
169
|
+
claude plugin marketplace add hermoso-ai/hermoso && claude plugin install hermoso@hermoso
|
|
184
170
|
```
|
|
185
171
|
|
|
186
|
-
|
|
187
|
-
|
|
172
|
+
Already inside a session? `/plugin marketplace add hermoso-ai/hermoso`, then `/plugin install hermoso@hermoso`.
|
|
173
|
+
Codex, Gemini CLI and the rest are in [Install in one command](#install-in-one-command).
|
|
174
|
+
3. **Sign in once.** `npx -y hermoso auth login` opens your browser; your agent also runs it by itself the first
|
|
175
|
+
time it needs Hermoso. On a machine with no browser, use `npx -y hermoso auth login --token hmk_…` with a key
|
|
176
|
+
from **Settings → Agents & API**.
|
|
177
|
+
4. **Ask for what you want**, in your normal prompts. Claude Code picks the Hermoso skill for the job and runs the
|
|
178
|
+
commands. You type none of them.
|
|
188
179
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
**Settings → Agents & API**, or skip the sign-in and pass the key to the client instead:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
195
|
-
```
|
|
180
|
+
Rather install the CLI by hand? `npm install -g hermoso` puts the same `hermoso` command on your PATH, and the
|
|
181
|
+
skills use it when it is there.
|
|
196
182
|
|
|
197
183
|
The hosted URL works in Claude Code too, but it is the worse path there and it is worth knowing why:
|
|
198
184
|
`claude mcp add --transport http hermoso "https://app.hermoso.ai/mcp?src=readme"` is accepted, and then `claude mcp list`
|
|
199
|
-
reports `! Needs authentication` because the client will not start the OAuth flow by itself
|
|
185
|
+
reports `! Needs authentication` because the client will not start the OAuth flow by itself: you have to open a
|
|
200
186
|
session, run `/mcp`, find the server and press Authenticate. Measured against Claude Code 2.1.241 on 2026-08-23.
|
|
201
187
|
|
|
202
188
|
Your agent now has the full studio **with your workspace's context**: the brand profile, products, logos and
|
|
203
189
|
learned memory you set up in the web app apply automatically (`get_brand` shows what's saved; omit `brand` in
|
|
204
|
-
`plan_ad`/`plan_variations` to use it). Renders bill your Hermoso credits
|
|
190
|
+
`plan_ad`/`plan_variations` to use it). Renders bill your Hermoso credits, at the same prices as the Studio. Only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management and analytics are free on every plan (posting to X and reading X data are the one per-call exception, managing X ads is free).
|
|
205
191
|
|
|
206
|
-
## 1. MCP server (stdio)
|
|
192
|
+
## 1. MCP server (stdio), optional in a coding agent
|
|
207
193
|
|
|
208
|
-
`hermoso mcp` runs a stdio MCP server exposing the full toolset
|
|
209
|
-
`npx -y hermoso mcp` fetches and runs it. Sign in once with
|
|
210
|
-
because `hermoso mcp` reads the bearer `hermoso auth login` stored:
|
|
194
|
+
`hermoso mcp` runs a stdio MCP server exposing the full toolset, for a client that wants Hermoso's tools in its own
|
|
195
|
+
list. The published `hermoso` package means no clone: `npx -y hermoso mcp` fetches and runs it. Sign in once with
|
|
196
|
+
the CLI and no key goes into any client config, because `hermoso mcp` reads the bearer `hermoso auth login` stored:
|
|
211
197
|
|
|
212
198
|
```bash
|
|
213
199
|
npm install -g hermoso && hermoso auth login && claude mcp add hermoso -- npx -y hermoso mcp
|
|
214
200
|
```
|
|
215
201
|
|
|
216
|
-
|
|
202
|
+
On a machine with no browser, skip the sign-in and pass the key to the client instead:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Cursor / Codex: sign in the same way, then add to `mcp.json` (Codex uses the TOML equivalent). Drop the `env`
|
|
217
209
|
block entirely if you signed in above; it is there for CI, where the process cannot read your home directory:
|
|
218
210
|
|
|
219
211
|
```json
|
|
@@ -323,7 +315,7 @@ consent screen, so the user does it in the app).
|
|
|
323
315
|
|
|
324
316
|
Render jobs queue server-side and poll to completion, returning a served URL.
|
|
325
317
|
|
|
326
|
-
## 2. CLI
|
|
318
|
+
## 2. CLI: the context-free path for terminal agents
|
|
327
319
|
|
|
328
320
|
`bin/hermoso.mjs` exposes the full MCP toolset as subprocess commands, so an agent can shell out instead of carrying a
|
|
329
321
|
fat tool manifest.
|
|
@@ -352,21 +344,22 @@ hermoso create_meta_campaign --name "…" # same thing, shorte
|
|
|
352
344
|
```
|
|
353
345
|
|
|
354
346
|
`call` goes through the same handler, the same argument validation and the same confirm/spend gates the MCP
|
|
355
|
-
server uses
|
|
347
|
+
server uses, so there is no second implementation to drift. `tools` and `tools <name>` read a registry bundled in
|
|
356
348
|
the package, so they need no key, no network and no sign-in.
|
|
357
349
|
|
|
358
|
-
## 3.
|
|
350
|
+
## 3. Skills: what a coding agent installs
|
|
359
351
|
|
|
360
352
|
`skills/` holds four installable skills: `hermoso-generate`, `hermoso-ad-from-brand`,
|
|
361
353
|
`hermoso-product-photoshoot`, `hermoso-research`.
|
|
362
354
|
|
|
363
|
-
|
|
355
|
+
They are what every one-command install at the top of this page adds, in Claude Code, Codex, Gemini CLI and
|
|
356
|
+
through `npx skills add`. From a clone, copying works too:
|
|
364
357
|
|
|
365
358
|
```bash
|
|
366
359
|
cp -r skills/* ~/.claude/skills/
|
|
367
360
|
```
|
|
368
361
|
|
|
369
|
-
Then invoke `/hermoso-ad-from-brand an ad for yourbrand.com
|
|
362
|
+
Then invoke `/hermoso-ad-from-brand an ad for yourbrand.com, our hero product`.
|
|
370
363
|
|
|
371
364
|
## Configuration
|
|
372
365
|
|
package/mcp/tools.mjs
CHANGED
|
@@ -3112,6 +3112,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3112
3112
|
captions: z.array(z.record(z.any())).optional().describe("Up to 20 WebVTT caption tracks: [{lang:'en', url:'https://…/en.vtt'}] or [{lang:'en', content:'WEBVTT\\n\\n00:00…'}]. Each file is capped at 20000 bytes."),
|
|
3113
3113
|
langs: z.array(z.string()).optional().describe("BCP-47 language tags, e.g. ['en']."),
|
|
3114
3114
|
linkCard: z.union([z.boolean(), z.record(z.any())]).optional().describe('Rich link card (`app.bsky.embed.external`). OMIT for the default (a card is built automatically when the post has a URL and no media). `false` never builds one. `true` builds one from the first URL in the text. An object {uri,title,description,thumbUrl} overrides any field — supply BOTH title and description and the page is never fetched. Cannot be combined with imageUrls/videoUrl: a post has ONE embed, so an explicit linkCard beside media is refused.'),
|
|
3115
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Bluesky has no cover setting and shows the video\u2019s FIRST frame, so by default Hermoso checks it and, only when that frame is blank (a template ad\u2019s empty opening card), sends Bluesky a copy with the first frame replaced by the video\u2019s best frame. Your Library file is never changed. true = send the file exactly as it is.'),
|
|
3115
3116
|
},
|
|
3116
3117
|
outputSchema: { url: z.string().optional(), uri: z.string().optional(), handle: z.string().optional(), note: z.string() },
|
|
3117
3118
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -3168,6 +3169,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3168
3169
|
videoUrl: z.string().optional().describe('one video (≤50MB). Passed alongside imageUrls it joins the album as one more item.'),
|
|
3169
3170
|
disablePreview: z.boolean().optional().describe('suppress the link-preview card on a text-only post. Default is Telegram’s own behaviour (previews on).'),
|
|
3170
3171
|
silent: z.boolean().optional().describe('deliver without a notification sound (Telegram’s disable_notification). This is NOT a visibility setting — the message is just as visible.'),
|
|
3172
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (Telegram\u2019s in-chat video cover). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
3171
3173
|
},
|
|
3172
3174
|
outputSchema: { ok: z.boolean().optional(), chatId: z.string().optional(), chatTitle: z.string().optional(), messageId: z.number().optional(), url: z.string().nullable().optional(), album: z.boolean().optional(), slides: z.number().optional(), video: z.boolean().optional(), note: z.string().optional() },
|
|
3173
3175
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -4595,6 +4597,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4595
4597
|
// Instagram" as a fact.
|
|
4596
4598
|
crossreshareToIg: z.boolean().optional().describe('THREADS ONLY — ALSO share this Threads post to the linked Instagram account AS A STORY (not a feed post), in the same publish. NOT available on a Threads CAROUSEL, which is refused by name rather than silently dropped. THERE IS NO CONFIRMATION: Threads returns no field saying whether the Story was created, so report it as REQUESTED and tell the user to check their Instagram Stories — never that it is live.'),
|
|
4597
4599
|
crossreshareDarkMode: z.boolean().optional().describe('THREADS ONLY — render that Instagram Story in dark mode. Only meaningful alongside crossreshareToIg; on its own it is refused rather than silently ignored, because a parameter that never reaches the wire must not look accepted.'),
|
|
4600
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (Instagram Reel: thumb_offset; Facebook video/Reel: an uploaded cover image) \u2014 and on Threads, which has no cover setting, a blank first frame is replaced on a copy sent to Threads only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
4598
4601
|
},
|
|
4599
4602
|
outputSchema: { ok: z.boolean().optional(), postId: z.string().optional(), url: z.string().optional(), target: z.string().optional(), page: z.string().optional(), account: z.string().optional() },
|
|
4600
4603
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -4675,7 +4678,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4675
4678
|
disableComment: z.boolean().optional().describe('TIKTOK — turn comments off on this post.'),
|
|
4676
4679
|
disableDuet: z.boolean().optional().describe('TIKTOK VIDEO ONLY — block Duets. TikTok’s photo-post API has no Duets, so this is refused on a photo/slideshow item rather than silently dropped.'),
|
|
4677
4680
|
disableStitch: z.boolean().optional().describe('TIKTOK VIDEO ONLY — block Stitches. Same photo-post rule as disableDuet.'),
|
|
4678
|
-
coverTimestampMs: z.number().optional().describe('TIKTOK VIDEO ONLY — which frame TikTok uses as the cover, in milliseconds from the start. Omit and TikTok uses the first frame.'),
|
|
4681
|
+
coverTimestampMs: z.number().optional().describe('TIKTOK VIDEO ONLY — which frame TikTok uses as the cover, in milliseconds from the start. Omit and Hermoso uses the video\u2019s best frame (platformCover:true leaves it to TikTok, which uses the first frame).'),
|
|
4679
4682
|
topicType: z.enum(['STANDARD', 'EVENT', 'OFFER', 'ALERT']).optional().describe('GOOGLE BUSINESS — the KIND of Post. STANDARD is the default; EVENT and OFFER both REQUIRE `event` (title + start date), and OFFER also takes `offer`.'),
|
|
4680
4683
|
actionType: z.enum(['BOOK', 'ORDER', 'SHOP', 'LEARN_MORE', 'SIGN_UP', 'CALL']).optional().describe('GOOGLE BUSINESS — the call-to-action button. Every button except CALL needs `link` (CALL dials the number on the listing and takes none). Google IGNORES the button link on an OFFER post — put the destination in offer.redeemOnlineUrl. Omit and a post carrying a link gets LEARN_MORE.'),
|
|
4681
4684
|
event: z.object({ title: z.string().optional(), startDate: z.string().optional(), startTime: z.string().optional(), endDate: z.string().optional(), endTime: z.string().optional() }).optional().describe('GOOGLE BUSINESS — required for an EVENT or OFFER post: {title, startDate:"YYYY-MM-DD", endDate, startTime:"HH:MM", endTime}. `title` is the EVENT’s headline, a different thing from the post `title` (which is the Pinterest/YouTube one). Google documents its TimeInterval as needing all four date/time parts to be valid, so send the times whenever you know them.'),
|
|
@@ -4729,6 +4732,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4729
4732
|
visibility: z.enum(['public', 'unlisted', 'private', 'draft']).optional().describe("how it should be published — DEFAULT 'public' (live). Only pass something else if the user explicitly asked to stage/hide it. Not every channel supports every value; an impossible combination is refused when you schedule it, with the reason."),
|
|
4730
4733
|
visibilityByChannel: z.record(z.string()).optional().describe('override visibility for one channel, e.g. { "tiktok": "draft" } to go live everywhere but stage TikTok for review'),
|
|
4731
4734
|
optimizeCopy: z.boolean().optional().describe('RECOMMENDED when one caption goes to several channels: fit the shared caption to each channel’s own rules when you schedule it (the fitted caption is stored on the scheduled post, so what you scheduled is what publishes) wherever no per-channel caption was written. The angle and every claim stay the author’s; a channel with its own caption is left exactly as written. Off by default so nobody’s words are rewritten unasked.'),
|
|
4735
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover on every channel that allows one (Instagram, Facebook, TikTok direct posts, LinkedIn Pages, Pinterest, Telegram, YouTube) \u2014 and on X, Threads and Bluesky, which have none, a blank first frame is replaced on a copy sent there only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
4732
4736
|
},
|
|
4733
4737
|
outputSchema: { id: z.string().optional(), at: z.string().optional(), channels: z.array(z.string()).optional(), label: z.string().optional() },
|
|
4734
4738
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -4887,6 +4891,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4887
4891
|
locationId: z.string().optional().describe('GOOGLE BUSINESS — a different listing (list_business_locations)'),
|
|
4888
4892
|
visibility: z.enum(['public', 'unlisted', 'private', 'draft']).optional().describe('NOTE: changing this without also naming visibilityByChannel clears any per-channel overrides, so "make it all draft" is not a no-op'),
|
|
4889
4893
|
visibilityByChannel: z.record(z.string()).optional(),
|
|
4894
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover on every channel that allows one \u2014 and on X, Threads and Bluesky, which have none, a blank first frame is replaced on a copy sent there only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
4890
4895
|
},
|
|
4891
4896
|
outputSchema: { id: z.string().optional(), at: z.string().optional(), channels: z.array(z.string()).optional(), visibility: z.string().optional() },
|
|
4892
4897
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
@@ -5084,6 +5089,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5084
5089
|
quotePostId: z.string().optional().describe('numeric id of a post to QUOTE \u2014 X renders the quoted post inside yours. This is NOT replyToId: a reply sits under the original in its thread, a quote stands alone on your own timeline with the original embedded, which is the one you want for commentary. X makes a quote mutually exclusive with media and with a poll (their own schema), and a quote is billed at the higher LINK rate because X appends the quoted post\u2019s t.co URL whatever your text says. Same X rule as replyToId: a quote of a post that does not mention this account is refused by X.'),
|
|
5085
5090
|
communityId: z.string().optional().describe('publish into an X COMMUNITY instead of the main timeline \u2014 the number in the community\u2019s own URL (x.com/i/communities/<id>). The connected account must be a MEMBER of it; X answers a non-member and a non-existent id with the same refusal and does not separate them.'),
|
|
5086
5091
|
paidPartnership: z.boolean().optional().describe('label the post a PAID PARTNERSHIP on X \u2014 the same disclosure Hermoso already ships for TikTok. OPT-IN ONLY: set it when the post is sponsored, gifted or otherwise paid for, and never assume it on the user\u2019s behalf.'),
|
|
5092
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. X has no cover setting and shows the video\u2019s FIRST frame, so by default Hermoso checks it and, only when that frame is blank (a template ad\u2019s empty opening card), sends X a copy with the first frame replaced by the video\u2019s best frame. Your Library file is never changed. true = send the file exactly as it is.'),
|
|
5087
5093
|
},
|
|
5088
5094
|
outputSchema: { ok: z.boolean().optional(), id: z.string().optional(), url: z.string().optional(), thread: z.boolean().optional(), media: z.boolean().optional(), altText: z.boolean().optional(), poll: z.boolean().optional(), costCredits: z.number().optional(), posts: z.array(z.object({ id: z.string().optional(), text: z.string().optional(), url: z.string().optional() })).optional(), quotedPostId: z.string().optional(), communityId: z.string().optional(), paidPartnership: z.boolean().optional() },
|
|
5089
5095
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -5371,6 +5377,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5371
5377
|
slideText: z.array(z.object({ title: z.string().optional(), description: z.string().optional(), link: z.string().optional() })).optional().describe('PINTEREST CAROUSEL ONLY \u2014 per-slide title, description and destination LINK, one object per slide in slide order. This is the ONLY genuine per-slide caption on any channel Hermoso publishes to: slide 4 can send people to the product ON slide 4, where every other platform gives a carousel one shared caption. Omit any field to leave it unset; the Pin\u2019s own title/description/link still describe the Pin as a whole. Pinterest publishes no length limit on these, so nothing is truncated. More entries than slides is refused rather than dropped.'),
|
|
5372
5378
|
coverImageUrl: z.string().optional().describe('video Pins only — a render to use as the cover frame'),
|
|
5373
5379
|
boardSectionId: z.string().optional().describe('optional section within the board'),
|
|
5380
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (Pinterest\u2019s cover key frame, to the whole second). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
5374
5381
|
},
|
|
5375
5382
|
outputSchema: { ok: z.boolean().optional(), id: z.string().nullable().optional(), url: z.string().nullable().optional(), boardId: z.string().optional(), title: z.string().optional(), kind: z.string().optional() },
|
|
5376
5383
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -5756,6 +5763,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5756
5763
|
publishAt: z.string().optional().describe('SCHEDULE the publish — ISO 8601, e.g. "2026-09-01T15:00:00Z", and it must be in the future. YouTube only allows this on a PRIVATE video and makes it PUBLIC at that moment, so pass privacy:"private" (or leave privacy unset) — asking for a scheduled "unlisted" or "public" post is refused rather than half-honoured.'),
|
|
5757
5764
|
notifySubscribers: z.boolean().optional().describe('THE DEFAULT FOLLOWS PRIVACY. privacy:"public" NOTIFIES the channel\'s subscribers — that is YouTube\'s own default and normally what someone publishing publicly wants. privacy:"unlisted" and "private" do NOT: the video is not on the channel, so announcing it is nonsense, and a blast to somebody\'s whole subscriber list cannot be undone. A scheduled publish (publishAt) is PRIVATE at upload, so it does not notify either — pass true to announce one. An explicit value ALWAYS wins in both directions: true announces an unlisted/private upload, false publishes publicly and quietly. The reply reports which way it went and why.'),
|
|
5758
5765
|
aiGenerated: z.boolean().optional().describe('YouTube\u2019s \u201caltered or synthetic content\u201d declaration (containsSyntheticMedia). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video the user uploaded through upload_file or from an external URL is NOT \u2014 real footage must not carry the label. true/false overrides.'),
|
|
5766
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (YouTube custom thumbnail; the same as thumbnailUrl:"auto" when true). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
5759
5767
|
},
|
|
5760
5768
|
outputSchema: { ok: z.boolean().optional(), videoId: z.string().optional(), url: z.string().optional(), privacy: z.string().optional(), requestedPrivacy: z.string().optional(), categoryId: z.string().optional(), categoryName: z.string().optional(), publishAt: z.string().optional(), scheduled: z.boolean().optional(), notifySubscribers: z.boolean().optional(), notifyNote: z.string().optional(), warning: z.string().optional(), scheduleWarning: z.string().optional(), thumbnailSet: z.boolean().optional(), thumbnailSource: z.string().nullable().optional(), thumbnailReadBack: z.string().nullable().optional(), thumbnailNote: z.string().optional(), title: z.string().optional() },
|
|
5761
5769
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -6528,6 +6536,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6528
6536
|
aiGenerated: z.boolean().optional().describe('TikTok\u2019s is_aigc AI-generated-content label (video posts). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video that came through upload_file or an external URL (the user\u2019s own footage) is NOT. true/false overrides.'),
|
|
6529
6537
|
brandedContent: z.boolean().optional().describe('discloses a paid partnership — cannot be combined with SELF_ONLY privacy'),
|
|
6530
6538
|
yourBrand: z.boolean().optional().describe('discloses that this promotes the creator’s own brand'),
|
|
6539
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (TikTok video_cover_timestamp_ms, on a direct post \u2014 a draft takes no cover, you pick it in the TikTok app). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
6531
6540
|
},
|
|
6532
6541
|
outputSchema: { ok: z.boolean().optional(), publishId: z.string().optional(), status: z.string().optional(), destination: z.string().optional(), media: z.string().optional(), images: z.number().optional(), coverIndex: z.number().optional(), postId: z.string().nullable().optional(), url: z.string().nullable().optional(), account: z.string().nullable().optional(), pending: z.boolean().optional() },
|
|
6533
6542
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -15457,6 +15466,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
15457
15466
|
videoThumbnailUrl: z.string().optional().describe('the COVER IMAGE for a videoUrl post — a Hermoso-hosted image (a render, or any picture of the user\u2019s via upload_file). Without it LinkedIn adds a system-generated thumbnail, which on an ad is usually whatever the first frame happens to be. Like captions this can only be set WHILE the video is uploaded, never afterwards. Requires videoUrl. This is NOT linkThumbnailUrl, which is the picture on a link-preview card.'),
|
|
15458
15467
|
visibility: z.enum(['PUBLIC', 'CONNECTIONS']).optional().describe('default PUBLIC'),
|
|
15459
15468
|
targetAudience: z.object({ geoLocations: z.array(z.string()).optional(), industries: z.array(z.string()).optional(), seniorities: z.array(z.string()).optional(), jobFunctions: z.array(z.string()).optional(), staffCountRanges: z.array(z.enum(['SIZE_1', 'SIZE_2_TO_10', 'SIZE_11_TO_50', 'SIZE_51_TO_200', 'SIZE_201_TO_500', 'SIZE_501_TO_1000', 'SIZE_1001_TO_5000', 'SIZE_5001_TO_10000', 'SIZE_10001_OR_MORE'])).optional(), degrees: z.array(z.string()).optional(), fieldsOfStudy: z.array(z.string()).optional(), organizations: z.array(z.string()).optional() }).optional().describe('LINKEDIN COMPANY PAGE POST ONLY — show the post only to Page followers matching these facets (URNs or bare numeric ids; search_linkedin_ads_targeting finds them). LinkedIn requires the matching audience to be over 300 followers and refuses a smaller one. Personal-profile posts cannot be targeted.'),
|
|
15469
|
+
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (LinkedIn\u2019s video thumbnail upload). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
15460
15470
|
},
|
|
15461
15471
|
outputSchema: { ok: z.boolean().optional(), id: z.string().optional(), url: z.string().optional(), organizationId: z.string().optional(), videoExtras: z.object({ captions: z.boolean().optional(), thumbnail: z.boolean().optional() }).optional(), note: z.string().optional() },
|
|
15462
15472
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.256",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 841 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 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",
|