hermoso 0.1.161 → 0.1.162

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
@@ -32,14 +32,14 @@ Two shapes, and the right one is decided by **what your client can do**, not by
32
32
  | **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. |
33
33
  | **Can run a shell** — Claude Code, Cursor, Codex, Cline, OpenClaw, Hermes, your own scripts | the CLI, `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. |
34
34
 
35
- **The measured difference** (2026-08-24, counted as real tool definitions rather than estimated from bytes):
35
+ **The measured difference** (2026-08-27, counted as real tool definitions rather than estimated from bytes):
36
36
 
37
37
  | | tools in range | loaded per session |
38
38
  | --- | --- | --- |
39
- | Hosted connector, default roster | 291 | **166,043 tokens** |
40
- | Hosted connector, `?tools=all` | 706 | **456,392 tokens** |
41
- | stdio server (`npx -y hermoso mcp`) | 269 | **153,257 tokens** |
42
- | **CLI** | **all 681** | **0** |
39
+ | Hosted connector, default roster | 306 | **181,713 tokens** |
40
+ | Hosted connector, `?tools=all` | 718 | **472,062 tokens** |
41
+ | stdio server (`npx -y hermoso mcp`) | 306 | **181,713 tokens** |
42
+ | **CLI** | **all 718** | **0** |
43
43
 
44
44
  The CLI answers the same questions on demand instead, and only when asked:
45
45
 
@@ -50,7 +50,7 @@ npx -y hermoso call plan_ad --json '{"product":"…"}' # run it
50
50
  ```
51
51
 
52
52
  So a terminal agent reaches its first call in roughly **3.4K tokens with the whole roster in range**, against
53
- **153K for a fraction of it**. `tools` and `tools <name>` read a registry bundled in the package — no key, no
53
+ **182K for a fraction of it**. `tools` and `tools <name>` read a registry bundled in the package — no key, no
54
54
  network, no sign-in — so an agent can browse the entire product before anyone signs in. Only `call` spends, and
55
55
  only that needs `hermoso auth login` once.
56
56
 
@@ -64,6 +64,49 @@ calls into one area. `enable_tools({groups:['ads']})` turns campaign management
64
64
  tools are then native — no shell quoting, structured results. One shell round trip beats loading a 221K-token
65
65
  group for a single tool; the reverse is true once a session settles into that area.
66
66
 
