hermoso 0.1.535 → 0.1.537
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/client.mjs +2 -1
- package/mcp/spend-guard.mjs +21 -3
- package/mcp/tools.mjs +2 -2
- package/package.json +1 -1
package/mcp/client.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
|
+
import { jobQuoteLegs } from './spend-guard.mjs'; // a quoted job as named legs (get_cost)
|
|
10
11
|
|
|
11
12
|
// Remote-connector identity: mcp/http.mjs wraps each request in mcpCtx.run({ token }) so every /api call a tool
|
|
12
13
|
// makes carries THAT caller's bearer (bills their account). stdio keeps using the env token — ctx is simply unset.
|
|
@@ -575,7 +576,7 @@ export async function submitJob(type, input, { label = '' } = {}) {
|
|
|
575
576
|
if (st?.quoteOnly) {
|
|
576
577
|
const d = await apiPost('/api/jobs', { type, input, label, dryRun: true });
|
|
577
578
|
const q = d?.quote || null;
|
|
578
|
-
|
|
579
|
+
for (const leg of jobQuoteLegs(q, { label, type })) (st.quoteLegs ||= []).push(leg); // the job's named parts when the server itemised it (a storyboard's paints, a portrait, the clip), else the job as one leg
|
|
579
580
|
throw quoteStopError();
|
|
580
581
|
}
|
|
581
582
|
// A TOOL CALL WITH AN idempotency_key: each job it queues carries a key derived from it (one per submit in the call), so a
|
package/mcp/spend-guard.mjs
CHANGED
|
@@ -108,17 +108,35 @@ export function quoteFromLegs(legs) {
|
|
|
108
108
|
const sum = L.reduce((t, l) => t + (Number(l.priceCredits) > 0 ? Number(l.priceCredits) : Number(l.credits) > 0 ? Number(l.credits) : 0), 0);
|
|
109
109
|
const hold = L.reduce((t, l) => t + (Number(l.credits) > 0 ? Number(l.credits) : 0), 0);
|
|
110
110
|
if (!L.length) return { credits: 0, legs: L, free: true };
|
|
111
|
-
|
|
111
|
+
const also = [...new Set(L.flatMap((l) => (Array.isArray(l.alsoBilled) ? l.alsoBilled : [])))];
|
|
112
|
+
return { credits: unpriced.length ? null : round1(sum), ...(hold > sum + 1e-9 && !unpriced.length ? { holdCredits: round1(hold) } : {}), ...(L.some((l) => l.basis === 'estimate') ? { estimate: true } : {}), legs: L, ...(unpriced.length ? { unpriced: [...new Set(unpriced.map((l) => l.label || 'a step'))] } : {}), ...(also.length ? { alsoBilled: also } : {}) };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The legs of ONE job's dry-run quote (2026-11-13). When the server itemised the job (`parts`: a storyboard's image paints,
|
|
116
|
+
* a cast portrait, an opening frame, the clip itself), each part is its own named leg; otherwise the job is one leg, as
|
|
117
|
+
* before. The parts are used only when they add up to the job's own figure, so the total is always the dry run's.
|
|
118
|
+
* `alsoBilled` names the small reads the job bills on top at their actual cost (the storyboard check, the product photo
|
|
119
|
+
* read): said in the answer, never a number that is not known.
|
|
120
|
+
*/
|
|
121
|
+
export function jobQuoteLegs(q, { label = '', type = '' } = {}) {
|
|
122
|
+
const credits = q && Number(q.credits) > 0 ? Number(q.credits) : null;
|
|
123
|
+
const also = q && Array.isArray(q.alsoBilled) ? q.alsoBilled.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 6) : [];
|
|
124
|
+
const one = { credits, ...(q && Number(q.priceCredits) > 0 ? { priceCredits: Number(q.priceCredits) } : {}), basis: credits != null ? 'hold' : 'actual', label: label || q?.label || type, ...(q && q.refused ? { refused: String(q.refused) } : {}), ...(also.length ? { alsoBilled: also } : {}) };
|
|
125
|
+
const parts = credits != null && !(Number(q.priceCredits) > 0) && Array.isArray(q.parts) ? q.parts.filter((x) => x && Number(x.credits) > 0 && String(x.label || '').trim()) : [];
|
|
126
|
+
if (!parts.length || Math.abs(parts.reduce((t, x) => t + Number(x.credits), 0) - credits) > 0.05) return [one];
|
|
127
|
+
return parts.map((x, i) => ({ credits: Number(x.credits), basis: 'hold', label: String(x.label).trim().slice(0, 90), ...(i === parts.length - 1 && also.length ? { alsoBilled: also } : {}) }));
|
|
112
128
|
}
|
|
113
129
|
const legLine = (l) => `${l.label || 'a step'}: ${Number(l.priceCredits) > 0 ? `${l.priceCredits}` : Number(l.credits) > 0 ? `${l.credits}` : 'its actual cost'}`;
|
|
130
|
+
const alsoLine = (q) => (Array.isArray(q.alsoBilled) && q.alsoBilled.length ? ` Billed on top at their actual cost, a small read each: ${q.alsoBilled.join(', ')}.` : '');
|
|
114
131
|
export function quoteText(tool, q) {
|
|
115
132
|
if (q.refused) return `NO PRICE (nothing ran, nothing was charged): ${tool} would not start: ${q.refused}`;
|
|
116
133
|
if (q.free) return `PRICE (nothing ran, nothing was charged): ${tool} with these arguments spends no credits.`;
|
|
117
134
|
if (q.credits == null) {
|
|
118
135
|
const known = q.legs.filter((l) => Number(l.credits) > 0);
|
|
119
|
-
|
|
136
|
+
const knownSum = round1(known.reduce((t, l) => t + (Number(l.priceCredits) > 0 ? Number(l.priceCredits) : Number(l.credits)), 0));
|
|
137
|
+
return `NO FIXED PRICE (nothing ran, nothing was charged): ${tool} is billed at the actual cost of ${q.unpriced.join(' and ')}, which is not known before it runs.${known.length ? ` The priced part is ${knownSum} credits: ${known.map(legLine).join('; ')}.` : ''}${alsoLine(q)} expected_credits cannot bind this call; run it without it.`;
|
|
120
138
|
}
|
|
121
|
-
return `PRICE (nothing ran, nothing was charged): ${tool} costs ${q.estimate ? 'about ' : ''}${q.credits} credits${q.holdCredits ? ` (it sets aside up to ${q.holdCredits} while it runs; the final charge is its actual cost)` : q.estimate ? ' (its own entry estimate; the final charge is the actual cost of the run)' : ''}.${q.legs.length > 1 ? ` Steps: ${q.legs.map(legLine).join('; ')}.` : ''} To run it at this price, call again without get_cost, with expected_credits: ${q.credits} and an idempotency_key.`;
|
|
139
|
+
return `PRICE (nothing ran, nothing was charged): ${tool} costs ${q.estimate ? 'about ' : ''}${q.credits} credits${q.holdCredits ? ` (it sets aside up to ${q.holdCredits} while it runs; the final charge is its actual cost)` : q.estimate ? ' (its own entry estimate; the final charge is the actual cost of the run)' : ''}.${q.legs.length > 1 ? ` Steps: ${q.legs.map(legLine).join('; ')}.` : ''}${alsoLine(q)} To run it at this price, call again without get_cost, with expected_credits: ${q.credits} and an idempotency_key.`;
|
|
122
140
|
}
|
|
123
141
|
export function expectedMismatchText(tool, expected, q) {
|
|
124
142
|
if (q.credits == null) return `Refused, nothing ran and nothing was charged: expected_credits was ${expected}, but ${tool} has no fixed price here (${q.refused || `billed at the actual cost of ${(q.unpriced || []).join(' and ') || 'its model steps'}`}). Run it without expected_credits, or ask the user first.`;
|
package/mcp/tools.mjs
CHANGED
|
@@ -15785,7 +15785,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
15785
15785
|
const amzPlacements = z.record(z.number()).optional().describe('bid increase per placement in whole percent 0-900, keys PLACEMENT_TOP, PLACEMENT_REST_OF_SEARCH, PLACEMENT_PRODUCT_PAGE, SITE_AMAZON_BUSINESS');
|
|
15786
15786
|
server.registerTool('list_amazon_ads_profiles', {
|
|
15787
15787
|
title: 'List Amazon Ads profiles',
|
|
15788
|
-
description: 'The Amazon Ads advertiser profiles shared with this brand (ticked under Manage accounts), one per advertiser per marketplace across North America, Europe and the Far East: profileId, marketplace, currency, seller or vendor, account name. Only these are usable. Every other Amazon Ads tool takes amazonProfileId. Read-only, free.',
|
|
15788
|
+
description: 'The Amazon Ads advertiser profiles shared with this brand (ticked under Manage accounts, or call list_connector_accounts with provider amazon_ads, then set_connector_accounts), one per advertiser per marketplace across North America, Europe and the Far East: profileId, marketplace, currency, seller or vendor, account name. Only these are usable. Every other Amazon Ads tool takes amazonProfileId. Read-only, free.',
|
|
15789
15789
|
inputSchema: {},
|
|
15790
15790
|
outputSchema: { ok: z.boolean().optional(), count: z.number().optional(), profiles: z.array(z.any()).optional(), note: z.string().optional() },
|
|
15791
15791
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
@@ -25818,7 +25818,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
25818
25818
|
loop: z.boolean().optional().describe('true = a seamless loop whose last frame flows back into its first. Only models with loop true in hermoso_capabilities; needs refImage and cannot be combined with endImage.'),
|
|
25819
25819
|
shots: z.array(z.object({ prompt: z.string().describe('what happens in this shot'), seconds: z.number().int().min(1).max(15).describe('this shot’s length in whole seconds') })).optional().describe('MULTI-SHOT: one clip cut into these shots, in order; the seconds add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut.'),
|
|
25820
25820
|
extend: z.boolean().optional().describe('true = EXTEND refVideo by durationSeconds (default 5) with what your prompt says happens next, as ONE clip. By default it CUTS to a NEW SHOT: new angle, same scene, room sound carried, no music at the cut. A named model (any i2v) renders it.'),
|
|
25821
|
-
extendMode: z.enum(['cut', 'continue']).optional().describe("with extend: 'continue' = the SAME take past its last frame
|
|
25821
|
+
extendMode: z.enum(['cut', 'continue']).optional().describe("with extend: 'continue' = the SAME take past its last frame when asked (continue it, keep going; models in hermoso_capabilities)."),
|
|
25822
25822
|
interactionId: z.string().optional().describe('with extend:true on an Omni model: the interactionId returned by an earlier render on that model — continues it from its own stored context instead of re-uploading refVideo.'),
|
|
25823
25823
|
durationSeconds: z.number().optional().describe('length of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10).'),
|
|
25824
25824
|
aspectRatio: z.string().optional().describe("default '9:16'"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.537",
|
|
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",
|