hermoso 0.1.4 → 0.1.5
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 +18 -14
- package/bin/hermoso.mjs +8 -1
- package/mcp/tools.mjs +27 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,18 +8,17 @@ image & video ads (your real product composited in, copy + CTA included) — all
|
|
|
8
8
|
## Instant: the hosted Claude.ai connector
|
|
9
9
|
|
|
10
10
|
Paste **`https://app.hermoso.ai/mcp`** into Claude → Settings → Connectors → *Add custom connector*, approve with
|
|
11
|
-
your Hermoso account, done —
|
|
11
|
+
your Hermoso account, done — the full studio toolset with your saved brand context, billed to your plan.
|
|
12
12
|
|
|
13
13
|
## Quickstart for Claude Code / Cursor / scripts (2 minutes)
|
|
14
14
|
|
|
15
15
|
1. **Get an account** at [app.hermoso.ai](https://app.hermoso.ai) — free tier included; plans & credits are the
|
|
16
16
|
same ones the web Studio uses.
|
|
17
17
|
2. **Create an agent key**: app.hermoso.ai → **Settings → Agents & API** → Create API key (`hmk_…`).
|
|
18
|
-
3. **Connect** (Claude Code shown; any MCP client works):
|
|
18
|
+
3. **Connect** — no clone needed, `npx` runs the published `hermoso` package (Claude Code shown; any MCP client works):
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
22
|
-
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- node "$(pwd)/mcp/hermoso-mcp.mjs"
|
|
21
|
+
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
23
22
|
```
|
|
24
23
|
|
|
25
24
|
Your agent now has the full studio **with your workspace's context**: the brand profile, products, logos and
|
|
@@ -28,28 +27,33 @@ learned memory you set up in the web app apply automatically (`get_brand` shows
|
|
|
28
27
|
|
|
29
28
|
## 1. MCP server (stdio) — Claude Code / Cursor / Codex
|
|
30
29
|
|
|
31
|
-
`
|
|
30
|
+
`hermoso mcp` runs a stdio MCP server exposing the full studio toolset (40+ tools). The published `hermoso`
|
|
31
|
+
package means no clone — `npx -y hermoso mcp` fetches and runs it:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
35
|
-
claude mcp add hermoso -- node "$(pwd)/mcp/hermoso-mcp.mjs"
|
|
34
|
+
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
Cursor / Codex — add to `mcp.json` (Codex uses the TOML equivalent):
|
|
39
38
|
|
|
40
39
|
```json
|
|
41
|
-
{ "mcpServers": { "hermoso": { "command": "
|
|
40
|
+
{ "mcpServers": { "hermoso": { "command": "npx", "args": ["-y", "hermoso", "mcp"],
|
|
42
41
|
"env": { "HERMOSO_API_BASE": "https://app.hermoso.ai", "HERMOSO_TOKEN": "<your token>" } } } }
|
|
43
42
|
```
|
|
44
43
|
|
|
45
44
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
46
45
|
|
|
47
|
-
**Tools (
|
|
48
|
-
`
|
|
49
|
-
`
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
**Tools (40+):** research/ad-spy (`find_competitors`, `pull_competitor_ads`, `research_ads`, `search_meta_ads`,
|
|
47
|
+
`search_google_ads`, `search_linkedin_ads`, `search_tiktok`, `search_instagram`, `search_youtube`, `search_reddit`,
|
|
48
|
+
`search_threads`, `scrapecreators_fetch`), plan → generate → finish (`plan_ad`, `plan_variations`, `generate_image`,
|
|
49
|
+
`generate_video`, `generate_avatar`, `render_ad`, `make_template_ad`, `stitch_video`, `reframe_video`,
|
|
50
|
+
`upscale_video`, `dub_video`, `change_voice`, `recast_motion`, `remix_static`, `finish_video`, `fix_beat`),
|
|
51
|
+
brand + account (`get_brand`, `list_brands`, `use_brand`, `draft_brand`, `list_product_photos`, `set_product_image`,
|
|
52
|
+
`hermoso_capabilities`, `hermoso_credits`, `buy_credits`), and analysis/jobs (`analyze_video`, `score_ad`,
|
|
53
|
+
`check_ad_policy`, `competitor_teardown`, `mine_angles`, `get_job`, `list_jobs`, `get_skill`, `list_skills`,
|
|
54
|
+
`fetch_asset`). Call `hermoso_capabilities` first — it returns valid model ids and per-render credit costs;
|
|
55
|
+
`tools/list` is the authoritative current set. Render jobs queue server-side and poll to completion, returning a
|
|
56
|
+
served URL.
|
|
53
57
|
|
|
54
58
|
## 2. CLI — the token-cheap path for terminal agents
|
|
55
59
|
|
package/bin/hermoso.mjs
CHANGED
|
@@ -70,6 +70,12 @@ async function main() {
|
|
|
70
70
|
process.env.HERMOSO_API_BASE = process.env.HERMOSO_API_BASE || cfg.apiBase || 'https://app.hermoso.ai';
|
|
71
71
|
if (cfg.token && !process.env.HERMOSO_TOKEN) process.env.HERMOSO_TOKEN = cfg.token;
|
|
72
72
|
if (cfg.profile && !process.env.HERMOSO_PROFILE) process.env.HERMOSO_PROFILE = cfg.profile;
|
|
73
|
+
|
|
74
|
+
// `hermoso mcp` → run the stdio MCP server (Claude Code / Cursor / Codex spawn this, e.g. `npx -y hermoso mcp`).
|
|
75
|
+
// It OWNS stdout as the JSON-RPC channel, so hand off immediately and print nothing to stdout here. The
|
|
76
|
+
// config-resolved API base + token (set just above) ride into the server's client via env; all logs go to stderr.
|
|
77
|
+
if (group === 'mcp') { await import('../mcp/hermoso-mcp.mjs'); return; }
|
|
78
|
+
|
|
73
79
|
const api = await import('../mcp/client.mjs');
|
|
74
80
|
const out = (label, data) => { if (flags.json) console.log(JSON.stringify(data, null, 2)); else console.log(label); };
|
|
75
81
|
const absUrl = (u) => (u && u.startsWith('/') ? api.API_BASE + u : u);
|
|
@@ -149,7 +155,8 @@ async function main() {
|
|
|
149
155
|
generate image --prompt [--ref] [--model] [--aspect] generate video|avatar|stitch … [--wait]
|
|
150
156
|
jobs list | jobs get <id> [--wait] competitors <domain>
|
|
151
157
|
ads pull (--company|--domain) research "<request>"
|
|
152
|
-
fetch <url> [--out]
|
|
158
|
+
fetch <url> [--out] mcp (run the stdio MCP server)
|
|
159
|
+
version
|
|
153
160
|
add --json to any command for machine output.`);
|
|
154
161
|
}
|
|
155
162
|
} catch (e) { die(e?.message || String(e)); }
|
package/mcp/tools.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const wrap = (fn) => async (args, extra) => {
|
|
|
41
41
|
catch (e) {
|
|
42
42
|
let msg = `Error: ${e?.message || e}`;
|
|
43
43
|
// credit outages need an actionable path the agent can relay — the web app has a top-up gate; here the URL is it
|
|
44
|
-
if (/not enough credits/i.test(msg)) msg += `\
|
|
44
|
+
if (/not enough credits/i.test(msg)) msg += `\nRun buy_credits to get a ready-to-pay checkout link (credit packs only; your human pays on Stripe's secure page — nothing was charged here). Or top up / upgrade at https://app.hermoso.ai (Settings → Billing). hermoso_credits shows the balance; hermoso_capabilities lists per-model credit costs.`;
|
|
45
45
|
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
46
46
|
}
|
|
47
47
|
};
|
|
@@ -69,10 +69,10 @@ export function registerTools(server) {
|
|
|
69
69
|
inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false },
|
|
70
70
|
}, wrap(async () => {
|
|
71
71
|
const d = await apiGet('/api/generate/status');
|
|
72
|
-
const img = (d.options?.image?.models || []).map(m => `${m.id} (${m.label}, ${m.credits}cr${m.best ? ', best' : ''})`).join('; ');
|
|
72
|
+
const img = (d.options?.image?.models || []).map(m => `${m.id} (${m.label}, ${m.credits}cr${m.refs ? `, ≤${m.refs.max} reference images` : ''}${m.hiRes ? ', 2K' : ''}${m.best ? ', best' : ''})`).join('; ');
|
|
73
73
|
// durations + per-duration credits MATTER: without them agents assume the generic "AI video caps at 8-10s"
|
|
74
74
|
// prior and wrongly steer users to stitching (a real Claude.ai session did exactly that on a 15s ad)
|
|
75
|
-
const vid = (d.options?.video?.models || []).map(m => `${m.id} (${m.label}: one continuous clip of ${(m.durations || []).map(x => `${x}s=${m.credits?.[x] ?? '?'}cr`).join(' ')}${m.audio ? ', native audio' : ', silent'}${m.best ? ', best' : ''})`).join('; ');
|
|
75
|
+
const vid = (d.options?.video?.models || []).map(m => `${m.id} (${m.label}: one continuous clip of ${(m.durations || []).map(x => `${x}s=${m.credits?.[x] ?? '?'}cr`).join(' ')}${m.audio ? ', native audio' : ', silent'}${m.refs ? `, ${m.refs.max} reference image${m.refs.max === 1 ? '' : 's'}${m.refs.required ? ' (required — image-to-video only)' : ''}` : ''}${m.resolutions ? `, resolutions ${m.resolutions.join('/')}` : ''}${m.best ? ', best' : ''})`).join('; ');
|
|
76
76
|
const text = `Image: ${d.image ? img : 'unavailable'}\nVideo: ${d.video ? vid : 'unavailable'}\nIMPORTANT: durations above are SINGLE-PASS — e.g. seedance-2 renders a full multi-beat 15s ad in ONE generation (do NOT assume a generic 8–10s cap, and do NOT stitch for ≤15s spots; stitching is only for longer). durationSeconds must be one of the model's listed values.\ncanEdit:${d.canEdit} canAvatar:${d.canAvatar} canPublish:${d.canPublish}\nRecipes (${(d.recipes || []).length}): ${(d.recipes || []).slice(0, 20).map(r => r.id).join(', ')}…`;
|
|
77
77
|
return ok(text, d);
|
|
78
78
|
}));
|
|
@@ -86,6 +86,28 @@ export function registerTools(server) {
|
|
|
86
86
|
return ok(`Balance: ${bal} credits${d.sessionUsed != null ? ` · session used: ${d.sessionUsed}` : ''}`, d);
|
|
87
87
|
}));
|
|
88
88
|
|
|
89
|
+
// AGENT BILLING HANDOFF: out of credits → mint a ready-to-pay Stripe checkout link for a credit PACK and hand the
|
|
90
|
+
// URL to the human. The human pays on Stripe's hosted page (agents never spend money directly); credits post to
|
|
91
|
+
// this account automatically once payment completes. Packs only — subscriptions are managed by a person in-app.
|
|
92
|
+
server.registerTool('buy_credits', {
|
|
93
|
+
description: "Out of credits? Get a ready-to-pay checkout link for a credit PACK. Call with no argument to list the available packs (id · credits · price); call again with `pack` set to a pack id to get a Stripe checkout URL. Hand that URL to your human — THEY pay on Stripe's secure hosted page (agents never spend money directly), and the credits land on this account the moment payment completes. Packs only; subscriptions are managed by a person in Settings → Billing. Nothing is charged until your human pays.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
pack: z.string().optional().describe('the pack id to buy (e.g. pack-2k) — omit to list the available packs first'),
|
|
96
|
+
},
|
|
97
|
+
annotations: { readOnlyHint: true, openWorldHint: true }, // creates no server-side charge; the human pays on Stripe's page
|
|
98
|
+
}, wrap(async ({ pack }) => {
|
|
99
|
+
const cfg = await apiGet('/api/billing/config');
|
|
100
|
+
const packs = (cfg.packs || []).map(p => ({ id: p.id, credits: p.credits, priceUsd: p.priceUsd }));
|
|
101
|
+
if (!pack) {
|
|
102
|
+
const lines = packs.map(p => `• ${p.id} — ${p.credits.toLocaleString()} credits · $${p.priceUsd}`).join('\n') || '(no packs configured)';
|
|
103
|
+
return ok(`Credit packs you can buy:\n${lines}\n\nCall buy_credits again with pack="<id>" to get a checkout link for your human to pay.`, { packs });
|
|
104
|
+
}
|
|
105
|
+
const match = packs.find(p => p.id === pack);
|
|
106
|
+
if (!match) return ok(`No pack "${pack}". Available: ${packs.map(p => p.id).join(', ') || '(none)'}. Call buy_credits with no argument to see details.`, { packs });
|
|
107
|
+
const d = await apiPost('/api/billing/checkout-link', { packId: pack });
|
|
108
|
+
return ok(`Checkout link for ${match.credits.toLocaleString()} credits ($${d.amountUsd ?? match.priceUsd}):\n${d.url}\n\nGive this URL to your human to pay on Stripe's secure page — the credits post to this account automatically once payment completes. Nothing is charged until they pay.`, d);
|
|
109
|
+
}));
|
|
110
|
+
|
|
89
111
|
server.registerTool('list_brands', {
|
|
90
112
|
description: "List every brand on this account (id + name) and which one this connection currently acts on. Multi-brand accounts: call this, then use_brand to switch. Read-only, free.",
|
|
91
113
|
inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false },
|
|
@@ -176,7 +198,7 @@ export function registerTools(server) {
|
|
|
176
198
|
|
|
177
199
|
|
|
178
200
|
server.registerTool('make_template_ad', {
|
|
179
|
-
description: "Render a NATIVE-STYLE TEMPLATE ad from pure HTML — no AI video/image model in the loop, renders in ~30 seconds for a couple of credits. Perfect for native-feel social ads at volume. YOU author the content (short, casual, believable — never marketing-speak). Templates (pass as config.template): 'imessage-chat' (VIDEO ~15s: a real-looking iMessage thread where a friend reveals the product as a rich-link card; config: { thread: { contactName, messages: [{from:'them'|'me', text?, product?:{image,title,domain}}] }, theme?:'dark'|'light', endCard:{headline,cta,domain,logo?,color} } — 4-6 short lowercase bubbles, product card mid-thread from 'me', 1-2 excited replies after); 'chatgpt-chat' (VIDEO: a ChatGPT answer streams the punchline; config: { question, answer (may **bold** the brand), productImage?, endCard }); 'apple-notes' (VIDEO: an iPhone note types itself out; config: { title, lines: string[], theme?, endCard }); 'value-prop' (VIDEO ~17s kinetic typography: config: { hook, claims: string[] (3-5, ≤
|
|
201
|
+
description: "Render a NATIVE-STYLE TEMPLATE ad from pure HTML — no AI video/image model in the loop, renders in ~30 seconds for a couple of credits. Perfect for native-feel social ads at volume. YOU author the content (short, casual, believable — never marketing-speak). Templates (pass as config.template): 'imessage-chat' (VIDEO ~15s: a real-looking iMessage thread where a friend reveals the product as a rich-link card; config: { thread: { contactName, messages: [{from:'them'|'me', text?, product?:{image,title,domain}}] }, theme?:'dark'|'light', endCard:{headline,cta,domain,logo?,color} } — 4-6 short lowercase bubbles, product card mid-thread from 'me', 1-2 excited replies after); 'chatgpt-chat' (VIDEO: a ChatGPT answer streams the punchline; config: { question, answer (may **bold** the brand), productImage?, endCard }); 'apple-notes' (VIDEO: an iPhone note types itself out; config: { title, lines: string[], theme?, endCard }); 'value-prop' (VIDEO ~17s kinetic typography: config: { hook (≤40 chars), claims: string[] (3-5 COMPLETE phrases, ≤6 words / ≤34 chars each — a finished thought, NEVER a clipped clause like 'Looks good on any'), productImages: string[] (2-3 DISTINCT photos — one rotates per card), palette: string[], endCard }); 'static-mockup' (IMAGE: config: { style:'imessage'|'notes'|'card', size?:{w,h}, ...style fields }); 'airdrop-carousel' (VIDEO ~10s: an iOS AirDrop share card springs up and cycles 3-16 REAL product photos to a full-lineup payoff; config: { brandName, products: [{image, title?}], contactLine?, endCard }); 'app-ui-tour' (VIDEO ~12-16s for APP brands: floating-iPhone mockup walks through REAL app screenshots with kinetic captions; config: { hook?, appName, iconImage?, beats: [{screenImage, caption}] (2-6), palette?, fontStack?, endCard }); 'imessage-cascade' (VIDEO ~12s: iOS notification banners spring in and stack over a blurred backdrop; config: { notifications: [{sender, text}] (4-8), backgroundImage?, endCard }); 'photo-grid' (VIDEO ~8s: collage assembles real photos one at a time; config: { title?, photos: [{image, label?}] (4-9), palette?, fontStack?, endCard }); 'vignette' (VIDEO ~12s: cinematic Ken-Burns hero film; config: { hook, lines: [2-4 ≤40ch], heroImage, palette?, fontStack?, endCard }); 'myth-vs-fact' (VIDEO ~15-26s VO-FIRST kinetic explainer with a real VOICEOVER — the family's ONE paid-audio format: a calm-authority read busts 2-4 myths, each MYTH line slamming in with a red per-line strike then the counter FACT line landing bold+affirmative, word-level KARAOKE lighting each word as the VO speaks it; config: { pairs: [{ myth (≤50ch, the common wrong belief), fact (≤60ch, the corrective truth — wrap its payoff phrase in [brackets] to accent it) }] (2-4), palette?, fontStack?, endCard }. Real product truths only — NEVER invent stats. Costs the flat template credits PLUS a small voiceover charge); 'carousel' (MULTI-IMAGE: 5-10 branded 1080×1080 PNG slides for Meta/LinkedIn/IG carousels — returns an images[] array, one PNG per slide; config: { cover: { hook?, title }, slides: [{ headline (≤8 words), support? (≤16 words), stat?: { value, label } }] (3-8; a stat slide is a REAL user-supplied number like '94%' or '40k+' + a label, never invented), cta: { headline, cta?, domain? }, productImage?, logo?, palette?, fontStack?, endCardColor? }). Image URLs may be any public URL — the server localizes them. Spends a couple of credits.",
|
|
180
202
|
inputSchema: {
|
|
181
203
|
config: z.object({}).passthrough().describe("the template config — MUST include config.template (one of the template ids above) plus that template's fields"),
|
|
182
204
|
},
|
|
@@ -595,7 +617,7 @@ export function registerTools(server) {
|
|
|
595
617
|
}));
|
|
596
618
|
|
|
597
619
|
server.registerTool('draft_brand', {
|
|
598
|
-
description: 'Onboard a brand profile — from a website domain, a free-text description, or a social handle — into a {name, products, logo, …} object you can pass to plan_ad / generate. 0 ScrapeCreators credits.',
|
|
620
|
+
description: 'Onboard a brand profile — from a website domain, a free-text description, or a social handle — into a {name, products, logo, …} object you can pass to plan_ad / generate. 0 ScrapeCreators credits. IMPORTANT: a domain can resolve to a DIFFERENT company than intended (e.g. bala.com is an engineering firm, not the Bala fitness brand at shopbala.com). Before spending any credits on research or renders, VERIFY the returned `name` (and `summary`) match the brand the user meant; if it looks wrong, re-draft with the correct domain or a description (pass save:false until confirmed) — this tool cannot ask the user, so the caller owns that check.',
|
|
599
621
|
inputSchema: {
|
|
600
622
|
domain: z.string().optional().describe('a website to scrape'),
|
|
601
623
|
description: z.string().optional().describe('a free-text brand description (no website)'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
4
5
|
"description": "Drive Hermoso — the AI ad studio — from any AI agent: MCP server, CLI, and Claude skills for researching winning ads and generating finished image & video ads.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|