hermoso 0.1.431 → 0.1.434
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/mcp/tool-hints.mjs +1 -1
- package/mcp/tools.mjs +37 -21
- package/package.json +1 -1
package/mcp/tool-hints.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export function videoChoiceText(tool, choice, opts = {}) {
|
|
|
115
115
|
// The link is the pricing page, which describes the options and starts nothing. Pure, no imports — the twin rule.
|
|
116
116
|
export const ACCOUNT_INFO_URL = 'https://hermoso.ai/pricing';
|
|
117
117
|
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
|
-
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)\b[^.!\n]*[.!]?/gi;
|
|
118
|
+
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
|
|
119
119
|
export function neutralCreditsText(msg) {
|
|
120
120
|
const base = String(msg ?? '').replace(UPSELL_SENTENCE_RE, '').replace(/[ \t]+$/gm, '').replace(/\s*[—–,:-]\s*$/gm, '.').trim();
|
|
121
121
|
return `${base}${base ? '\n' : ''}${CREDITS_LOW_NOTE}`;
|
package/mcp/tools.mjs
CHANGED
|
@@ -565,7 +565,7 @@ const wrap = (fn) => {
|
|
|
565
565
|
const _widget = hostRendersWidgets();
|
|
566
566
|
if (e?.videoChoice && typeof e.videoChoice === 'object') { msg = 'Error: ' + videoChoiceText(_tool, e.videoChoice, { widgetHost: _widget }); _hints.push(...videoChoiceHints(_tool, e.videoChoice, { widgetHost: _widget })); }
|
|
567
567
|
else if (_widget && /not enough credits|out of credits|needs (a paid plan|the Pro plan)/i.test(msg)) { msg = neutralCreditsText(msg); _hints.push(...creditsLowHints()); }
|
|
568
|
-
else if (/not enough credits|out of credits|needs (a paid plan|the Pro plan)/i.test(msg)) _hints.push({ do: 'buy_credits({})', why: 'this account cannot cover the call; buy_credits quotes on a saved card or returns a checkout link, and billing_status shows the balance and the billing role' }), msg += `\nRun buy_credits to top up (credit packs): with a saved card it quotes (quoteToken included) then one-click charges on confirm:true + quote_token; with no card yet it returns a checkout link your human pays once (the card saves for one-click after). billing_status shows your balance, plan + billing role; if you're an admin, upgrade_plan moves to a bigger monthly plan (a person pays on Stripe). hermoso_credits shows the balance; hermoso_capabilities lists per-model credit costs
|
|
568
|
+
else if (/not enough credits|out of credits|needs (a paid plan|the Pro plan)/i.test(msg)) _hints.push({ do: 'buy_credits({})', why: 'this account cannot cover the call; buy_credits quotes on a saved card or returns a checkout link, and billing_status shows the balance and the billing role' }), msg += `\nRun buy_credits to top up (credit packs): with a saved card it quotes (quoteToken included) then one-click charges on confirm:true + quote_token; with no card yet it returns a checkout link your human pays once (the card saves for one-click after). billing_status shows your balance, plan + billing role; if you're an admin, upgrade_plan moves to a bigger monthly plan (a person pays on Stripe). hermoso_credits shows the balance; hermoso_capabilities lists per-model credit costs.${/no markup on models/i.test(msg) ? ' If the user wants credits to go further, upgrade_plan moves them to a monthly plan with no markup on models (a person pays on Stripe).' : ''}`;
|
|
569
569
|
// connector not connected → hand the human a ONE-CLICK connect link (OAuth needs a browser, so it can't happen
|
|
570
570
|
// in-agent) — 2026-07-23. Detected from the STRUCTURED signal, never from the prose (see notConnectedHint).
|
|
571
571
|
else {
|
|
@@ -2340,7 +2340,15 @@ export function parseToolScope(raw) {
|
|
|
2340
2340
|
// read their descriptions as claiming what the tool cannot do (2026-10-18). x_post_metrics answers there.
|
|
2341
2341
|
// fetch_social_data is a generic proxy over allowlisted vendor paths, which OpenAI's scan held as operations "not
|
|
2342
2342
|
// individually exposed" (2026-10-18); see WIDGET_HOST_SPLITS for why it is withheld rather than split.
|
|
2343
|
-
|
|
2343
|
+
// THE TOOLS THAT MOVE MONEY JOINED THEM 2026-10-19, for the plugin submission's attestation that the plugin "does not
|
|
2344
|
+
// initiate or execute money transfers, crypto transfers, or investment trades on behalf of users": a Stripe refund
|
|
2345
|
+
// pays money out, a Stripe subscription charges the customer's saved card, cancelling one ends a paid plan, and a
|
|
2346
|
+
// payment link exists to collect money. Ad budgets are NOT here: a budget is a spend setting on the user's own ad
|
|
2347
|
+
// account, not a transfer (an Apple budget order is a cap and spends nothing). The other Stripe writes (product, price,
|
|
2348
|
+
// coupon, customer) create records and move nothing; tools/chatgpt-no-money-movement-check.mjs makes a NEW Stripe
|
|
2349
|
+
// write fail until somebody classifies it.
|
|
2350
|
+
export const WIDGET_MONEY_MOVEMENT_TOOLS = Object.freeze(['refund_stripe_charge', 'create_stripe_subscription', 'cancel_stripe_subscription', 'create_stripe_payment_link']);
|
|
2351
|
+
export const WITHHELD_FROM_WIDGET_HOSTS = new Set(['buy_credits', 'upgrade_plan', 'set_auto_reload', 'billing_status', 'delete_brand', 'connect_connector', 'x_post_insights', 'x_post_insights_historical', 'fetch_social_data', ...WIDGET_MONEY_MOVEMENT_TOOLS]);
|
|
2344
2352
|
// The withheld tools whose refusal is the generic-executor sentence rather than the commerce one.
|
|
2345
2353
|
const WIDGET_GENERIC_PROXIES = new Set(['fetch_social_data']);
|
|
2346
2354
|
const executorLike = (name) => EXECUTOR_TOOLS.has(name) || WIDGET_GENERIC_PROXIES.has(name);
|
|
@@ -4401,12 +4409,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4401
4409
|
const _rmLine = _rm ? `\nRemake engines (recast_motion, restyle_video, edit_video engine): auto picks, for a clip with no real-looking person, ${Object.entries(_rm.defaults || {}).map(([k, v]) => `${k} → ${v}`).join(', ')}; with a person in the clip, ${Object.entries(_rm.faceDefaults || {}).map(([k, v]) => `${k} → ${v}`).join(', ')}${_rm.personClipLibraryDefaults ? ` (a person clip our face library can carry, i.e. one of our renders or faceRoute on a paid plan: ${Object.entries(_rm.personClipLibraryDefaults).map(([k, v]) => `${k} → ${v}`).join(', ')})` : ''}. seedance ${(_rm.seconds || []).join('-')}s, up to ${_rm.imagesMax} pictures, ${Object.entries(_rm.seedanceCredits || {}).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}${_rm.wanCredits ? `. wan clips up to ${_rm.wanMaxSourceSeconds}s, ${Object.entries(_rm.wanCredits).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}${_rm.wanLabel ? ` (${_rm.wanLabel})` : ''}` : ''}${_rm.wanPrimeCredits ? `. wan-prime (Wan 3.0 Prime by name) ${Object.entries(_rm.wanPrimeCredits).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}` : ''}` : '';
|
|
4402
4410
|
const _je = d.jobEngines; // the certified engine per job (watched side-by-side tests, 2026-10-19)
|
|
4403
4411
|
const _jeLine = _je && _je.defaults ? `\nDefault engine per job (watched side-by-side tests): ${Object.entries(_je.defaults).map(([k, v]) => `${k} → ${v}`).join(', ')}; a talking ad is one take of up to ${_je.talkingMaxSeconds}s. A model the user picks by name is kept; never switch it.` : '';
|
|
4404
|
-
const text = `Image: ${d.image ? img : 'unavailable'}\nVideo: ${d.video ? vid : 'unavailable'}\n${_lenLine}\nExtend a clip (generate_video extend:true + refVideo): by default it cuts to a NEW SHOT of what happens next (new angle, same scene, any i2v model, default ${d.jobEngines?.defaults?.extend || 'certified'})${_extLine ? `; extendMode:'continue' keeps the SAME take instead, on: ${_extLine}` : ''}${_shotLine ? `\nMulti-shot clips (generate_video shots): ${_shotLine}` : ''}${_rsLine ? `\nRestyle looks (restyle_video style): ${_rsLine}` : ''}${_rmLine}${_jeLine}\n${_pickLine}\n${_resLine}${_motionLine}${_avLine}\nVoice engines (generate_voice): ${voice}\nWriting models (generate_text): ${llm}\nPRICES: every credit figure above is an estimate. ${PRICE_ESTIMATE_LINE}\ncanEdit:${d.canEdit} canAvatar:${d.canAvatar}\nRecipes (${(d.recipes || []).length}): ${(d.recipes || []).slice(0, 20).map(r => r.id).join(', ')}…\n\n${CAPABILITY_MAP}`; return ok(text + connLine, d);
|
|
4412
|
+
const text = `Image: ${d.image ? img : 'unavailable'}\nVideo: ${d.video ? vid : 'unavailable'}\n${_lenLine}\nExtend a clip (generate_video extend:true + refVideo): by default it cuts to a NEW SHOT of what happens next (new angle, same scene, any i2v model, default ${d.jobEngines?.defaults?.extend || 'certified'})${_extLine ? `; extendMode:'continue' keeps the SAME take instead, on: ${_extLine}` : ''}${_shotLine ? `\nMulti-shot clips (generate_video shots): ${_shotLine}` : ''}${_rsLine ? `\nRestyle looks (restyle_video style): ${_rsLine}` : ''}${_rmLine}${_jeLine}\n${_pickLine}\n${_resLine}${d.pricing ? `\nPRICES ABOVE ARE THIS ACCOUNT'S OWN: ${d.pricing.memberRate ? 'no markup on image, video and voice models (the model maker\'s price + a 10% service fee, 1 credit = $0.02)' : 'standard prices (every paid plan pays the model maker\'s price + 10% on image, video and voice models; model rows carry memberCredits)'}.` : ''}${_motionLine}${_avLine}\nVoice engines (generate_voice): ${voice}\nWriting models (generate_text): ${llm}\nPRICES: every credit figure above is an estimate. ${PRICE_ESTIMATE_LINE}\ncanEdit:${d.canEdit} canAvatar:${d.canAvatar}\nRecipes (${(d.recipes || []).length}): ${(d.recipes || []).slice(0, 20).map(r => r.id).join(', ')}…\n\n${CAPABILITY_MAP}`; return ok(text + connLine, d);
|
|
4405
4413
|
}));
|
|
4406
4414
|
|
|
4407
4415
|
server.registerTool('hermoso_credits', {
|
|
4408
4416
|
title: 'Credit balance',
|
|
4409
|
-
description: 'Return the
|
|
4417
|
+
description: 'Return the credit balance, recent priced calls with their credits, and costModel (the one-sentence rule of what costs credits). THE RULE: only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management, analytics, comments, DMs and connectors are FREE on every plan (X is the single per-call exception). Check before kicking off paid generation; answer "does posting cost credits?" with NO. Paid plans: media models at cost + 10% (memberRate).',
|
|
4410
4418
|
inputSchema: {}, outputSchema: {
|
|
4411
4419
|
accountBalance: z.number().nullable().optional().describe('the account’s Hermoso credit balance (authoritative when authed)'),
|
|
4412
4420
|
balance: z.number().optional().describe('raw vendor meter balance (operator/local-dev surface)'),
|
|
@@ -4425,7 +4433,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4425
4433
|
// THE CHARGES THEMSELVES (2026-09-12): the same ledger list as Billing ▸ Usage, so "where did my credits go" has an
|
|
4426
4434
|
// answer here too. Best effort: a reply about the balance never fails on the history read.
|
|
4427
4435
|
let recent = ''; try { const u = await apiGet('/api/billing/usage', { limit: 10 }); if (u?.items?.length) recent = `\nRecent charges (newest first):\n${u.items.map((r) => `• ${String(r.at).slice(0, 16).replace('T', ' ')} UTC · ${r.label} · ${r.credits} credits`).join('\n')}`; } catch {}
|
|
4428
|
-
|
|
4436
|
+
const _mr = d.memberRate === true ? ' This account\'s plan has no markup on image, video and voice models (the model maker\'s price + a 10% service fee).' : d.planUpsell === true && !hostRendersWidgets() ? ' Monthly plans have no markup on models, so credits go further (upgrade_plan); Free and top-up credits use standard prices.' : '';
|
|
4437
|
+
return ok(`Balance: ${bal} credits${d.sessionUsed != null ? ` · session used: ${d.sessionUsed}` : ''}.${_mr} ${d.costModel || 'Credits are spent ONLY on AI models and Ad Spy research; posting, scheduling, campaign management, analytics and connectors are free (X is the one per-call exception).'} ${d.costEstimate || PRICE_ESTIMATE_LINE}${recent}`, d);
|
|
4429
4438
|
}));
|
|
4430
4439
|
|
|
4431
4440
|
// AGENT BILLING: out of credits → top up. With a saved card + billing-admin rights this is the SAME one-click
|
|
@@ -4455,7 +4464,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4455
4464
|
const packs = (cfg.packs || []).map(p => ({ id: p.id, credits: p.credits, priceUsd: p.priceUsd }));
|
|
4456
4465
|
if (!pack) {
|
|
4457
4466
|
const lines = packs.map(p => `• ${p.id} — ${p.credits.toLocaleString()} credits · $${p.priceUsd}`).join('\n') || '(no packs configured)';
|
|
4458
|
-
|
|
4467
|
+
let _up = null; try { _up = (await apiGet('/api/billing/status'))?.planUpsell || null; } catch {}
|
|
4468
|
+
return ok(`Credit packs you can buy:\n${lines}\n\nCall buy_credits again with pack="<id>". With a saved card it's a one-click charge (you'll be asked to confirm); otherwise you get a checkout link for your human.${_up ? `\n\n${_up.line} ${_up.note} Top-up credits use standard prices; upgrade_plan lists the monthly plans.` : ''}\n\nTip: to never run out again, turn on auto-reload (set_auto_reload) — it auto-tops-up from the saved card the moment your balance runs low.`, { packs });
|
|
4459
4469
|
}
|
|
4460
4470
|
const match = packs.find(p => p.id === pack);
|
|
4461
4471
|
if (!match) return ok(`No pack "${pack}". Available: ${packs.map(p => p.id).join(', ') || '(none)'}. Call buy_credits with no argument to see details.`, { packs });
|
|
@@ -4530,7 +4540,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4530
4540
|
|
|
4531
4541
|
server.registerTool('billing_status', {
|
|
4532
4542
|
title: 'Billing status',
|
|
4533
|
-
description: "Show this account's billing at a glance: current plan (
|
|
4543
|
+
description: "Show this account's billing at a glance: current plan (quote plan.priceUsd per plan.period), credit balance, whether auto-reload is on, whether a card is on file, and whether YOU (this key) have ADMIN rights to change billing. Read-only, free. Call it before upgrade_plan / set_auto_reload to know what's possible — members have read-only billing. IN A SHARED TEAM WORKSPACE A MEMBER SEES THE PLAN AND THE BALANCE ONLY: the workspace owner's payment card and auto-reload belong to them and are not reported (billingScope:'member'). Never tell a member there is no card on file — the honest answer is that you cannot see it. plan.memberRate: media models at cost + 10%; plan.legacy: an older plan kept at its price.",
|
|
4534
4544
|
inputSchema: {}, outputSchema: {
|
|
4535
4545
|
plan: z.any().optional().describe("the current plan ({id, label, monthlyUsd, period, priceUsd}). QUOTE priceUsd PER period, never monthlyUsd: monthlyUsd is kept for back-compat and reports an annual subscriber at the monthly rate, which is how a $490/yr account got told it pays $49."),
|
|
4536
4546
|
balanceCredits: z.number().nullable().optional().describe('the current credit balance'),
|
|
@@ -4559,7 +4569,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4559
4569
|
: d.paymentMethodOnFile ? `yes${d.card ? ` (${d.card.brand} ····${d.card.last4})` : ''}` : 'no';
|
|
4560
4570
|
const _per = d.plan?.period === 'yr' ? 'yr' : 'mo';
|
|
4561
4571
|
const _price = _per === 'yr' ? (d.plan?.priceUsd ?? d.plan?.monthlyUsd) : (d.plan?.monthlyUsd ?? d.plan?.priceUsd);
|
|
4562
|
-
const
|
|
4572
|
+
const _pr = d.plan?.memberRate ? `\nPricing: no markup on image, video and voice models (the model maker\'s price + a 10% service fee, 1 credit = $0.02)${d.plan?.legacyPrice ? '; this plan keeps its legacy price (upgrade_plan lists the plans on sale; once left it cannot be chosen again)' : ''}` : d.plan?.nextRenewal ? `\nPricing: standard until the next renewal; from then on the same price with no markup on media models and ${d.plan.nextRenewal.credits} credits a month` : (d.plan?.monthlyUsd || 0) > 0 ? '' : d.planUpsell ? `\nPricing: standard. ${d.planUpsell.line} ${d.planUpsell.note} Free and top-up credits use standard prices.` : '';
|
|
4573
|
+
const text = `Plan: ${d.plan?.label} ($${_price}/${_per})${_pr}\nBalance: ${d.balanceCredits} credits\nAuto-reload: ${arLine}\nCard on file: ${cardLine}\nYour billing role: ${d.role}${d.isAdmin ? ' — you can change the plan / auto-reload' : ' — read-only; ask an admin to change the plan or auto-reload'}${withheld && d.billingNote ? `\n${d.billingNote}` : ''}`;
|
|
4563
4574
|
return ok(text, d);
|
|
4564
4575
|
}));
|
|
4565
4576
|
|
|
@@ -4567,33 +4578,38 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4567
4578
|
// changes + downgrades are made in-app (the tool returns exactly what to do). Admin-only; a human always pays.
|
|
4568
4579
|
server.registerTool('upgrade_plan', {
|
|
4569
4580
|
title: 'Upgrade plan',
|
|
4570
|
-
description: "Change
|
|
4581
|
+
description: "Change the SUBSCRIPTION plan (admin only). Monthly plans, no markup on media models. No argument lists them. New subscriber: a Checkout URL for the human. Existing subscriber upgrading: a quote + quote_token; once the human agrees, call again with confirm:true and quote_token to charge the card on file. Downgrade: a billing-page link; it starts at the term's end.",
|
|
4571
4582
|
inputSchema: {
|
|
4572
4583
|
plan: z.string().optional().describe('the plan id to move to (e.g. pro) — omit to list the available plans first'),
|
|
4573
|
-
period: z.enum(['mo', 'yr']).optional().describe(
|
|
4584
|
+
period: z.enum(['mo', 'yr']).optional().describe("monthly only ('yr' buys monthly)"),
|
|
4585
|
+
confirm: z.boolean().optional().describe('true (with quote_token) to make a quoted upgrade'),
|
|
4586
|
+
quote_token: z.string().optional().describe("the quoteToken from the quote step"),
|
|
4574
4587
|
},
|
|
4575
4588
|
outputSchema: {
|
|
4576
4589
|
plans: z.array(z.any()).optional().describe('available paid plans ({id, name, priceUsd, credits}) when listing'),
|
|
4577
|
-
mode: z.string().optional().describe("'checkout'
|
|
4590
|
+
mode: z.string().optional().describe("'checkout' | 'confirm' | 'portal' | 'in_app'"),
|
|
4578
4591
|
url: z.string().optional().describe('the ready-to-pay Stripe Checkout URL (checkout mode)'),
|
|
4579
4592
|
plan: z.string().optional().describe('the target plan id'),
|
|
4580
4593
|
planLabel: z.string().optional().describe('the target plan display name'),
|
|
4581
4594
|
monthlyUsd: z.number().optional().describe('the plan’s monthly price in USD'),
|
|
4582
|
-
chargeUsd: z.number().optional().describe('the
|
|
4583
|
-
period: z.string().optional().describe("
|
|
4584
|
-
action: z.string().optional().describe("
|
|
4585
|
-
guidance: z.string().optional().describe('
|
|
4595
|
+
chargeUsd: z.number().optional().describe('the charge in USD'),
|
|
4596
|
+
period: z.string().optional().describe("'mo'"),
|
|
4597
|
+
action: z.string().optional().describe("'upgrade' or 'downgrade'"),
|
|
4598
|
+
guidance: z.string().optional().describe('what happens next, in words'),
|
|
4586
4599
|
},
|
|
4587
4600
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, // not read-only: with `plan` set it mints a Stripe Checkout Session. It still charges nothing — the human pays.
|
|
4588
|
-
}, wrap(async ({ plan, period }) => {
|
|
4601
|
+
}, wrap(async ({ plan, period, confirm, quote_token }) => {
|
|
4589
4602
|
const cfg = await apiGet('/api/billing/config');
|
|
4590
|
-
const plans = (cfg.plans || []).filter(p => p.priceUsd > 0).map(p => ({ id: p.id, name: p.name, priceUsd: p.priceUsd, credits: p.credits }));
|
|
4603
|
+
const plans = (cfg.plans || []).filter(p => p.priceUsd > 0 && !p.legacy && !p.cohort).map(p => ({ id: p.id, name: p.name, priceUsd: p.priceUsd, credits: p.credits, noMarkup: !!p.noMarkup, makes: p.makes || null }));
|
|
4591
4604
|
if (!plan) {
|
|
4592
|
-
const lines = plans.map(p => `• ${p.id} — ${p.name}: $${p.priceUsd}/mo · ${p.credits.toLocaleString()} credits/mo`).join('\n') || '(no plans configured)';
|
|
4593
|
-
return ok(`Subscription plans:\n${lines}\n\nCall upgrade_plan again with plan="<id>" (admin only). Downgrades + changes for existing subscribers are made in the app.`, { plans });
|
|
4605
|
+
const lines = plans.map(p => `• ${p.id} — ${p.name}: $${p.priceUsd}/mo · ${p.credits.toLocaleString()} credits/mo${p.makes?.videoAds ? ` (about ${p.makes.videoAds} ${p.makes.videoSeconds}s video ads or ${p.makes.images} images)` : ''}${p.noMarkup ? ' · no markup on image, video and voice models' : ''}`).join('\n') || '(no plans configured)';
|
|
4606
|
+
return ok(`Subscription plans (monthly):\n${lines}\n${cfg.memberPricing?.note || ''}\n\nCall upgrade_plan again with plan="<id>" (admin only). Downgrades + changes for existing subscribers are made in the app.`, { plans });
|
|
4594
4607
|
}
|
|
4595
|
-
const d = await apiPost('/api/billing/plan-link', { planId: plan, period });
|
|
4596
|
-
if (d.
|
|
4608
|
+
const d = await apiPost('/api/billing/plan-link', { planId: plan, period, ...(confirm && quote_token ? { confirm: true, quoteToken: quote_token } : {}) });
|
|
4609
|
+
if (d.ok && d.plan) return ok(`Done: the plan is now ${d.plan}. The new credits land within a minute (billing_status shows them).`, d);
|
|
4610
|
+
if (d.mode === 'confirm') return ok(`${d.guidance}\nquote_token: ${d.quoteToken}`, d);
|
|
4611
|
+
if (d.mode === 'portal') return ok(`${d.guidance}\n${d.url}`, d);
|
|
4612
|
+
if (d.mode === 'checkout') return ok(`Checkout link for the ${d.planLabel} plan ($${d.monthlyUsd}/mo, no markup on image, video and voice models${d.period === 'yr' ? `, billed $${d.chargeUsd}/yr` : ''}):\n${d.url}\n\nGive this URL to your human to subscribe on Stripe's secure page. Nothing is charged until they pay.`, d);
|
|
4597
4613
|
return ok(d.guidance, d); // in_app — an existing-subscriber upgrade or a downgrade (done by a person in the app)
|
|
4598
4614
|
}));
|
|
4599
4615
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.434",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 922 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, 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 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",
|