67
+ ## Your agent can sign itself up
68
+
69
+ An agent with no Hermoso account can provision one, get its own key, and be rendering ads in the same session.
70
+ No human at a browser, no ticket, no waiting.
71
+
72
+ ```bash
73
+ # 1. Start a signup. This call takes no credential, because the credential is what it creates.
74
+ curl -sX POST https://app.hermoso.ai/v1/signup \
75
+ -H 'content-type: application/json' \
76
+ -d '{"plan":"pro","period":"mo"}'
77
+ # -> { "id": "cs_...", "checkout_url": "https://checkout.stripe.com/...", "claim_token": "hsc_..." }
78
+
79
+ # 2. Pay at checkout_url. Store claim_token first: it is returned only in that response.
80
+
81
+ # 3. Claim it. Poll until status is "ready".
82
+ curl -sX POST https://app.hermoso.ai/v1/signup/cs_.../claim \
83
+ -H 'content-type: application/json' \
84
+ -d '{"claim_token":"hsc_..."}'
85
+ # -> { "status": "ready", "api_key": "hmk_...", "credits": 3000 }
86
+ ```
87
+
88
+ That `hmk_` key is the same credential everything else on this page takes: `/v1`, the MCP server, the CLI. Point
89
+ your client at it and the full surface is open.
90
+
91
+ **Paying is something a browser-capable agent can already do itself.** Checkout is Stripe's own hosted page, so
92
+ Claude in Chrome and clients like it complete it unattended today. Everything else is a one-click handoff: send
93
+ `checkout_url` to whoever holds the card. The same shape covers you later, once you are running: `buy_credits`
94
+ and `upgrade_plan` mint a ready-to-pay link for more credits or a bigger plan, and `billing_status` reads the
95
+ balance any time.
96
+
97
+ **The agentic path takes a paid plan.** Any of them. The free plan is there for a person signing up at
98
+ [app.hermoso.ai](https://app.hermoso.ai), and asking for it here returns a refusal that says so. Nothing is
99
+ created until the payment completes, so an unpaid signup leaves no account behind and charges nothing.
100
+
101
+ **One thing still wants a person, and it is worth knowing up front.** Connecting a social or ad account means an
102
+ OAuth consent screen, and a consent screen cannot be completed headlessly on any platform. `list_connectors`
103
+ shows what is already connected and what is not. Everything else runs with no browser at all: research,
104
+ generation, publishing to a channel that is already connected, campaign builds, reporting.
105
+
106
+ Full request and response shapes, plus every other endpoint, are in the OpenAPI document at
107
+ [app.hermoso.ai/openapi.json](https://app.hermoso.ai/openapi.json), served live from the same table that mounts
108
+ the routes.
109
+
67
110
  ## Instant: the hosted Claude.ai connector
68
111
 
69
112
  Paste **`https://app.hermoso.ai/mcp`** into Claude → Settings → Connectors → *Add custom connector*, approve with
@@ -72,7 +115,8 @@ your Hermoso account, done — the full toolset with your saved brand context, b
72
115
  ## Quickstart for Claude Code (one line)
73
116
 
74
117
  1. **Get an account** at [app.hermoso.ai](https://app.hermoso.ai) — free tier included; plans & credits are the
75
- same ones the web Studio uses.
118
+ same ones the web Studio uses. Or skip the browser entirely and let your agent sign itself up on a paid plan
119
+ with `POST /v1/signup` (above).
76
120
  2. **Run one line.** Your browser opens once to sign in. Nothing to paste, and no key lands in `.claude.json`:
77
121
 
78
122
  ```bash
@@ -124,7 +168,7 @@ Then ask your agent: *“Generate an image ad with Hermoso.”*
124
168
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
125
169
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
126
170
  social (`search_tiktok`, `search_instagram`, `search_youtube`, `search_reddit`, `search_threads`);
127
- `scrapecreators_fetch`, `mine_angles`, `analyze_video`, `check_ad_policy`, `list_skills` / `get_skill`.
171
+ `fetch_social_data`, `mine_angles`, `analyze_video`, `check_ad_policy`, `list_skills` / `get_skill`.
128
172
 
129
173
  **Create** — `draft_brand` → `plan_ad` → `render_ad` (the Studio quality pipeline: composited text, clean speech,
130
174
  music, brand end card), or `generate_image` / `generate_video` / `generate_avatar` (UGC creators + lip-sync).
@@ -221,7 +265,7 @@ Render jobs queue server-side and poll to completion, returning a served URL.
221
265
 
222
266
  ## 2. CLI — the token-cheap path for terminal agents
223
267
 
224
- `bin/hermoso.mjs` mirrors the core tools as subprocess commands, so an agent can shell out instead of carrying a
268
+ `bin/hermoso.mjs` exposes the full MCP toolset as subprocess commands, so an agent can shell out instead of carrying a
225
269
  fat tool manifest.
226
270
 
227
271
  ```bash
package/mcp/http.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  // ───────────────────────────────────────────────────────────────────────────────────────────────────────
2
2
  // REMOTE MCP CONNECTOR — DEFERRED. This is the Claude.ai "custom connector" surface (https://<host>/mcp):
3
- // a Streamable-HTTP MCP transport + OAuth so any Claude.ai / Cursor user can connect Hermoso by URL and sign in,
4
- // exactly like Higgsfield's mcp.higgsfield.ai/mcp.
3
+ // a Streamable-HTTP MCP transport + OAuth so any Claude.ai / Cursor user can connect Hermoso by URL and sign in.
5
4
  //
6
5
  // It is written so the cloud step is a CONFIG FLIP, not a rewrite — but it is intentionally OFF and will REFUSE
7
6
  // to mount until BOTH are true:
@@ -34,16 +34,16 @@
34
34
  // rsync'd into a published package that has no lib/ and no repo around it.
35
35
  // RESEARCH IS NEVER GATED, AND THIS LIST IS THE REASON THE WHOLE CHANGE IS SAFE.
36
36
  //
37
- // The ad libraries and organic social search run on OUR ScrapeCreators key, not on the user's connection — a brand
37
+ // The ad libraries and organic social search run on OUR OWN research key, not on the user's connection — a brand
38
38
  // with nothing connected can and must still spy on its competitors' Meta ads. But their NAMES look exactly like
39
39
  // connector tools: `search_meta_ads` contains `_meta_`, `search_youtube` contains `youtube`. Six of the nine would
40
40
  // have been silently gated by the rules below, which would have broken the product's single most-used feature for
41
41
  // every new account — the exact users this change exists to protect.
42
42
  //
43
43
  // DERIVED, NOT HAND-LISTED. `tools/studio-roster-check.mjs` asserts this set is a SUPERSET of server.js's own
44
- // `SC_WINDOW_TOOLS` — the set the route already uses to decide which tools take a ScrapeCreators balance window.
45
- // So a tenth SC-backed tool added there fails the suite rather than quietly losing research for zero-connector
46
- // accounts. Anything ScrapeCreators pays for, the user reaches without connecting anything.
44
+ // `SC_WINDOW_TOOLS` — the set the route already uses to decide which tools take a research-balance window.
45
+ // So a tenth research-backed tool added there fails the suite rather than quietly losing research for zero-connector
46
+ // accounts. Anything our own research key pays for, the user reaches without connecting anything.
47
47
  //
48
48
  // NOTE what is deliberately NOT here: `search_instagram_hashtag`, `instagram_profile`, `search_threads_keyword`,
49
49
  // `discover_tiktok_creators`, `tiktok_creator_info`. Those read the PLATFORM's data through the USER'S token
@@ -51,7 +51,7 @@
51
51
  // connection, so gating them is correct.
52
52
  export const NEVER_GATE = new Set([
53
53
  'search_meta_ads', 'search_google_ads', 'search_linkedin_ads', 'search_tiktok',
54
- 'search_instagram', 'search_youtube', 'search_reddit', 'search_threads', 'scrapecreators_fetch',
54
+ 'search_instagram', 'search_youtube', 'search_reddit', 'search_threads', 'fetch_social_data',
55
55
  ]);
56
56
 
57
57
  // Tool-name → connector provider. ORDERED: the first match wins, so the more specific pattern must come first.
package/mcp/tools.mjs CHANGED
@@ -151,7 +151,7 @@ export const CAPABILITY_MAP = [
151
151
  'What Hermoso can do — the full agent surface (every tool below runs over this MCP):',
152
152
  // SECOND LINE, deliberately: the map below is a menu, and a menu read as a sequence is the whole defect.
153
153
  INDEPENDENCE,
154
- 'A) AD SPY / RESEARCH — spy on the ads already winning in any market, then mine them. find_competitors · competitor_teardown · pull_competitor_ads · research_ads (open brief) · ad libraries search_meta_ads / search_google_ads / search_linkedin_ads · organic social search_tiktok / search_instagram / search_youtube / search_reddit / search_threads · search_instagram_hashtag (LISTENING on the brand’s OWN Meta credentials rather than a scraper: the real public posts carrying a hashtag, with their captions — feed them into mine_angles or write the next post from the language you found. “recent” is the LAST 24 HOURS only, so a huge tag legitimately returns zero on a quiet day; ask again with edge “top” before saying anything about how busy it is) · instagram_profile (any Instagram @handle → the account’s NUMERIC Instagram id from Meta itself, plus its real name, bio, follower and post counts — Meta’s own numbers, not a scraper’s. It is also the ONLY way to get the id manage_meta_partnership_creator’s allowTagging list requires; professional accounts only) · scrapecreators_fetch (any allowlisted endpoint) · mine_angles · analyze_video · check_ad_policy · list_skills / get_skill (teardowns + creative playbooks).',
154
+ 'A) AD SPY / RESEARCH — spy on the ads already winning in any market, then mine them. find_competitors · competitor_teardown · pull_competitor_ads · research_ads (open brief) · ad libraries search_meta_ads / search_google_ads / search_linkedin_ads · organic social search_tiktok / search_instagram / search_youtube / search_reddit / search_threads · search_instagram_hashtag (LISTENING on the brand’s OWN Meta credentials rather than a scraper: the real public posts carrying a hashtag, with their captions — feed them into mine_angles or write the next post from the language you found. “recent” is the LAST 24 HOURS only, so a huge tag legitimately returns zero on a quiet day; ask again with edge “top” before saying anything about how busy it is) · instagram_profile (any Instagram @handle → the account’s NUMERIC Instagram id from Meta itself, plus its real name, bio, follower and post counts — Meta’s own numbers, not a scraper’s. It is also the ONLY way to get the id manage_meta_partnership_creator’s allowTagging list requires; professional accounts only) · fetch_social_data (any allowlisted endpoint) · mine_angles · analyze_video · check_ad_policy · list_skills / get_skill (teardowns + creative playbooks).',
155
155
  'B) CREATE — finished, on-brand image & video ads (real product composited in, copy + CTA baked). draft_brand / get_brand / update_brand (patch single fields without re-onboarding) / use_brand · list_brands / create_brand / delete_brand (one account holds MANY brand workspaces — an agency runs every client through here; each has its own brand, memory, swipefile, Library and connectors, and create_brand → draft_brand onboards a new one end to end) · plan_ad (concept + copy) → render_ad (the Studio quality pipeline) or generate_image / generate_video / generate_avatar (UGC creators + lip-sync) · list_creators / save_creator / delete_creator (the workspace’s REUSABLE CAST — saved creators with their portrait urls, so the SAME person stars in every ad; list them before ever generating a new one, then cast one into the ad with render_ad’s `creator`, which also skips the character-portrait render and so costs LESS than casting a stranger) · make_template_ad (native HTML ad formats) · remix_static / recast_motion / reframe_video / upscale_video / dub_video / change_voice / finish_video / fix_beat / stitch_video · plan_variations + score_ad (fan out + rank).',
156
156
  'C) RAW MODEL PLAYGROUND — direct access to the full catalog (30+ image / video / voice / writing models, each with the exact per-render credit cost shown above), no ad framing: generate_image / generate_video (useBrand:false) for plain prompt-only renders, generate_voice for raw text-to-speech against any voice engine, and generate_text for the writing models (Claude / Gemini / GPT / Llama / DeepSeek…) — all against ANY catalog id.',
157
157
  'D) ACCOUNT — hermoso_credits (balance) · billing_status (plan + your billing role) · buy_credits (one-click top-up on the saved card, or a first-purchase checkout link) · upgrade_plan / set_auto_reload (admin) · list_jobs / get_job (track async renders) · get_settings / update_settings (the LANGUAGE every ad, script, plan and answer is written in — set it once and every render obeys it, over MCP as well as in the app — plus app appearance and the weekly competitor-watch email) · list_team / invite_member / remove_member / set_role (who else can work in this brand).',
@@ -225,7 +225,7 @@ export const MCP_INSTRUCTIONS = [
225
225
  // discoverable is the other half of the fix, so the reasons to call it are spelled out rather than merely permitted.
226
226
  'ACT ON THE REQUEST, DO NOT SURVEY IT: when the user asks for something to be made, make it. generate_image, generate_video and render_ad all run with `model` omitted, and an unnamed render goes to the server’s own default model, which is a sound general-purpose pick, so there is nothing you have to look up before rendering. Call hermoso_capabilities (free) when you actually need what it holds: a specific model id, an exact credit cost, a model’s live durations / aspect ratios / resolutions, or whether a capability is enabled on this account. Reporting the model catalog back is never the answer to a request to create something.',
227
227
  'Capability map:',
228
- '• AD SPY / RESEARCH: find_competitors, competitor_teardown, pull_competitor_ads, research_ads; ad libraries search_meta_ads / search_google_ads / search_linkedin_ads; organic search_tiktok / search_instagram / search_youtube / search_reddit / search_threads; scrapecreators_fetch; mine_angles; analyze_video; check_ad_policy; list_skills / get_skill.',
228
+ '• AD SPY / RESEARCH: find_competitors, competitor_teardown, pull_competitor_ads, research_ads; ad libraries search_meta_ads / search_google_ads / search_linkedin_ads; organic search_tiktok / search_instagram / search_youtube / search_reddit / search_threads; fetch_social_data; mine_angles; analyze_video; check_ad_policy; list_skills / get_skill.',
229
229
  '• CREATE (finished ads): render_ad (Studio quality pipeline) or generate_image / generate_video / generate_avatar render on their own; plan_ad authors a board first when the ad wants one and render_ad takes it; get_brand (what we already know) / draft_brand (onboard one) / update_brand (patch a field) manage the saved brand, which the create tools hydrate by themselves; list_creators / save_creator / delete_creator (the reusable saved CAST — re-cast the same face instead of generating a new person every time; render_ad’s `creator` stars one of them in the ad); make_template_ad (native HTML formats); make_thumbnail (YouTube / Shorts / Instagram video thumbnails + covers — use it for any thumbnail or video-cover ask, never generate_image); remix_static / recast_motion / reframe_video / upscale_video / dub_video / change_voice / finish_video / fix_beat / stitch_video; plan_variations + score_ad.',
230
230
  '• RAW MODEL PLAYGROUND: generate_image / generate_video (useBrand:false) for prompt-only renders, generate_voice for text-to-speech, generate_text for the writing models — against any of 30+ image / video / voice / writing model ids (exact costs in hermoso_capabilities), no ad framing.',
231
231
  '• ACCOUNT & WORKSPACES: hermoso_credits, billing_status, buy_credits (one-click top-up / first-purchase link), upgrade_plan / set_auto_reload (admin), list_jobs / get_job; list_brands / create_brand / use_brand / delete_brand (one account holds MANY brand workspaces — an agency runs every client through here, each with its own brand, memory, Library and connectors; create_brand → draft_brand onboards a new one, delete_brand is confirm-gated); get_settings / update_settings (the LANGUAGE every ad, script, plan and answer is written in — set it once and every render obeys it — plus app appearance and the weekly competitor-watch email); list_team / invite_member / remove_member / set_role.',
@@ -487,7 +487,7 @@ const HOOK_ATTR = {
487
487
  hook: z.string().optional().describe('WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer "which hooks work", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "before_after") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works.'),
488
488
  subject: z.string().optional().describe('WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. "winter coat", "free trial", "founder story"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group.'),
489
489
  };
490
- // Higgsfield's "Duration to boards" table in one line — fill every act to the model max, remainder LAST, and pull the
490
+ // The "Duration to boards" table in one line — fill every act to the model max, remainder LAST, and pull the
491
491
  // deficit off the previous act when the remainder would fall under the provider floor (their own 18 -> 14+4). Mirrors
492
492
  // hfClipDurations in acts-packing.mjs, which is what actually packs the render; here it only makes the refusal concrete.
493
493
  const hfSplitHint = (total, max = VIDEO_SINGLE_CLIP_CEILING, min = 4) => {
@@ -2029,7 +2029,7 @@ function replayTools(rawServer, opts, canon) {
2029
2029
  // THEIR key — with no recourse on their side and, because it never touches our error ledger, no visibility on ours.
2030
2030
  // The same tool answers the same way on every retry, so it is a deterministic loop wearing a transient's clothes.
2031
2031
  //
2032
- // AND WE ARE THE PRODUCER, NOT THE VENDOR. 14,579 live ScrapeCreators strings held 570 real surrogate PAIRS and
2032
+ // AND WE ARE THE PRODUCER, NOT THE VENDOR. 14,579 live upstream strings held 570 real surrogate PAIRS and
2033
2033
  // ZERO lone ones; `clip()` manufactures them by truncating at a CHARACTER COUNT (server.js now truncates on a code
2034
2034
  // POINT boundary, which removes the source — this stays as the boundary that catches everything else, including
2035
2035
  // the 1,166 other numeric `.slice(0, N)` sites and any vendor that hands us pre-broken text).
@@ -5106,7 +5106,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5106
5106
  }));
5107
5107
  server.registerTool('list_bluesky_convos', {
5108
5108
  title: 'List Bluesky direct-message conversations',
5109
- description: 'Read the connected Bluesky account\u2019s DM conversations \u2014 who each one is with, how many messages are unread, and whether it is a REQUEST (someone the account does not follow, which Bluesky holds separately, the same idea as a message request). Free, no ScrapeCreators credits, no vendor approval: AT Protocol app passwords are not scoped. It DOES need a PRIVILEGED app password \u2014 one created with direct-message access ticked \u2014 and says so precisely if the saved one cannot chat, which is a property of the password and NOT a broken connection. Filters: readState "unread", status "request" or "accepted", kind "direct" or "group". An unknown filter value is refused by name, never silently dropped.',
5109
+ description: 'Read the connected Bluesky account\u2019s DM conversations \u2014 who each one is with, how many messages are unread, and whether it is a REQUEST (someone the account does not follow, which Bluesky holds separately, the same idea as a message request). Free, no credits, no vendor approval: AT Protocol app passwords are not scoped. It DOES need a PRIVILEGED app password \u2014 one created with direct-message access ticked \u2014 and says so precisely if the saved one cannot chat, which is a property of the password and NOT a broken connection. Filters: readState "unread", status "request" or "accepted", kind "direct" or "group". An unknown filter value is refused by name, never silently dropped.',
5110
5110
  inputSchema: {
5111
5111
  limit: z.number().optional().describe('how many conversations, 1\u2013100 (default 25)'),
5112
5112
  cursor: z.string().optional().describe('walk further back \u2014 pass the cursor from a previous call'),
@@ -14137,7 +14137,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
14137
14137
  server.group('create');
14138
14138
  server.registerTool('plan_ad', {
14139
14139
  title: 'Plan an ad concept',
14140
- description: 'Creative director: turn a brand + product/brief into a finished ad CONCEPT — copy variants (headline/primary/cta) plus an image_concept.prompt OR a video_storyboard, with the resolved recipe + the model ids to render with. Renders nothing; chain its output into generate_image / generate_video. THE USER’S EXPLICIT LENGTH IS SOVEREIGN: when they name a duration ("a 30 second ad", "make it 45s"), pass it as durationSeconds — the board is then AUTHORED to that length (its scenes sum to it) and render_ad renders it as one clip or stitched acts accordingly. Leaving it out lets the planner pick its own default, which is how an explicit ask silently becomes a 15s spot. Spends LLM tokens, 0 ScrapeCreators credits.',
14140
+ description: 'Creative director: turn a brand + product/brief into a finished ad CONCEPT — copy variants (headline/primary/cta) plus an image_concept.prompt OR a video_storyboard, with the resolved recipe + the model ids to render with. Renders nothing; chain its output into generate_image / generate_video. THE USER’S EXPLICIT LENGTH IS SOVEREIGN: when they name a duration ("a 30 second ad", "make it 45s"), pass it as durationSeconds — the board is then AUTHORED to that length (its scenes sum to it) and render_ad renders it as one clip or stitched acts accordingly. Leaving it out lets the planner pick its own default, which is how an explicit ask silently becomes a 15s spot. Spends credits.',
14141
14141
  inputSchema: {
14142
14142
  brand: z.union([z.string(), z.object({}).passthrough()]).optional().describe('brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one'),
14143
14143
  product: z.string().describe('what to advertise + any angle/offer the user specified'),
@@ -14540,11 +14540,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
14540
14540
 
14541
14541
  server.registerTool('make_explainer', {
14542
14542
  title: 'Make an explainer video',
14543
- description: "Turn a TOPIC into a finished narrated explainer video. Writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE, exactly as Higgsfield's stills pipeline does it — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; `captions:true` adds held key points and `subtitles:true` adds narration-timed CAPS (see both). It is an image film WITH motion, not N video-model renders — that's what keeps it affordable. `style` picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; `frameDensity:'lean'` roughly halves it and `'minimal'` (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.",
14543
+ description: "Turn a TOPIC into a finished narrated explainer video. Writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; `captions:true` adds held key points and `subtitles:true` adds narration-timed CAPS (see both). It is an image film WITH motion, not N video-model renders — that's what keeps it affordable. `style` picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; `frameDensity:'lean'` roughly halves it and `'minimal'` (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.",
14544
14544
  inputSchema: {
14545
14545
  topic: z.string().describe('what the explainer should teach or explain — a topic or a short brief'),
14546
14546
  durationSeconds: z.number().optional().describe('target length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections'),
14547
- frameDensity: z.enum(['standard', 'lean', 'minimal']).optional().describe("how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density Higgsfield's own stills pipeline enforces; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is ONE picture per narration section, which is cheapest and is frankly a slideshow. Only drop below the default if the user asked for something cheaper."),
14547
+ frameDensity: z.enum(['standard', 'lean', 'minimal']).optional().describe("how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is ONE picture per narration section, which is cheapest and is frankly a slideshow. Only drop below the default if the user asked for something cheaper."),
14548
14548
  aspectRatio: z.enum(['9:16', '16:9', '1:1', '4:5', '3:4']).optional().describe("'9:16' default"),
14549
14549
  style: z.enum(['cinematic', 'editorial_collage', 'flat_vector', 'stickman', 'whiteboard', 'ink_marker', 'silhouette', 'storybook', 'paper_diorama', 'isometric', 'claymation', 'pixel_art', 'watercolor', 'fluffy_toy', 'low_poly', 'stylized_3d', 'studio_3d', 'mannequin']).optional().describe("visual style. 'cinematic' (default) is photoreal; the rest are non-photoreal styled looks — editorial_collage (halftone cutouts + marker accents), flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook (gouache), paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy (felted plush), low_poly, stylized_3d (matte clay render), studio_3d (preschool toy 3D on a white sweep — the Kids default), mannequin (clay-render reenactment figures — a History alternate). Ask the user which they want rather than picking silently; a styled pick costs more (see the cost note)."),
14550
14550
  channel: z.enum(['explainer', 'history', 'kids', 'fairytale']).optional().describe("the CHANNEL TYPE — it sets the pacing, the narration register and the default look, and is orthogonal to `style` (a named style always wins): explainer (casual second-person, fast cuts), history (witty chronological retelling / documentary), kids (fastest, question-first, warm teacher), fairytale (slow, atmospheric myth or folklore). Default 'explainer'."),
@@ -14781,7 +14781,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
14781
14781
  return ok(wireText, { ...j, url });
14782
14782
  }));
14783
14783
 
14784
- // ---------- skills (Higgsfield get_workflow_instructions parity: workflows ship as SKILL.md bundles) ----------
14784
+ // ---------- skills (workflows ship as SKILL.md bundles) ----------
14785
14785
  server.group('create');
14786
14786
  // The bundle dirs/content may still carry the pre-rename brand — always serve them under the product name.
14787
14787
  const brandSkillText = (s) => String(s).replace(/HEIST_/g, 'HERMOSO_').replace(/heist-/g, 'hermoso-').replace(/Heist/g, 'Hermoso').replace(/\bheist\b/g, 'hermoso');
@@ -15655,7 +15655,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15655
15655
  server.group('research');
15656
15656
  server.registerTool('find_competitors', {
15657
15657
  title: 'Find competitors',
15658
- description: "Discover a brand's competitor / similar / adjacent brands from its domain (Claude grounded by web search). mode=competitors (default, excludes the searched company), inspiration (best relevant ads incl. it), or company. 0 ScrapeCreators credits.",
15658
+ description: "Discover a brand's competitor / similar / adjacent brands from its domain (Claude grounded by web search). mode=competitors (default, excludes the searched company), inspiration (best relevant ads incl. it), or company. 0 credits.",
15659
15659
  inputSchema: {
15660
15660
  domain: z.string().describe('the brand domain, e.g. flourish.com'),
15661
15661
  mode: z.enum(['competitors', 'inspiration', 'company']).optional().describe("'competitors' (default, excludes the searched company), 'inspiration' (best relevant ads incl. it), or 'company'"),
@@ -15674,7 +15674,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15674
15674
  server.registerTool('pull_competitor_ads', {
15675
15675
  _meta: openaiMeta(AD_SPY_URI, 'Pulling their live ads…', 'Competitor ads pulled'),
15676
15676
  title: 'Pull competitor ads',
15677
- description: 'THE FAST PATH for "show me the ads <brand> is running" \u2014 one named brand\u2019s real live ads from the META (Facebook/Instagram) ad library, deduped, sorted, with the right page resolved. A single call, back in a few seconds. Prefer this over research_ads whenever the brand is named. Meta only, deliberately: it has by far the richest creative and is what people mean by "their ads". For Google or LinkedIn specifically, use search_google_ads or search_linkedin_ads. Spends ScrapeCreators credits.',
15677
+ description: 'THE FAST PATH for "show me the ads <brand> is running" \u2014 one named brand\u2019s real live ads from the META (Facebook/Instagram) ad library, deduped, sorted, with the right page resolved. A single call, back in a few seconds. Prefer this over research_ads whenever the brand is named. Meta only, deliberately: it has by far the richest creative and is what people mean by "their ads". For Google or LinkedIn specifically, use search_google_ads or search_linkedin_ads. Spends credits.',
15678
15678
  inputSchema: {
15679
15679
  companyName: z.string().optional().describe('the advertiser name'),
15680
15680
  domain: z.string().optional().describe('the advertiser domain'),
@@ -15750,7 +15750,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15750
15750
  const cards = adSpyCards(rows);
15751
15751
  const modelRows = widget ? stripAdMedia(rows) : rows;
15752
15752
  // ON A WIDGET HOST, THE ANSWER IS THE CARD — SO STOP ASKING THE MODEL TO RE-TYPE IT (2026-08-24). Measured on
15753
- // this exact path: our fan-out (server + ScrapeCreators) is ~5s, and the user waits ~30. The rest is the model
15753
+ // this exact path: our fan-out is ~5s, and the user waits ~30. The rest is the model
15754
15754
  // reading sixteen ad rows and then WRITING a bullet for every one of them, next to a card that is already
15755
15755
  // showing all sixteen with their creative. That enumeration is the latency, it is duplicated effort, and we
15756
15756
  // invite it by handing over the rows at all.
@@ -15782,7 +15782,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15782
15782
  description: 'Set (or STOP) this workspace\'s standing COMPETITOR WATCH — the weekly job that re-checks each named brand\'s ad libraries and reports what is NEW since last time. The same watch the web app\'s Ad Spy ▸ Watching tab manages, and the same one the weekly digest email is sent from (turn that email on/off with update_settings({watchEmail})). '
15783
15783
  + 'This REPLACES the whole watched list, it does not add to it — pass every brand you want watched, every time. Max 5 brands (the server trims past that). Pass an EMPTY list to stop the watch entirely, which also clears the findings. '
15784
15784
  + 'Give a `domain` wherever you know one: Google Ads Transparency is looked up BY DOMAIN and is skipped for a brand without one, and the domain is what resolves the right Meta page for a brand with an ambiguous name. '
15785
- + 'The run itself spends ScrapeCreators credits against the ad libraries (roughly 3 per brand on Meta, 1 each on Google and LinkedIn) and is hard-capped per run server-side, so an oversized watch is trimmed rather than allowed to run away. Setting the list is free; only a run spends. '
15785
+ + 'The run itself spends credits against the ad libraries (roughly 3 per brand on Meta, 1 each on Google and LinkedIn) and is hard-capped per run server-side, so an oversized watch is trimmed rather than allowed to run away. Setting the list is free; only a run spends. '
15786
15786
  + 'runNow:true runs it once IMMEDIATELY (a background job — it spends now) and then keeps the weekly cadence; leave it off and the first check is a week out. '
15787
15787
  + 'The country and the platform mix are NOT settable here — a re-set inherits whatever the pending run already carried (US / Meta for a watch that has never been configured otherwise). Read the findings back with list_watch_findings.',
15788
15788
  inputSchema: {
@@ -15883,7 +15883,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15883
15883
  server.registerTool('research_ads', {
15884
15884
  _meta: openaiMeta(AD_SPY_URI, 'Researching ads…', 'Ad research ready'),
15885
15885
  title: 'Research ads',
15886
- description: 'Open-ended ad research that needs JUDGMENT across platforms — comparisons, "what angle is working", "who else is doing this", anything where the right sources are not known up front. It is an agentic loop (several rounds of library pulls plus a written synthesis) and typically takes 30-60 seconds, so it is the WRONG tool for a question that names its own answer. For one named brand\u2019s live ads use pull_competitor_ads; for one keyword or one advertiser on Meta use search_meta_ads \u2014 both are a single call and return in a few seconds. Spends LLM tokens + ScrapeCreators credits.',
15886
+ description: 'Open-ended ad research that needs JUDGMENT across platforms — comparisons, "what angle is working", "who else is doing this", anything where the right sources are not known up front. It is an agentic loop (several rounds of library pulls plus a written synthesis) and typically takes 30-60 seconds, so it is the WRONG tool for a question that names its own answer. For one named brand\u2019s live ads use pull_competitor_ads; for one keyword or one advertiser on Meta use search_meta_ads \u2014 both are a single call and return in a few seconds. Spends credits an agentic loop, so a handful rather than the one-call cost of a targeted search.',
15887
15887
  inputSchema: {
15888
15888
  query: z.string().describe('what to research, e.g. "the longest-running protein-pancake ads on Meta"'),
15889
15889
  brand: z.union([z.string(), z.object({}).passthrough()]).optional().describe('brand name or profile object to tailor the research to; omit to use the workspace’s saved brand'),
@@ -15970,7 +15970,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
15970
15970
  server.registerTool('search_meta_ads', {
15971
15971
  _meta: openaiMeta(AD_SPY_URI, 'Searching Meta ads…', 'Found Meta ads'),
15972
15972
  title: 'Search Meta ads',
15973
- description: "Structured Meta (Facebook/Instagram) Ad Library pull — use when you know exactly WHAT to fetch: a keyword (query) OR one advertiser (companyName / pageId). Returns compact JSON {page_name, body, cta, link, dates, media} per ad. For open-ended research that needs judgment across platforms, use research_ads instead. Spends ScrapeCreators credits (~1–2).",
15973
+ description: "Structured Meta (Facebook/Instagram) Ad Library pull — use when you know exactly WHAT to fetch: a keyword (query) OR one advertiser (companyName / pageId). Returns compact JSON {page_name, body, cta, link, dates, media} per ad. For open-ended research that needs judgment across platforms, use research_ads instead. Spends a credit or two.",
15974
15974
  inputSchema: {
15975
15975
  query: z.string().optional().describe('keyword search across ALL advertisers (use INSTEAD of companyName/pageId)'),
15976
15976
  companyName: z.string().optional().describe('one advertiser’s ads by brand name'),
@@ -16040,7 +16040,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16040
16040
  server.registerTool('search_linkedin_ads', {
16041
16041
  _meta: openaiMeta(AD_SPY_URI, 'Searching LinkedIn ads…', 'Found LinkedIn ads'),
16042
16042
  title: 'Search LinkedIn ads',
16043
- description: "Structured LinkedIn Ad Library search by company name, keyword, or companyId — use for a targeted B2B pull; use research_ads for open-ended research. Returns compact JSON {advertiser, headline, description, cta, link, media, dates, impressions} per ad — LinkedIn is the one library exposing real impression counts. Spends ScrapeCreators credits (~1).",
16043
+ description: "Structured LinkedIn Ad Library search by company name, keyword, or companyId — use for a targeted B2B pull; use research_ads for open-ended research. Returns compact JSON {advertiser, headline, description, cta, link, media, dates, impressions} per ad — LinkedIn is the one library exposing real impression counts. Spends about a credit.",
16044
16044
  inputSchema: {
16045
16045
  company: z.string().optional().describe('advertiser company name'),
16046
16046
  keyword: z.string().optional().describe('keyword across all advertisers'),
@@ -16075,7 +16075,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16075
16075
  server.registerTool('search_tiktok', {
16076
16076
  _meta: openaiMeta(AD_SPY_URI, 'Searching TikTok videos…', 'Found TikTok videos'),
16077
16077
  title: 'Search TikTok',
16078
- description: "Organic TikTok keyword search (there is NO TikTok ad library) — top-performing videos to mine for hooks/trends/remixable creative. Returns compact JSON {desc, author, handle, plays, likes, link, cover} per video, ranked by plays. Use research_ads for open-ended research. Spends ScrapeCreators credits (~1).",
16078
+ description: "Organic TikTok keyword search (there is NO TikTok ad library) — top-performing videos to mine for hooks/trends/remixable creative. Returns compact JSON {desc, author, handle, plays, likes, link, cover} per video, ranked by plays. Use research_ads for open-ended research. Spends about a credit.",
16079
16079
  inputSchema: {
16080
16080
  query: z.string().describe('keyword or hashtag (no # needed)'),
16081
16081
  limit: z.number().int().optional().describe('max videos returned (1–25, default 8)'),
@@ -16101,7 +16101,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16101
16101
  server.registerTool('search_instagram', {
16102
16102
  _meta: openaiMeta(AD_SPY_URI, 'Searching Instagram reels…', 'Found Instagram reels'),
16103
16103
  title: 'Search Instagram',
16104
- description: "Organic Instagram REELS keyword search (/v2/instagram/reels/search — ScrapeCreators' only IG keyword surface; profile/hashtag pulls go through scrapecreators_fetch with a handle). Returns compact JSON {desc, author, handle, plays, likes, link, cover} per reel, ranked by plays. Spends ScrapeCreators credits (~1).",
16104
+ description: "Organic Instagram REELS keyword search (/v2/instagram/reels/search — our only IG keyword surface; profile/hashtag pulls go through fetch_social_data with a handle). Returns compact JSON {desc, author, handle, plays, likes, link, cover} per reel, ranked by plays. Spends about a credit.",
16105
16105
  inputSchema: {
16106
16106
  query: z.string().describe('keyword to search reels for'),
16107
16107
  limit: z.number().int().optional().describe('max reels returned (1–25, default 8)'),
@@ -16129,7 +16129,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16129
16129
  server.registerTool('search_youtube', {
16130
16130
  _meta: openaiMeta(AD_SPY_URI, 'Searching YouTube videos…', 'Found YouTube videos'),
16131
16131
  title: 'Search YouTube',
16132
- description: "Organic YouTube keyword search (/v1/youtube/search) — videos to mine for hooks/angles/long-form structure. Returns compact JSON {desc (title), author, handle, plays, link, cover} per video, ranked by views. Spends ScrapeCreators credits (~1).",
16132
+ description: "Organic YouTube keyword search (/v1/youtube/search) — videos to mine for hooks/angles/long-form structure. Returns compact JSON {desc (title), author, handle, plays, link, cover} per video, ranked by views. Spends about a credit.",
16133
16133
  inputSchema: {
16134
16134
  query: z.string().describe('keyword to search videos for'),
16135
16135
  limit: z.number().int().optional().describe('max videos returned (1–25, default 8)'),
@@ -16150,7 +16150,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16150
16150
 
16151
16151
  server.registerTool('search_reddit', {
16152
16152
  title: 'Search Reddit',
16153
- description: "Reddit keyword search (/v1/reddit/search, top-ranked) — a goldmine for the customer's OWN words (pain points, objections, language) to mine into ad hooks and copy. Returns compact JSON {desc (title+selftext), subreddit, upvotes, comments, link} per post. Spends ScrapeCreators credits (~1).",
16153
+ description: "Reddit keyword search (/v1/reddit/search, top-ranked) — a goldmine for the customer's OWN words (pain points, objections, language) to mine into ad hooks and copy. Returns compact JSON {desc (title+selftext), subreddit, upvotes, comments, link} per post. Spends about a credit.",
16154
16154
  inputSchema: {
16155
16155
  query: z.string().describe('what to search Reddit for'),
16156
16156
  limit: z.number().int().optional().describe('max posts returned (1–25, default 8)'),
@@ -16173,7 +16173,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16173
16173
  server.registerTool('search_threads', {
16174
16174
  _meta: openaiMeta(AD_SPY_URI, 'Searching Threads posts…', 'Found Threads posts'),
16175
16175
  title: 'Search Threads',
16176
- description: "Organic Threads keyword search (/v1/threads/search) — short-form text/social posts for trend + voice research. Returns compact JSON {desc, author, handle, likes, link, cover} per post. Spends ScrapeCreators credits (~1).",
16176
+ description: "Organic Threads keyword search (/v1/threads/search) — short-form text/social posts for trend + voice research. Returns compact JSON {desc, author, handle, likes, link, cover} per post. Spends about a credit.",
16177
16177
  inputSchema: {
16178
16178
  query: z.string().describe('keyword to search Threads for'),
16179
16179
  limit: z.number().int().optional().describe('max posts returned (1–25, default 8)'),
@@ -16197,11 +16197,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
16197
16197
  return adsOut('posts', all.length, all.slice(0, nAds(limit)), '', 'threads');
16198
16198
  }));
16199
16199
 
16200
- server.registerTool('scrapecreators_fetch', {
16201
- title: 'Fetch ScrapeCreators endpoint',
16202
- description: "Generic ScrapeCreators escape hatch for any ALLOWLISTED long-tail endpoint the dedicated search_* tools don't cover — e.g. {path:'/v1/instagram/profile', params:{handle:'nike'}}. Allowlisted platform families: TikTok (+ TikTok Shop), Instagram, YouTube, Facebook (organic profiles/posts/events/marketplace), LinkedIn (organic posts/companies), Twitter/X, Reddit, Threads, Snapchat, Pinterest, Twitch, Bluesky, Truth Social, Rumble, Spotify, SoundCloud, GitHub, Google search, link-in-bio pages (Linktree etc.). Param names vary per endpoint (profiles use `handle`, keyword searches use `query`, Reddit uses `subreddit`). WARNING: returns RAW provider JSON — large and messy; prefer the dedicated search_* tools. Spends ScrapeCreators credits.",
16200
+ server.registerTool('fetch_social_data', {
16201
+ title: 'Fetch social data',
16202
+ description: "Generic escape hatch for any ALLOWLISTED long-tail social/web endpoint the dedicated search_* tools don't cover — e.g. {path:'/v1/instagram/profile', params:{handle:'nike'}}. Allowlisted platform families: TikTok (+ TikTok Shop), Instagram, YouTube, Facebook (organic profiles/posts/events/marketplace), LinkedIn (organic posts/companies), Twitter/X, Reddit, Threads, Snapchat, Pinterest, Twitch, Bluesky, Truth Social, Rumble, Spotify, SoundCloud, GitHub, Google search, link-in-bio pages (Linktree etc.). Param names vary per endpoint (profiles use `handle`, keyword searches use `query`, Reddit uses `subreddit`). WARNING: returns RAW provider JSON — large and messy; prefer the dedicated search_* tools. Spends credits.",
16203
16203
  inputSchema: {
16204
- path: z.string().describe("exact SC endpoint path, e.g. '/v1/tiktok/profile' — non-allowlisted paths are rejected"),
16204
+ path: z.string().describe("exact endpoint path, e.g. '/v1/tiktok/profile' — non-allowlisted paths are rejected"),
16205
16205
  params: z.object({}).passthrough().optional().describe("endpoint query params, e.g. {handle:'nike'}"),
16206
16206
  },
16207
16207
  outputSchema: {}, // deliberately empty — the raw provider payload (any shape, can be huge) stays in the text
@@ -16234,7 +16234,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16234
16234
 
16235
16235
  server.registerTool('draft_brand', {
16236
16236
  title: 'Draft brand profile',
16237
- 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.',
16237
+ 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 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.',
16238
16238
  inputSchema: {
16239
16239
  domain: z.string().optional().describe('a website to scrape'),
16240
16240
  description: z.string().optional().describe('a free-text brand description (no website)'),
@@ -16337,11 +16337,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
16337
16337
  return ok(`Asset: ${absolute}\nDownload: ${dl}`, { url: absolute, downloadUrl: dl });
16338
16338
  }));
16339
16339
 
16340
- // ---------- post-production & analysis (Higgsfield-parity wave: each wraps an EXISTING worker/route) ----------
16340
+ // ---------- post-production & analysis (each wraps an EXISTING worker/route) ----------
16341
16341
  server.group('create');
16342
16342
  server.registerTool('analyze_video', {
16343
16343
  title: 'Analyze video',
16344
- description: "Break a video ad down into its structure: the verbatim transcript (voiceover + on-screen text) with a beat list, plus duration and sampled frame timestamps. Use to study a reference/competitor ad before remixing its structure. Costs ~a transcription call; no ScrapeCreators credits.",
16344
+ description: "Break a video ad down into its structure: the verbatim transcript (voiceover + on-screen text) with a beat list, plus duration and sampled frame timestamps. Use to study a reference/competitor ad before remixing its structure. Costs ~a transcription call.",
16345
16345
  inputSchema: { url: z.string().describe('the video URL (a served /generated/ path or a public http(s) video)') },
16346
16346
  outputSchema: {
16347
16347
  durationSeconds: z.number().optional().describe('the video length in seconds'),
@@ -16518,7 +16518,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16518
16518
 
16519
16519
  server.registerTool('competitor_teardown', {
16520
16520
  title: 'Competitor teardown',
16521
- description: "Tear a competitor's ad strategy down into an actionable playbook: their opening-hook MIX, longest-running campaign THEMES, the WHITE SPACE nobody in their set runs, 2-3 render-ready COUNTER-PLAYS, and the territories they own that you should avoid. Pass `competitor` {name, domain?}. CONTRACT: supply `ads` (raw ad objects from a prior pull_competitor_ads / search_meta_ads call) to tear exactly those down, OR omit `ads` and this pulls the competitor's real Meta ads first (spends ~1-2 ScrapeCreators credits, longest-running = proven winners). Auto-tailors the white space + counter-plays to YOUR saved brand. Spends LLM tokens (0 SC credits when you pass ads).",
16521
+ description: "Tear a competitor's ad strategy down into an actionable playbook: their opening-hook MIX, longest-running campaign THEMES, the WHITE SPACE nobody in their set runs, 2-3 render-ready COUNTER-PLAYS, and the territories they own that you should avoid. Pass `competitor` {name, domain?}. CONTRACT: supply `ads` (raw ad objects from a prior pull_competitor_ads / search_meta_ads call) to tear exactly those down, OR omit `ads` and this pulls the competitor's real Meta ads first (spends a credit or two, longest-running = proven winners). Auto-tailors the white space + counter-plays to YOUR saved brand. Spends credits (free when you pass ads).",
16522
16522
  inputSchema: {
16523
16523
  competitor: z.object({ name: z.string().describe('the competitor brand name'), domain: z.string().optional().describe('their domain — sharpens the auto-pull page match') }).describe('the competitor to tear down'),
16524
16524
  ads: z.array(z.object({}).passthrough()).optional().describe('ad objects to tear down (from pull_competitor_ads / search_meta_ads). Omit to auto-pull their Meta ads first.'),
@@ -16600,7 +16600,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16600
16600
 
16601
16601
  server.registerTool('mine_angles', {
16602
16602
  title: 'Mine customer angles',
16603
- description: "Mine ad ANGLES from real customer language: gathers the customer's own words (Reddit, TikTok, the brand's review page + review-site results) and returns a RANKED angle bank — each angle tagged (pain / outcome / identity / fear / competitive-displacement / social-proof / contrast), 2-5 VERBATIM proof quotes, a 0-100 score with breakdown, and a ready-to-run hook in the customer's own voice. Reads YOUR saved brand (pass brandId to target a specific brand — that switches this key's active brand like use_brand). To tear down a COMPETITOR use competitor_teardown instead. Spends a few ScrapeCreators credits + LLM tokens.",
16603
+ description: "Mine ad ANGLES from real customer language: gathers the customer's own words (Reddit, TikTok, the brand's review page + review-site results) and returns a RANKED angle bank — each angle tagged (pain / outcome / identity / fear / competitive-displacement / social-proof / contrast), 2-5 VERBATIM proof quotes, a 0-100 score with breakdown, and a ready-to-run hook in the customer's own voice. Reads YOUR saved brand (pass brandId to target a specific brand — that switches this key's active brand like use_brand). To tear down a COMPETITOR use competitor_teardown instead. Spends a few credits.",
16604
16604
  inputSchema: {
16605
16605
  brandId: z.string().optional().describe('a brand id/name from list_brands to mine for; omit to use the active brand'),
16606
16606
  },
@@ -16760,7 +16760,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16760
16760
  inputSchema: {
16761
16761
  channel: z.string().optional().describe('restrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, reddit, pinterest)'),
16762
16762
  authentic: z.boolean().optional().describe('true if the planned ad is an authentic/UGC/creator-register render — on-screen-text hooks are then reported unusable, with the reason'),
16763
- category: z.string().optional().describe("the product category (e.g. 'skincare serum', 'protein powder', 'sunglasses') — returns the setting Higgsfield's Location x Tier matrix puts that category in, with the reason"),
16763
+ category: z.string().optional().describe("the product category (e.g. 'skincare serum', 'protein powder', 'sunglasses') — returns the setting our Location x Tier matrix puts that category in, with the reason"),
16764
16764
  tier: z.enum(['luxury', 'premium', 'drugstore']).optional().describe('product tier, used with category — changes the FINISH of the room, never the room. Default premium.'),
16765
16765
  },
16766
16766
  outputSchema: { hooks: z.array(z.any()).optional(), settings: z.array(z.any()).optional(), patterns: z.array(z.any()).optional(), patternRule: z.string().optional(), suggestedSetting: z.any().optional(), evidence: z.any().optional(), ranked: z.any().optional() },
@@ -126,7 +126,7 @@ export function wellFormedValue(v) {
126
126
  * the thing that manufactures lone surrogates in the first place.
127
127
  *
128
128
  * `.slice(0, n)` counts code units, so it cuts an astral character in half whenever the boundary lands mid-pair.
129
- * Measured on live vendor text: 14,579 ScrapeCreators strings held 570 real PAIRS and ZERO lone surrogates — the
129
+ * Measured on live vendor text: 14,579 upstream strings held 570 real PAIRS and ZERO lone surrogates — the
130
130
  * vendor is not emitting broken text, our own truncation is creating it, at 1 caption length in every 21.
131
131
  *
132
132
  * THE LENGTH CONTRACT IS PRESERVED: the result is never LONGER than `.slice(0, n)` would be — at worst one code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.161",
3
+ "version": "0.1.162",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 718 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",
@@ -14,7 +14,7 @@ allowed-tools: Bash
14
14
  Drive the **Hermoso CLI** to go from a brand to a finished ad in three steps. Report the final media URL.
15
15
 
16
16
  ## Setup
17
- - `hermoso version` to confirm the CLI; `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**..
17
+ - `hermoso version` to confirm the CLI; `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**.. No account at all? An agent can sign itself up on a paid plan with `POST /v1/signup` at app.hermoso.ai, no browser needed; see the Hermoso README.
18
18
 
19
19
  ## Procedure
20
20
  1. **Onboard the brand** (skip if the user already gave full brand details):
@@ -16,7 +16,7 @@ You drive the **Hermoso CLI** (`hermoso`) to render images and videos. Always re
16
16
 
17
17
  ## Setup (once)
18
18
  1. Ensure the CLI is available. From the Hermoso repo: `node bin/hermoso.mjs version` (or `hermoso version` if globally installed via `npm i -g`).
19
- 2. `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**.
19
+ 2. `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**. No account at all? An agent can sign itself up on a paid plan with `POST /v1/signup` at app.hermoso.ai, no browser needed; see the Hermoso README.
20
20
 
21
21
  ## Procedure
22
22
  1. **Always run `hermoso capabilities` first.** It lists the valid image/video **model ids**, their credit costs, aspect ratios, video durations, and the recipe ids. Never guess a model id.
@@ -15,7 +15,7 @@ Use Hermoso's reference-image compositing so the real product (label, colours, s
15
15
  scene around it is generated. Drive the **Hermoso CLI**.
16
16
 
17
17
  ## Setup
18
- - `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**.; run `hermoso capabilities` once to see image model ids + recipes.
18
+ - `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**.; run `hermoso capabilities` once to see image model ids + recipes. No account at all? An agent can sign itself up on a paid plan with `POST /v1/signup` at app.hermoso.ai, no browser needed; see the Hermoso README.
19
19
 
20
20
  ## Procedure
21
21
  1. Get the **product image** path/URL from the user. This is the `--ref` — it forces product-accurate compositing.
@@ -14,7 +14,7 @@ allowed-tools: Bash
14
14
  This is Hermoso's discovery half (which most generators don't have). Drive the **Hermoso CLI**.
15
15
 
16
16
  ## Setup
17
- - `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**..
17
+ - `hermoso auth login` (opens your browser once; nothing to paste). On a machine with no browser: `hermoso auth login --token <your key>`, using a key from the app under **MCP & CLI**.. No account at all? An agent can sign itself up on a paid plan with `POST /v1/signup` at app.hermoso.ai, no browser needed; see the Hermoso README.
18
18
 
19
19
  ## Procedure
20
20
  Pick the tool that fits the ask:
@@ -27,5 +27,5 @@ Pick the tool that fits the ask:
27
27
  4. **Synthesize**: report the strongest hooks, angles, formats, and what's worth copying — be specific (quote the actual headlines/angles). If the user then wants to build one, hand off to `hermoso-ad-from-brand` / `hermoso-generate`.
28
28
 
29
29
  ## Notes
30
- - Research spends ScrapeCreators credits (ad-library calls) + LLM tokens; keep platform scope to what's asked.
30
+ - Research spends credits (ad-library calls); keep platform scope to what's asked.
31
31
  - Add `--json` for the raw ad objects (URLs, copy, run dates) when the user wants the data, not a summary.