hermoso 0.1.539 → 0.1.542

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
@@ -133,8 +133,9 @@ just download it. Use the one piece you need, or all of it together.
133
133
  ## Install in one command
134
134
 
135
135
  This repo is a plugin marketplace, a Gemini CLI extension and a skills package at once, so a coding agent takes
136
- Hermoso in one line. Every install brings the same six skills (`hermoso-research`, `hermoso-generate`,
137
- `hermoso-ad-from-brand`, `hermoso-product-photoshoot`, `hermoso-marketing`, `hermoso-edit-timeline`), and the skills drive the `hermoso`
136
+ Hermoso in one line. Every install brings the same seven skills (`hermoso-research`, `hermoso-generate`,
137
+ `hermoso-ad-from-brand`, `hermoso-product-photoshoot`, `hermoso-marketing`, `hermoso-edit-timeline`,
138
+ `hermoso-launch-app`), and the skills drive the `hermoso`
138
139
  CLI through `npx`. No
139
140
  tool list is loaded into your session: a CLI command costs nothing until it runs, and it reaches every tool.
140
141
  The first time, your agent runs `npx -y hermoso auth login`, which opens a browser to sign in.
@@ -286,7 +287,7 @@ Connect, approve with your Hermoso account, and you are done: the full toolset w
286
287
  1. **Get an account** at [app.hermoso.ai](https://app.hermoso.ai). The free tier is included; plans and credits
287
288
  are the same ones the web Studio uses. Or skip the browser entirely and let your agent sign itself up on a paid
288
289
  plan with `POST /v1/signup` (above).
289
- 2. **Install the plugin.** It adds the six Hermoso skills, which drive the `hermoso` CLI through `npx`:
290
+ 2. **Install the plugin.** It adds the seven Hermoso skills, which drive the `hermoso` CLI through `npx`:
290
291
 
291
292
  ```bash
292
293
  claude plugin marketplace add hermoso-ai/hermoso && claude plugin install hermoso@hermoso
@@ -514,7 +515,7 @@ the package, so they need no key, no network and no sign-in.
514
515
 
515
516
  ## 3. Skills: what a coding agent installs
516
517
 
517
- `skills/` holds six installable skills:
518
+ `skills/` holds seven installable skills:
518
519
 
519
520
  | Skill | What it does |
520
521
  | --- | --- |
@@ -524,6 +525,7 @@ the package, so they need no key, no network and no sign-in.
524
525
  | `hermoso-product-photoshoot` | A real product photo composited into studio, lifestyle or hero scenes |
525
526
  | `hermoso-marketing` | The whole loop: research, create, publish and schedule, paid campaigns, measure |
526
527
  | `hermoso-edit-timeline` | The manual for composing any video edit or transition with `edit_timeline` |
528
+ | `hermoso-launch-app` | Launch and market an app or site you just built: brand, launch ads, a week of scheduled posts |
527
529
 
528
530
  They are what every one-command install at the top of this page adds, in Claude Code, Codex, Gemini CLI and
529
531
  through `npx skills add`. From a clone, copying works too:
package/mcp/http.mjs CHANGED
@@ -599,7 +599,7 @@ const inflightNameOf = (body) => { const msgs = Array.isArray(body) ? body : [bo
599
599
  const baseInstructions = instructionsFor(scope, { widgetHost });
600
600
  let firstRun = null;
601
601
  if (typeof firstRunFor === 'function' && user.accountId) {
602
- try { firstRun = await Promise.race([Promise.resolve(firstRunFor({ accountId: user.accountId })).catch(() => null), new Promise((r) => { const t = setTimeout(() => r(null), 1500); if (t && typeof t.unref === 'function') t.unref(); })]); } catch { firstRun = null; }
602
+ try { firstRun = await Promise.race([Promise.resolve(firstRunFor({ accountId: user.accountId, brandKey: user.brandKey === true })).catch(() => null), new Promise((r) => { const t = setTimeout(() => r(null), 1500); if (t && typeof t.unref === 'function') t.unref(); })]); } catch { firstRun = null; }
603
603
  }
604
604
  firstRun = firstRun && typeof firstRun === 'object' && firstRun.fresh === true ? { ...firstRun, offer: firstRunOffers(baseInstructions) } : null;
605
605
  const server = new McpServer({ name: 'hermoso', version: PKG_VERSION }, { instructions: withFirstRunLine(baseInstructions, firstRun) });
package/mcp/tools.mjs CHANGED
@@ -674,7 +674,13 @@ export function noteBrandOf(tool, args) {
674
674
  for (const k of HERMOSO_BRAND_KEYS.get(tool) || []) { const v = args?.[k]; if (typeof v === 'string' && v.trim()) return v.trim().slice(0, 120); }
675
675
  return '';
676
676
  }
677
- export async function withConnectPrompt(tool, r, post = apiPost, brand = '') {
677
+ // …AND AT MOST ONE NEXT STEP, WHICHEVER IT IS (2026-11-14, lib/agent-nudge.mjs). `nudge: 1` tells the server this twin reads
678
+ // `next` off the answer: the one step it chose for this account (save a brand first, then connect a channel), with the
679
+ // sentence to say, each offered at most once in seven days and never once done. The server words the text, the step and
680
+ // the hint, so a step added later needs no new twin; a server that predates this answers the connect prompt as before.
681
+ // `raw` marks a call on the raw model lane, which gets no note of ours about a brand; `asked` says this session's
682
+ // first-run note already asked for their website, so the same session is not asked twice.
683
+ export async function withConnectPrompt(tool, r, post = apiPost, brand = '', opts = {}) {
678
684
  try {
679
685
  const kind = finishedRenderKind(tool, r);
680
686
  if (!kind) return r;
@@ -683,10 +689,16 @@ export async function withConnectPrompt(tool, r, post = apiPost, brand = '') {
683
689
  const sc = r.structuredContent || {};
684
690
  const job = String(sc.jobId || sc.id || '').slice(0, 80);
685
691
  const client = store ? String(store.client || '') : 'hermoso';
686
- const d = await Promise.race([Promise.resolve(post('/api/mcp/connect-prompt', { tool, kind, job, client, ...(brand ? { brand } : {}) })).catch(() => null), new Promise((res) => { const t = setTimeout(() => res(null), CONNECT_PROMPT_WAIT_MS); if (t && typeof t.unref === 'function') t.unref(); })]);
692
+ const d = await Promise.race([Promise.resolve(post('/api/mcp/connect-prompt', { tool, kind, job, client, nudge: 1, ...(opts && opts.raw ? { raw: true } : {}), ...(opts && opts.asked ? { asked: true } : {}), ...(brand ? { brand } : {}) })).catch(() => null), new Promise((res) => { const t = setTimeout(() => res(null), CONNECT_PROMPT_WAIT_MS); if (t && typeof t.unref === 'function') t.unref(); })]);
687
693
  if (!d || d.show !== true || typeof d.text !== 'string' || !d.text) return r;
688
- const out = { ...r, content: [...r.content, { type: 'text', text: d.text }], structuredContent: { ...sc, connectPrompt: { channels: Array.isArray(d.channels) ? d.channels : [] } } };
689
- return withHints(out, [...hintsOf(r), { do: 'offer to post this; hand the user one of the connect links in this reply', why: 'no publishing channel is connected, and connecting needs a browser sign-in on the platform' }]);
694
+ const next = d.next && typeof d.next === 'object' && typeof d.next.step === 'string' && d.next.step ? d.next : null;
695
+ const hint = d.hint && typeof d.hint === 'object' && typeof d.hint.do === 'string' && d.hint.do ? d.hint : null;
696
+ if (next && next.step !== 'connect_channel') {
697
+ const out = { ...r, content: [...r.content, { type: 'text', text: d.text }], structuredContent: { ...sc, next } };
698
+ return hint ? withHints(out, [...hintsOf(r), hint]) : out;
699
+ }
700
+ const out = { ...r, content: [...r.content, { type: 'text', text: d.text }], structuredContent: { ...sc, connectPrompt: { channels: Array.isArray(d.channels) ? d.channels : [] }, ...(next ? { next } : {}) } };
701
+ return withHints(out, [...hintsOf(r), hint || { do: 'offer to post this; hand the user one of the connect links in this reply', why: 'no publishing channel is connected, and connecting needs a browser sign-in on the platform' }]);
690
702
  } catch { return r; }
691
703
  }
692
704
  // THE WEEK-PLAN NOTE (2026-11-02, lib/week-plan.mjs): the offer after a first connection, and "your week is finished". The
@@ -699,6 +711,7 @@ const WEEK_PLAN_NOTE_SKIP = new Set(['plan_week', 'get_job', 'list_jobs']);
699
711
  export async function withWeekPlanNote(tool, r, post = apiPost, brand = '') {
700
712
  try {
701
713
  if (!r || r.isError || !Array.isArray(r.content) || WEEK_PLAN_NOTE_SKIP.has(tool)) return r;
714
+ if (r.structuredContent && r.structuredContent.next && typeof r.structuredContent.next.step === 'string') return r; // one next step a reply (a paging cursor called `next` is not one): this note is not asked for, so it is not spent, and the next reply carries it
702
715
  const store = mcpCtx.getStore();
703
716
  if (store && (store.connectPromptQuiet || (store.remote && store.client === undefined))) return r;
704
717
  const client = store ? String(store.client || '') : 'hermoso';
@@ -872,6 +885,7 @@ const wrap = (fn) => {
872
885
  _fr.said = true;
873
886
  const _n = ((_st.apiCharged || 0) + (_st.jobCharged || 0)) > 0 ? null : firstRunNote(_tool, _fr, r);
874
887
  if (_n) {
888
+ if (/draft_brand/.test(String(_n.hint?.do || ''))) _fr.brandAsked = true; // this session has asked for their website: its next finished creation does not ask again (withConnectPrompt `asked`)
875
889
  const _last = [...r.content].reverse().find((b) => b && b.type === 'text' && typeof b.text === 'string');
876
890
  if (_last) _last.text += `\n\n${_n.text}`; else r.content.push({ type: 'text', text: _n.text });
877
891
  return noPitchOnWidget(withHints(r, [_n.hint]));
@@ -879,7 +893,7 @@ const wrap = (fn) => {
879
893
  }
880
894
  } catch {}
881
895
  const _noteBrand = noteBrandOf(_tool, args); // the brand this call acted on, so both notes are about it
882
- return noPitchOnWidget(await withWeekPlanNote(_tool, await withConnectPrompt(_tool, withWidgetResultNote(_tool, r, hostRendersWidgets()), apiPost, _noteBrand), apiPost, _noteBrand));
896
+ return noPitchOnWidget(await withWeekPlanNote(_tool, await withConnectPrompt(_tool, withWidgetResultNote(_tool, r, hostRendersWidgets()), apiPost, _noteBrand, { raw: args?.raw === true, asked: mcpCtx.getStore()?.firstRun?.brandAsked === true }), apiPost, _noteBrand));
883
897
  }
884
898
  catch (e) {
885
899
  try { recordToolOutcome(_tool, { ok: false, ms: Date.now() - _t0 }); } catch {}
@@ -6636,6 +6650,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
6636
6650
  // ZERO BRANDS IS A REAL STATE (2026-10-01): the owner may delete their last brand. Say so, and say how to make one,
6637
6651
  // rather than printing an empty list under a heading that implies there should be something in it.
6638
6652
  if (!(d.brands || []).length) return ok(`This account has no profiles yet (the last one was deleted, or none was ever made). create_brand makes one, and draft_brand fills it in from a website, a description or a social handle.${sharedTxt}`, { ...d, sharedWorkspaces: shared });
6653
+ // A KEY LIMITED TO ONE BRAND lists that brand and says so (the server filters the roster and sends the sentence),
6654
+ // so an agent does not go looking for profiles it was never given.
6655
+ if (d.limited && d.limited.sentence) return ok(`${d.limited.sentence} It is the only profile this connection can see or act on:\n${lines}${sharedTxt}`, { ...d, sharedWorkspaces: shared });
6639
6656
  return ok(`Profiles on this account:\n${lines}\n\nSwitch with use_brand.${sharedTxt}`, { ...d, sharedWorkspaces: shared });
6640
6657
  }));
6641
6658
 
@@ -6685,6 +6702,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
6685
6702
  return ok(`Now acting on ${sh.name || 'that shared profile'}${sh.ownerName ? ` (${sh.ownerName}'s profile)` : ''} (profile ${sh.profileUuid}). Its brand details, memory, Library and CONNECTED ACCOUNTS are what every tool sees from here; switch back by naming one of your own profiles.`,
6686
6703
  { ok: true, brand: { id: sh.profileUuid, name: sh.name || null, ownerAccountId: sh.ownerAccountId, role: sh.role || 'member' }, shared: true });
6687
6704
  }
6705
+ // A key limited to one brand cannot switch, and says so in the server's own sentence instead of "no such profile".
6706
+ if (d.limited && d.limited.sentence) return { content: [{ type: 'text', text: `${d.limited.sentence} It cannot switch to another brand.` }], isError: true };
6688
6707
  const own = (d.brands || []).map(b => `• ${b.name} (id: ${b.id})`).join('\n');
6689
6708
  const shr = shared.map(w => `• ${w.name || 'Shared profile'}${w.ownerName ? ` — ${w.ownerName}` : ''} (id: ${w.profileUuid}, shared with you)`).join('\n');
6690
6709
  if (!own && !shr) return { content: [{ type: 'text', text: `No profile matching "${brand}": this account has no profiles yet. create_brand makes one.` }], isError: true }; // zero brands is a real state (2026-10-01)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.539",
3
+ "version": "0.1.542",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI marketing agent and AI ad generator, run from your own AI agent. 1233 tools. No markup on models on Starter and up. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on any plan; Free includes 5 posts a month and every paid plan posts without a limit. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram, WordPress, Discord, Slack, Whop and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
6
6
  "type": "module",
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: hermoso-launch-app
3
+ description: >-
4
+ Launch and market an app or site you just built, using Hermoso. Sets up the brand from the app, makes launch
5
+ ads and posts, schedules them on the owner's own social channels and, if asked, builds paused ad campaigns.
6
+ Use when the user says "launch this", "market my app", "get me users", "make launch posts", "promote what we
7
+ just built", or finishes a build in an app builder (Lovable, Replit, Bolt, v0, Base44) and asks what is next.
8
+ NOT for: building the app itself, or a single image from a prompt (use hermoso-generate).
9
+ argument-hint: "[the app's URL or one line about what it does, e.g. 'https://myapp.com, a habit tracker for runners']"
10
+ ---
11
+
12
+ # Hermoso: launch the app you just built
13
+
14
+ You built something. This gets it in front of people: a saved brand, launch creative, a week of scheduled posts
15
+ and, only if the user asks, paid campaigns created paused.
16
+
17
+ ## Before you start
18
+ - Hermoso has to be connected to this host as an MCP server (URL `https://app.hermoso.ai/mcp`, sign in once).
19
+ If its tools are not available, send the user to https://hermoso.ai/connect/ and pick their host
20
+ (Lovable, Replit, Bolt, v0 and Base44 each have a page with the exact clicks).
21
+ - In a terminal there is no MCP server to connect: the same tools run through the Hermoso CLI. Sign in once with
22
+ `npx -y hermoso auth login` (opens a browser; nothing to paste). On a machine with no browser:
23
+ `npx -y hermoso auth login --token <your key>`, using a key from app.hermoso.ai under **MCP & CLI**. Then
24
+ `hermoso tools --search <what you want>` finds a tool, `hermoso tools <name>` prints its arguments and
25
+ `hermoso call <name> --json '{...}'` runs it (after `npm install -g hermoso`, or prefix each with `npx -y`).
26
+ No account at all? An agent can sign itself up on a paid plan with `POST /v1/signup` at app.hermoso.ai; see
27
+ the Hermoso README.
28
+ - A tool you do not see in your list still exists: `find_tools` searches every Hermoso tool and `call_tool`
29
+ runs any of them by name.
30
+ - The connector works while you build. It is not part of the published app, so never wire Hermoso calls into
31
+ the app's own code unless the user asks for an API integration.
32
+
33
+ ## The launch, in order
34
+ Do only the steps the user wants. Say what you are about to do before anything that spends credits or posts.
35
+
36
+ 1. **Know the product.** Use what you already know from the build: what the app does, who it is for, its name,
37
+ its live URL. Ask only for what is missing. One line is enough.
38
+ 2. **Save the brand.** If the app has a public URL, call `draft_brand` with it so every ad matches the app's
39
+ look and voice. No URL yet? Call `draft_brand` with a short description instead. `get_brand` shows what is
40
+ already saved; do not draft twice.
41
+ 3. **See what works in this market (optional, costs credits).** `find_competitors` then `research_ads`, and
42
+ report the two or three hooks that keep showing up. Skip this if the user just wants posts.
43
+ 4. **Make the launch creative.** Three is a good first set: a static launch ad, a carousel or second static
44
+ with a different angle, and one short vertical video if the user wants video. `generate_image` and
45
+ `render_ad` pick sound defaults when you do not name a model. Video takes a few minutes: read the job with
46
+ `get_job` and never call it done before it finishes. Show the user each result.
47
+ 5. **Check the channels.** `list_connectors` says which social accounts are connected. None connected is not
48
+ an error: give the user the connect link from the tool result, or tell them to open Hermoso and connect
49
+ the channels they want under Connectors. Do not post anywhere the user did not name.
50
+ 6. **Schedule the week.** Show the exact caption and the exact account for each post and get a yes. Then
51
+ `schedule_post`, one a day, at the times the user picks or mornings in their time zone. `list_scheduled`
52
+ reads the queue back. Never rewrite or shorten a caption the user wrote.
53
+ 7. **Paid ads, only if asked.** Campaigns are created paused on the user's own ad accounts and read back
54
+ before you describe them. Turning one on is the user's decision, stated in their own words.
55
+ 8. **Report.** What was made (with links), what is scheduled and when, what is waiting on the user (a channel
56
+ to connect, a campaign to turn on), and the one thing to do next week: read what performed with
57
+ `post_performance` and make more of that.
58
+
59
+ ## Rules
60
+ - Nothing is published, scheduled or spent on ads without the user saying so in this conversation.
61
+ - If the free credits run out, say so plainly and stop. Do not swap in a cheaper version of what was asked.
62
+ - Use the app's real name, screens and URL. Do not invent features, prices, reviews or numbers for the ads.