hermoso 0.1.387 → 0.1.390
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-cost.mjs +3 -0
- package/mcp/tools.mjs +19 -7
- package/package.json +1 -1
package/mcp/tool-cost.mjs
CHANGED
|
@@ -43,6 +43,9 @@ export const CREATE_FREE_READS = Object.freeze([
|
|
|
43
43
|
'get_job', 'list_library', 'fetch_asset', 'list_skills', 'get_skill',
|
|
44
44
|
'list_product_photos', 'fetch_app_screens', 'list_hooks', 'list_templates', 'face_check',
|
|
45
45
|
'list_meta_posts', 'list_published_posts', 'post_performance', 'diagnose_posts', 'backfill_posts',
|
|
46
|
+
// The product catalog (2026-10-06): a store read or write, and a product page read with curl and parsed locally (no
|
|
47
|
+
// model; POST /api/products answers creditsUsed: 0). Each says "Free" / "0 credits" in its own description.
|
|
48
|
+
'list_products', 'add_product', 'refresh_products', 'update_product', 'remove_product',
|
|
46
49
|
]);
|
|
47
50
|
// Reads inside `research`. `find_competitors` says "0 credits" in its own description (it is the discovery model,
|
|
48
51
|
// billed to us, not a paid ad-data call); the watch tools only write and read a stored preference — the weekly
|
package/mcp/tools.mjs
CHANGED
|
@@ -5725,6 +5725,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5725
5725
|
const draftLines = drafts.slice(0, 20).map((x, i) => ` ${i + 1}. ${x.id} · ${x.at} → ${(x.channels || []).join(', ')} · ${x.kind}\n ${String(x.message || '').replace(/\n+/g, ' / ').slice(0, 200)}\n ${x.media || '(no file)'}`);
|
|
5726
5726
|
return ok([
|
|
5727
5727
|
`Autoposting: ${state}.${d.note ? `\n${d.note}` : ''}`,
|
|
5728
|
+
d.creditNote || '',
|
|
5728
5729
|
`Posting times: ${(d.postingTimes || []).join(', ') || 'none set (add some on the Schedule tab)'} ${d.postingSchedule?.timezone || 'UTC'}. It fills ${d.daysAhead} day(s) ahead; ${d.queuedPosts} post(s) queued now.`,
|
|
5729
5730
|
(d.channelsView || []).length ? `Channels:\n${d.channelsView.map((c) => ` ${c.on ? '[on] ' : '[off]'} ${c.label} (${c.channel}): ${c.on ? `${c.postsPerDay} a day${c.times.length ? ` at ${c.times.join(', ')}` : ''}` : 'not posting'}${c.note ? ` · ${c.note}` : ''}`).join('\n')}` : (d.connectedUnreadable ? 'Channels: could not be read just now.' : 'Channels: none connected — autopilot has nowhere to post. Connect one under Workspace ▸ Connectors.'),
|
|
5730
5731
|
`Makes ${d.postsPerDayMade || 0} new post(s) a day, every one a fresh render made for it${d.estCreditsPerDay != null && d.postsPerDayMade ? `, about ${d.estCreditsPerDay} credits a day at today's prices` : ''}.${d.zeroBudget ? ` ${d.budgetNote}` : ''} Used: ${d.spend?.today || 0} credits today, ${d.spend?.week || 0} this week.`,
|
|
@@ -5763,9 +5764,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5763
5764
|
const { goal, goalNote, pillars, formats, avoid, learnings, forgetCaptionEdits, ...rest } = a;
|
|
5764
5765
|
const brief = Object.fromEntries(Object.entries({ goal, goalNote, pillars, formats, avoid, learnings, ...(forgetCaptionEdits ? { editExamples: [] } : {}) }).filter(([, v]) => v !== undefined));
|
|
5765
5766
|
const d = await apiPut('/api/schedule/refill', { ...rest, ...(Object.keys(brief).length ? { brief } : {}) });
|
|
5766
|
-
return ok(d.enabled
|
|
5767
|
+
return ok((d.enabled
|
|
5767
5768
|
? `Autoposting is ON in ${d.mode === 'auto' ? 'AUTO-PUBLISH mode — each run makes fresh posts and schedules them to go out on their own.' : 'REVIEW mode — each run makes fresh posts and keeps them as drafts; nothing is scheduled until you approve them with run_post_refill.'} get_post_refill lists the channels it posts to and what it costs a day. Run run_post_refill (a preview by default: it renders and queues nothing, and writing its copy costs a few credits) to see a plan.`
|
|
5768
|
-
: 'Autoposting is OFF. The recurring job is gone and nothing new will be made. Posts already in the calendar are untouched — cancel those with cancel_scheduled if you want them gone.', d);
|
|
5769
|
+
: 'Autoposting is OFF. The recurring job is gone and nothing new will be made. Posts already in the calendar are untouched — cancel those with cancel_scheduled if you want them gone.') + (d.creditNote ? `\n${d.creditNote}` : ''), d);
|
|
5769
5770
|
}));
|
|
5770
5771
|
server.registerTool('run_post_refill', {
|
|
5771
5772
|
title: 'Run autoposting / review its drafts',
|
|
@@ -5814,7 +5815,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5814
5815
|
return ok([
|
|
5815
5816
|
d.summary || '',
|
|
5816
5817
|
lines.length ? `\nThe posts:\n${lines.join('\n')}` : '',
|
|
5817
|
-
(d.noChannels || d.zeroBudget || (d.enabled === false && !(d.posts || []).length)) ? '' : d.dryRun ? `\nNOTHING WAS RENDERED OR QUEUED — this was a preview${d.copyCredits ? ` (writing its copy cost ${d.copyCredits} credit(s))` : ''}. Show it to the user; call again with dryRun:false only if they say yes.`
|
|
5818
|
+
(d.noChannels || d.zeroBudget || (d.outOfCredits && !(d.posts || []).length) || (d.enabled === false && !(d.posts || []).length)) ? '' : d.dryRun ? `\nNOTHING WAS RENDERED OR QUEUED — this was a preview${d.copyCredits ? ` (writing its copy cost ${d.copyCredits} credit(s))` : ''}. Show it to the user; call again with dryRun:false only if they say yes.`
|
|
5818
5819
|
: (d.mode === 'auto' ? `\n${q} post(s) scheduled. Pull any of them before it goes out with cancel_scheduled.` : ((d.drafts || []).length ? `\n${d.drafts.length} new draft(s) waiting for approval — approve, edit or discard them with run_post_refill.` : '\nNo new drafts were made by this run (get_post_refill lists any drafts already waiting).')),
|
|
5819
5820
|
qFail.length ? `\n${qFail.length} could not be queued: ${qFail.map(x => `${x.at} — ${x.error}`).join(' · ')}` : '',
|
|
5820
5821
|
(d.notes || []).length ? `\nNotes: ${d.notes.join(' · ')}` : '',
|
|
@@ -17990,7 +17991,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17990
17991
|
model: z.string().optional().describe('image model id from hermoso_capabilities. A model whose `refs.mode` is "edit" there (gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor'),
|
|
17991
17992
|
imageSize: z.string().optional().describe('pixel-size preset for models that support it: 1K/2K, and 4K on the models hermoso_capabilities lists with a 4K imageSize price (a 4K ask on any other model is refused, free) — omit for the default'),
|
|
17992
17993
|
fixLabel: z.boolean().optional().describe('default true: when the saved brand\'s product photo rides in this render, the product\'s label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check, a credit or two; a re-print adds about ten). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed.'),
|
|
17993
|
-
logoPlacement: z.enum(['auto', 'overlay', 'in_scene', 'none']).optional().describe('overlay:
|
|
17994
|
+
logoPlacement: z.enum(['auto', 'overlay', 'in_scene', 'none']).optional().describe('overlay: real logo file laid flat. in_scene: on something in the scene, from the file, checked. auto (default): from the prompt'),
|
|
17994
17995
|
mask: z.string().optional().describe('MASKED EDIT — change ONE region of an image and keep the rest: a local path or URL of a mask image for refImages[0] (the image being edited). Either convention works and the reply says which it read: TRANSPARENT pixels = change, or, on a mask with no transparency, WHITE = change and black = keep. Any size; it is scaled to the image. The mask GUIDES the edit rather than stencilling it: the new content can blend a little past its edge. Runs on the model hermoso_capabilities marks `refs.mask` (gpt-image-2.5): leave `model` empty or name that one — any other named model is refused, free. Needs refImages; the result keeps the source image\'s own frame, so aspectRatio is not applied.'),
|
|
17995
17996
|
},
|
|
17996
17997
|
outputSchema: {
|
|
@@ -18150,7 +18151,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18150
18151
|
restrainedGrade: z.boolean().optional().describe('true for a calm / premium / muted look instead of the default punchy poster grade'),
|
|
18151
18152
|
logo: z.string().optional().describe('a brand logo URL or path to place into the composition'),
|
|
18152
18153
|
logo3d: z.boolean().optional().describe('first turn the flat logo into a volumetric 3D render (one extra billed image), then composite that'),
|
|
18153
|
-
logoPlacement: z.enum(['auto', 'overlay', 'in_scene']).optional().describe('overlay
|
|
18154
|
+
logoPlacement: z.enum(['auto', 'overlay', 'in_scene']).optional().describe('overlay (exact, flat) | in_scene (from the file, checked); auto: in_scene with logo3d'),
|
|
18154
18155
|
split: z.object({ mode: z.enum(['plain', 'before_after', 'versus', 'custom']), panels: z.array(z.string()).optional() }).passthrough().optional().describe('split/panel LAYOUT — only when the user asks for one ("split", "before/after", "versus screen"). "X vs Y" as a SCENE stays one unified frame'),
|
|
18155
18156
|
reference: z.object({}).passthrough().optional().describe("fields YOU extracted by eye from a reference thumbnail. Extract ALL of: brief (one dense sentence on the concept), subject (pose/action generically, NEVER a specific identity), elements, location, composition, background, split (boolean), split_count, person_count (0-3), emotion (one of the 11 presets or 'other'), emotion_detail (one vivid sentence covering eyes, brows, mouth, head angle). emotion + emotion_detail carry the reference's actual facial performance, which is the single biggest CTR lever on a face; split/split_count reproduce its panel structure. The reference image itself is never sent to the model"),
|
|
18156
18157
|
tweak: z.object({ kind: z.string(), value: z.string() }).describe('surgical pixel-faithful edit of a FINISHED thumbnail (needs sourceImage): kind emotion / background / background_color / rim_light, or any other kind with the edit in words as value').optional(),
|
|
@@ -19444,13 +19445,15 @@ function memoryNoteVerdict(text) {
|
|
|
19444
19445
|
})));
|
|
19445
19446
|
server.registerTool('update_brand', {
|
|
19446
19447
|
title: 'Update profile fields',
|
|
19447
|
-
description: 'Patch SPECIFIC fields of the active profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, and how the brand and product names are pronounced) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. If no brand is saved yet and you only INFERRED one from what the user is making, ask them to confirm it is their brand before saving it (they may be working for a client or just trying things). Only pass the fields you’re changing.',
|
|
19448
|
+
description: 'Patch SPECIFIC fields of the active profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, logo, light logo, and how the brand and product names are pronounced) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. If no brand is saved yet and you only INFERRED one from what the user is making, ask them to confirm it is their brand before saving it (they may be working for a client or just trying things). Only pass the fields you’re changing.',
|
|
19448
19449
|
inputSchema: { ...STORE_BRAND,
|
|
19449
19450
|
name: z.string().optional(), domain: z.string().optional().describe('website domain'), sells: z.string().optional().describe('what the brand sells'),
|
|
19450
19451
|
summary: z.string().optional().describe('one-line description'), category: z.string().optional(), audience: z.string().optional(),
|
|
19451
19452
|
positioning: z.string().optional(), voice: z.string().optional().describe('brand voice/tone'), style: z.string().optional().describe('visual style — palette, typography, aesthetic'), goal: z.string().optional().describe('current marketing goal'),
|
|
19452
19453
|
pronounce: z.string().optional().describe('how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. "KOH-dee-ak"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name.'),
|
|
19453
19454
|
pronunciations: z.record(z.string()).optional().describe('how PRODUCT names are said aloud, e.g. {"Power Cakes": "POW-er cakes"}. Merged into the saved ones; an empty string removes one.'),
|
|
19455
|
+
logo: z.string().optional().describe('logo image URL or path'),
|
|
19456
|
+
logoLight: z.string().optional().describe('light logo for dark pictures; "none" removes'),
|
|
19454
19457
|
},
|
|
19455
19458
|
outputSchema: { ok: z.boolean().optional(), updated: z.array(z.string()).optional(), brand: z.any().optional() },
|
|
19456
19459
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
@@ -19464,6 +19467,15 @@ function memoryNoteVerdict(text) {
|
|
|
19464
19467
|
for (const [k, v] of Object.entries(a.pronunciations).slice(0, 30)) { const n = String(k || '').trim().slice(0, 60); if (!n) continue; const say = String(v || '').trim().slice(0, 60); if (say) cur[n] = say; else delete cur[n]; }
|
|
19465
19468
|
patch.pronunciations = cur;
|
|
19466
19469
|
}
|
|
19470
|
+
// THE LOGO FILES (2026-10-06): the logo, and the optional light version laid on when the original would not read on a
|
|
19471
|
+
// dark or busy background. A local path is uploaded first (toRef); only a raster file is kept (an SVG cannot be laid on).
|
|
19472
|
+
for (const k of ['logo', 'logoLight']) {
|
|
19473
|
+
if (a[k] == null || !String(a[k]).trim()) continue;
|
|
19474
|
+
if (k === 'logoLight' && /^(none|remove|off)$/i.test(String(a[k]).trim())) { patch.logoLight = ''; continue; }
|
|
19475
|
+
const ref = await toRef(String(a[k]).trim());
|
|
19476
|
+
if (!ref || !/^(https?:\/\/|data:image\/(png|jpe?g|webp|gif);base64,)/i.test(ref) || /\.svg(\?|$)/i.test(ref)) return { content: [{ type: 'text', text: `The ${k === 'logo' ? 'logo' : 'light logo'} must be a PNG, JPEG or WebP image (a URL or a local file). Nothing was changed.` }], isError: true };
|
|
19477
|
+
patch[k] = ref;
|
|
19478
|
+
}
|
|
19467
19479
|
if (!Object.keys(patch).length) return { content: [{ type: 'text', text: 'Pass at least one brand field to change.' }], isError: true };
|
|
19468
19480
|
const merged = { ...brand, ...patch };
|
|
19469
19481
|
await writeStore('heist.brand.v1', merged); // the store PUT preserves server-side brand enrichments (playbook/pronounce)
|
|
@@ -21281,7 +21293,7 @@ function memoryNoteVerdict(text) {
|
|
|
21281
21293
|
plan: z.array(z.any()).optional().describe('the `plan` (or `remaining`) a previous call returned: render exactly those ads, no new planning'),
|
|
21282
21294
|
batchId: z.string().optional().describe('the batchId a previous call returned, so the rest lands in the same batch'),
|
|
21283
21295
|
dryRun: z.boolean().optional().describe('true = plan and quote only, nothing rendered'),
|
|
21284
|
-
logoPlacement: z.enum(['auto', 'overlay', 'in_scene', 'none']).optional().describe('overlay (default)
|
|
21296
|
+
logoPlacement: z.enum(['auto', 'overlay', 'in_scene', 'none']).optional().describe('overlay (default) | in_scene (on something, checked) | none | auto'),
|
|
21285
21297
|
},
|
|
21286
21298
|
outputSchema: {
|
|
21287
21299
|
batchId: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.390",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 906 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 and Telegram. 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",
|