hermoso 0.1.436 → 0.1.438
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 +2 -2
- package/mcp/client.mjs +1 -1
- package/mcp/openai-extensions.mjs +1 -1
- package/mcp/roster-scope.mjs +2 -0
- package/mcp/tool-hints.mjs +8 -0
- package/mcp/tools.mjs +408 -137
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
|
|
|
5
5
|
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
6
|
campaigns behind them, all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**928 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
9
|
catalog with exact per-render credit costs plus the full capability map.
|
|
10
10
|
|
|
11
11
|
**No markup on models.** Stop overpaying markups on AI models. Plans: [hermoso.ai/pricing](https://hermoso.ai/pricing).
|
|
@@ -242,7 +242,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
242
242
|
|
|
243
243
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
244
244
|
|
|
245
|
-
### What the
|
|
245
|
+
### What the 928 tools cover
|
|
246
246
|
|
|
247
247
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
248
248
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/client.mjs
CHANGED
|
@@ -146,7 +146,7 @@ async function unwrap(res, { read = false } = {}) {
|
|
|
146
146
|
// ledger with the tool name off x-hermoso-tool. wrap() reports ONLY the errors that lack this marker — a local
|
|
147
147
|
// throw, a schema rejection, a socket reset — which is what stops the twins double-counting every 4xx.
|
|
148
148
|
// `connectUrl` rides a not-connected 401 with the brand already in it, so a hint never rebuilds the link.
|
|
149
|
-
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}) }); // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
149
|
+
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}), ...(body?.meta?.welcomeClaim && typeof body.meta.welcomeClaim === 'object' && body.meta.welcomeClaim.url ? { welcomeClaim: { credits: Number(body.meta.welcomeClaim.credits) || 0, url: String(body.meta.welcomeClaim.url) } } : {}), ...(body?.remedy && typeof body.remedy === 'object' && typeof body.remedy.code === 'string' ? { remedy: body.remedy } : {}) }); // `remedy` (2026-10-25): the server's structured next step (a publish outcome, Meta's security hold), handed on whole to wrap()'s _meta and /v1 // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
150
150
|
}
|
|
151
151
|
// WHAT THIS CALL WAS CHARGED (2026-10-19). A paid route answers {data, creditsUsed}, and the unwrap below drops the
|
|
152
152
|
// sibling, so a synchronous paid tool's reply could never say what it cost. It is added to the running tool call's
|
|
@@ -1274,7 +1274,7 @@ const PROVIDER_INFO = {
|
|
|
1274
1274
|
openai_ads: ['ChatGPT Ads', 'ads'], apple_ads: ['Apple Ads', 'ads'], applovin_ads: ['AppLovin Ads', 'ads'],
|
|
1275
1275
|
google_drive: ['Google Drive, Sheets and Docs', 'data'], onedrive: ['OneDrive', 'data'], google_analytics: ['Google Analytics', 'data'],
|
|
1276
1276
|
search_console: ['Google Search Console', 'data'], merchant_center: ['Google Merchant Center', 'data'], slack: ['Slack', 'data'],
|
|
1277
|
-
shopify: ['Shopify', 'data'], stripe: ['Stripe', 'data'], hubspot: ['HubSpot', 'data'],
|
|
1277
|
+
shopify: ['Shopify', 'data'], stripe: ['Stripe', 'data'], hubspot: ['HubSpot', 'data'], notion: ['Notion', 'data'],
|
|
1278
1278
|
};
|
|
1279
1279
|
export const providerInfo = (p) => {
|
|
1280
1280
|
const hit = PROVIDER_INFO[p];
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -99,6 +99,8 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
99
99
|
[/intercom/, 'intercom'],
|
|
100
100
|
// Vercel (2026-10-16): the brand's OWN websites, offered only to a workspace that connected Vercel.
|
|
101
101
|
[/vercel/, 'vercel'],
|
|
102
|
+
// Notion (2026-10-25): the user's OWN Notion workspace, offered only to a workspace that connected it.
|
|
103
|
+
[/notion/, 'notion'],
|
|
102
104
|
// ── posting-only channels ──
|
|
103
105
|
[/youtube/, 'youtube'],
|
|
104
106
|
[/google_business|business_location|^list_business_(categories|attributes)$|^business_google_updated$/, 'google_business'],
|
package/mcp/tool-hints.mjs
CHANGED
|
@@ -85,6 +85,7 @@ export function videoChoiceHints(tool, choice, opts = {}) {
|
|
|
85
85
|
? { do: `tell the user the video needs ${c.short ?? '?'} more credits than the account has; they manage credits in their Hermoso account at ${ACCOUNT_INFO_URL}`, why: 'this host does not offer purchases, so nothing is bought from here' }
|
|
86
86
|
: { do: 'buy_credits({})', why: `${c.short ?? '?'} credits short of the video; a pack or a plan covers it, then the same call plans the video` },
|
|
87
87
|
];
|
|
88
|
+
if (c.welcomeClaim && c.welcomeClaim.url) out.unshift(welcomeClaimHint(c.welcomeClaim));
|
|
88
89
|
if (o.draft && o.draft.model) out.push({ do: videoChoiceDraftCall(tool, o.draft), why: `a light draft on ${o.draft.label || o.draft.model} (${o.draft.durationSeconds}s) is ~${(Number(o.draft.credits) || 0) + (Number(o.draft.planCredits) || 0)} credits and fits the balance; render the premium version after topping up` });
|
|
89
90
|
return out;
|
|
90
91
|
}
|
|
@@ -101,6 +102,7 @@ export function videoChoiceText(tool, choice, opts = {}) {
|
|
|
101
102
|
? ` 2. The user can add credits in their Hermoso account (${ACCOUNT_INFO_URL}); then the same call makes the video as asked. Nothing can be bought from here.`
|
|
102
103
|
: ` 2. Add credits: buy_credits({}) quotes a pack on a saved card or returns a checkout link${o.topup?.url ? ` (or ${o.topup.url})` : ''}; then repeat the same call and the video goes ahead as asked.`,
|
|
103
104
|
];
|
|
105
|
+
if (c.welcomeClaim && c.welcomeClaim.url) lines.splice(1, 0, ` First: ${welcomeClaimSentence(c.welcomeClaim)}`);
|
|
104
106
|
if (d) lines.push(` 3. Render the video anyway as a light draft on ${d.label || d.model} (${d.durationSeconds}s, ~${(Number(d.credits) || 0) + (Number(d.planCredits) || 0)} credits${d.audio === false ? '; SILENT: no voice, dialogue or music, so tell the user before they pick it for a spoken ad' : ''}): ${videoChoiceDraftCall(tool, d)}.${opts.widgetHost ? '' : ' Premium models once they top up.'}`);
|
|
105
107
|
else lines.push(` (No light-model draft fits this balance, so there is no "render anyway" option here.)`);
|
|
106
108
|
return lines.join('\n');
|
|
@@ -116,6 +118,12 @@ export function videoChoiceText(tool, choice, opts = {}) {
|
|
|
116
118
|
export const ACCOUNT_INFO_URL = 'https://hermoso.ai/pricing';
|
|
117
119
|
export const CREDITS_LOW_NOTE = `The Hermoso account's credit balance is too low for this. Tell the user plainly; they manage their account and credits at ${ACCOUNT_INFO_URL}. Nothing can be bought from here.`;
|
|
118
120
|
const UPSELL_SENTENCE_RE = /\s*(?:please\s+)?(?:upgrade(?:\s+your\s+plan)?\s+or\s+top\s+up|top\s+up|add\s+credits|buy\s+(?:more\s+)?credits|monthly\s+plans\s+have\s+no\s+markup\s+on\s+models)\b[^.!\n]*[.!]?/gi; // the monthly-plan upsell (2026-10-20) is a promotion too: a widget host (ChatGPT) never hears it
|
|
121
|
+
// ── FREE CREDITS NOT CLAIMED YET (2026-10-25, ChatGPT funnel) ─────────────────────────────────────────────────────
|
|
122
|
+
// An account whose welcome credits were never added (the one-time check that a person is behind it did not pass
|
|
123
|
+
// silently at sign-in) reads 0 credits. Its fix is opening the app once, signed in, which is not a purchase, so it is
|
|
124
|
+
// said on every host, ChatGPT included. The server marks the refusal (meta.welcomeClaim = {credits, url}).
|
|
125
|
+
export const welcomeClaimSentence = (w) => `This account has not received its ${Number(w?.credits) || 0} free credits yet. The user opens ${w?.url} once in a browser, signed in with the same account, and they arrive in a few seconds after a quick check that a person is there (no card).`;
|
|
126
|
+
export const welcomeClaimHint = (w) => ({ do: `ask the user to open ${w?.url} once in a browser, signed in with the same account, to receive the ${Number(w?.credits) || 0} free credits; then run the same call again`, why: 'the account never received its free credits: they wait behind a one-time check that a person is behind the account, which runs in the browser' });
|
|
119
127
|
export function neutralCreditsText(msg) {
|
|
120
128
|
const base = String(msg ?? '').replace(UPSELL_SENTENCE_RE, '').replace(/[ \t]+$/gm, '').replace(/\s*[—–,:-]\s*$/gm, '.').trim();
|
|
121
129
|
return `${base}${base ? '\n' : ''}${CREDITS_LOW_NOTE}`;
|