hermoso 0.1.355 → 0.1.359
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/tools.mjs +21 -12
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -16988,7 +16988,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16988
16988
|
brand: z.union([z.string(), z.object({}).passthrough()]).optional().describe('brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one'),
|
|
16989
16989
|
product: z.string().describe('what to advertise + any angle/offer the user specified'),
|
|
16990
16990
|
format: z.enum(['auto', 'image', 'video']).optional().describe("'image', 'video', or 'auto' when unspecified"),
|
|
16991
|
-
durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess
|
|
16991
|
+
durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 30s spot in one unbroken take (2026-09-29), and a length the user names always wins.'),
|
|
16992
16992
|
hook: z.string().optional().describe('force the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "macro_asmr"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text.'),
|
|
16993
16993
|
setting: z.string().optional().describe('force the WHERE — a setting id from list_hooks (e.g. "kitchen", "gym", or a surreal one like "volcano_rim" / "airplane_wing", which are played 100% straight and never acknowledged). Omit for a neutral setting.'),
|
|
16994
16994
|
recipe: z.string().optional().describe('a recipe id from hermoso_capabilities to force an archetype'),
|
|
@@ -19878,6 +19878,10 @@ function memoryNoteVerdict(text) {
|
|
|
19878
19878
|
const site = await apiGet('/api/site/images', { url: p.domain });
|
|
19879
19879
|
const imgs = [...(site?.images || [])].filter(Boolean);
|
|
19880
19880
|
if (imgs.length) { p.productImages = imgs.slice(0, 12); if (!p.product) p.product = imgs[0]; }
|
|
19881
|
+
// THE PRODUCTS' OWN NAMES beside their photos (2026-09-29): the shop's title, else the name the photo review gave
|
|
19882
|
+
// the photo it kept. The Studio's Elements names each product card (and its @handle) from these, never from a
|
|
19883
|
+
// stored file's random name. Only for photos this brand keeps.
|
|
19884
|
+
if (imgs.length) { const _pt = (Array.isArray(site?.productTitles) ? site.productTitles : []).filter((x) => x && typeof x.title === 'string' && x.title.trim() && p.productImages.includes(x.image)).map((x) => ({ image: x.image, title: x.title.trim().slice(0, 60) })); if (_pt.length) p.productTitles = _pt; }
|
|
19881
19885
|
// products is an array of product-NAME STRINGS everywhere else (public/app.js writes `_prodNames`), and
|
|
19882
19886
|
// brandContext joins it straight into "use these EXACT names". /api/site/images returns {title,image}
|
|
19883
19887
|
// OBJECTS, so storing them raw printed "[object Object], [object Object]" into every subsequent plan —
|
|
@@ -20116,10 +20120,11 @@ function memoryNoteVerdict(text) {
|
|
|
20116
20120
|
// motion and the ORIGINAL AUDIO untouched. The plan is one server call; every variant is a `multiply` job (opening-frame keyframe edit → motion transfer driven by the source, so length, cut, motion and audio are the original's).
|
|
20117
20121
|
server.registerTool('multiply_ad', {
|
|
20118
20122
|
title: 'Multiply an ad',
|
|
20119
|
-
description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant.",
|
|
20123
|
+
description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant. `change` is what should change, in the user's own words ('older women', 'winter streets', 'swap the mug for our bottle'): every variant applies it and the variants still differ around it; a part of it about the voice, words, language, music, length or captions cannot change here, and the reply says so and names the tool that can.",
|
|
20120
20124
|
inputSchema: {
|
|
20121
20125
|
video: z.string().describe('the source video URL'),
|
|
20122
20126
|
count: z.number().optional().describe('how many variants, 1-12 (default 6)'),
|
|
20127
|
+
change: z.string().optional().describe("what should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely)"),
|
|
20123
20128
|
axes: z.array(z.string()).optional().describe('what to vary: character, outfit, location, objects (default all four), or your own, e.g. "season"'),
|
|
20124
20129
|
notes: z.string().optional().describe('anything the variants must respect, e.g. "keep it women 25-40", "no gyms"'),
|
|
20125
20130
|
regions: z.array(z.string()).optional().describe('markets to restyle for, one or more variants each, e.g. ["Berlin","Tokyo","São Paulo"] — visuals only; audio is never translated here'),
|
|
@@ -20127,21 +20132,22 @@ function memoryNoteVerdict(text) {
|
|
|
20127
20132
|
},
|
|
20128
20133
|
outputSchema: { jobs: z.array(z.any()).optional(), plan: z.any().optional(), perVariantCredits: z.number().optional(), totalCredits: z.number().optional(), dryRun: z.boolean().optional() },
|
|
20129
20134
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
20130
|
-
}, wrap(async ({ video, count, axes, notes, regions, dryRun }) => {
|
|
20135
|
+
}, wrap(async ({ video, count, change, axes, notes, regions, dryRun }) => {
|
|
20131
20136
|
const src = String(video || '').trim();
|
|
20132
20137
|
if (!/^https?:\/\//.test(src)) return { content: [{ type: 'text', text: 'Pass the source video as a URL — a previous render, a job result, or an entry from list_library.' }], isError: true };
|
|
20133
20138
|
const n = Math.max(1, Math.min(12, Math.round(+count) || 6));
|
|
20134
|
-
const plan = await apiPost('/api/multiply/plan', { video: src, count: n, ...(axes ? { axes } : {}), ...(notes ? { notes } : {}), ...(regions ? { regions } : {}) });
|
|
20139
|
+
const plan = await apiPost('/api/multiply/plan', { video: src, count: n, ...(change ? { change } : {}), ...(axes ? { axes } : {}), ...(notes ? { notes } : {}), ...(regions ? { regions } : {}) });
|
|
20135
20140
|
const p = plan?.data || plan;
|
|
20141
|
+
const changeLine = `${p.change ? `\nEvery variant: ${p.change}` : ''}${p.changeNote ? `\n${p.changeNote}` : ''}`; // WHAT SHOULD CHANGE (2026-09-29), and what of it a variant cannot change
|
|
20136
20142
|
const lines = (p.variants || []).map((v, i) => `${i + 1}. ${v.label} — ${['character', 'outfit', 'location', 'objects'].filter(a => v[a] && v[a] !== 'same').map(a => a + ': ' + v[a]).join('; ')}`);
|
|
20137
20143
|
const quote = `~${p.perVariantCredits} credits per variant · ~${p.totalCredits} for ${(p.variants || []).length}`;
|
|
20138
|
-
if (dryRun) return { content: [{ type: 'text', text: `Plan (nothing rendered). Source: ${Object.entries(p.source || {}).map(([k, v]) => k + ': ' + v).join('; ')}\n${lines.join('\n')}\n${quote}. Run again without dryRun to render.` }], structuredContent: { plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits, dryRun: true } };
|
|
20144
|
+
if (dryRun) return { content: [{ type: 'text', text: `Plan (nothing rendered). Source: ${Object.entries(p.source || {}).map(([k, v]) => k + ': ' + v).join('; ')}\n${lines.join('\n')}${changeLine}\n${quote}. Run again without dryRun to render.` }], structuredContent: { plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits, dryRun: true } };
|
|
20139
20145
|
const jobs = [];
|
|
20140
20146
|
for (const v of (p.variants || [])) {
|
|
20141
20147
|
const job = await submitJob('multiply', { video: src, instruction: v.instruction, label: v.label }, { label: `Multiply · ${v.label}` });
|
|
20142
20148
|
jobs.push({ id: job.id, label: v.label });
|
|
20143
20149
|
}
|
|
20144
|
-
return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}
|
|
20150
|
+
return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}.${changeLine}\n${jobs.map((j, i) => `${i + 1}. ${j.label} → job ${j.id}`).join('\n')}\nEach variant is one keyframe edit plus a motion transfer of the whole source (2-6 minutes) and comes back the SAME length as the source. Call get_job with each id until it reports done; a variant has NO file until then.` }], structuredContent: { jobs, plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits } };
|
|
20145
20151
|
}));
|
|
20146
20152
|
|
|
20147
20153
|
// HOOK MULTIPLIER (2026-09-18): ONE finished video ad → N complete versions with NEW opening hooks, the rest of the ad and
|
|
@@ -20238,19 +20244,21 @@ function memoryNoteVerdict(text) {
|
|
|
20238
20244
|
// the engine; this wrapper only fills the brand's product photo. Held out of the default list on size (ON_DEMAND_TOOLS).
|
|
20239
20245
|
server.registerTool('recast_hook', {
|
|
20240
20246
|
title: 'Recast a viral hook with your subject',
|
|
20241
|
-
description: "RECAST A VIRAL HOOK
|
|
20247
|
+
description: "RECAST A VIRAL HOOK with the brand's own subject (the egg rolling off the table becomes your product; your host does the viral move), then join it into your clip. hook: a public post link (TikTok, Reel, Facebook, X, YouTube; one lookup) or a video file. Subject: creator (a saved creator's name or id), image (a product or a person; a real person's photo records their consent), or neither for the brand's first product photo. A DUO OR GROUP (2-4 people): subjects [{creator or image, position: 'left'|'right'|'center' or your own words, refs?, outfit?}], each bound to its place in the frame, keeping that performer's movement and timing on the original sound. LOCATION AND OUTFITS, same pass: location (a place photo or words) moves the scene; outfit (a photo or words) re-dresses the subject, subjects[].outfit each person; with no subject named nobody is swapped and only those change (useBrand:true also swaps in the brand product). Pictures are references, 4 in all (people or product, then location, then outfits); past 4 it is refused by name: put one in words. FULL LENGTH: past the editor's one pass (15 s with a face) the window is recast in passes, each continuing from the previous one's frame and crossfaded across the cut, under the original sound: one video up to 60 s, quoted as passes plus join; the reply names the moment to watch. mode 'auto' (default): 'swap' is a region edit that keeps camera, set, light, timing, on-screen text and sound; 'motion' makes your person perform the hook's moves (needs a person in the hook; not with location or outfit). swap: your own words for what changes. start/end pick the window (3 s minimum; default: a group or a location/outfit change takes the whole clip, one subject the first 10 s). PAID, QUOTED FIRST: without confirm it returns the plan and credits; call again with confirm:true once the user okays it. Then get_job until done, and post_edit with videoUrl = the result and ops [{op:'join', clips:[{url: your clip}], bridge:{kind:'impact'}}].",
|
|
20242
20248
|
inputSchema: {
|
|
20243
20249
|
hook: z.string().describe('the viral hook: a post link or a video URL'),
|
|
20244
20250
|
creator: z.string().optional().describe("a saved creator's name or id (list_creators)"),
|
|
20245
20251
|
image: z.string().optional().describe('a picture of the subject instead: a product or a person'),
|
|
20246
|
-
subjects: z.array(z.object({ creator: z.string().optional().describe("a saved creator's name or id"), image: z.string().optional().describe('or a picture of this person'), position: z.string().optional().describe("where they are in the hook: 'left', 'right', 'center', or your own words ('who raps first'); all or none"), refs: z.array(z.string()).optional().describe("up to 3 more views of the SAME person (a saved creator's pose plates ride automatically)"), name: z.string().optional() }).passthrough()).max(4).optional().describe('a duo or group recast, 2-4 people, instead of creator/image'),
|
|
20252
|
+
subjects: z.array(z.object({ creator: z.string().optional().describe("a saved creator's name or id"), image: z.string().optional().describe('or a picture of this person'), position: z.string().optional().describe("where they are in the hook: 'left', 'right', 'center', or your own words ('who raps first'); all or none"), refs: z.array(z.string()).optional().describe("up to 3 more views of the SAME person (a saved creator's pose plates ride automatically)"), name: z.string().optional(), outfit: z.string().optional().describe('what this person wears instead: an outfit photo URL or words; alone (no creator/image) it keeps this person and changes only the clothes') }).passthrough()).max(4).optional().describe('a duo or group recast, 2-4 people, instead of creator/image'),
|
|
20253
|
+
location: z.string().optional().describe('move the scene: a photo URL of the place, or words ("a sunny beach at golden hour")'),
|
|
20254
|
+
outfit: z.string().optional().describe('what the subject (or, with no subject, the main person in the hook) wears instead: an outfit photo URL or words'),
|
|
20247
20255
|
swap: z.string().optional().describe('what changes, in your own words'),
|
|
20248
20256
|
mode: z.enum(['auto', 'swap', 'motion']).optional(),
|
|
20249
20257
|
start: z.number().optional(), end: z.number().optional(),
|
|
20250
20258
|
prompt: z.string().optional().describe('extra direction for the render'),
|
|
20251
20259
|
model: z.string().optional().describe('swap: a clip editor id (hermoso_capabilities); omit for the default'),
|
|
20252
20260
|
tier: z.enum(['pro', 'standard']).optional().describe('motion: the motion-transfer tier (default pro)'),
|
|
20253
|
-
useBrand: z.boolean().optional().describe('false = no brand product photo'),
|
|
20261
|
+
useBrand: z.boolean().optional().describe('false = no brand product photo; true with location/outfit and no subject = also swap in the brand product'),
|
|
20254
20262
|
confirm: z.boolean().optional().describe('true = render it (after the user okayed the quote)'),
|
|
20255
20263
|
},
|
|
20256
20264
|
outputSchema: { plan: z.any().optional(), job: z.any().optional(), credits: z.number().optional(), queued: z.boolean().optional() },
|
|
@@ -20258,12 +20266,13 @@ function memoryNoteVerdict(text) {
|
|
|
20258
20266
|
}, wrap(async (a) => {
|
|
20259
20267
|
let b = {};
|
|
20260
20268
|
const subs = Array.isArray(a.subjects) ? a.subjects.filter(Boolean) : [];
|
|
20261
|
-
|
|
20269
|
+
const sceneOnly = !a.creator && !a.image && !subs.some((x) => x && (x.creator || x.image)) && (a.location || a.outfit || subs.some((x) => x && x.outfit)); // a location / outfit change with nobody swapped
|
|
20270
|
+
if (a.useBrand !== false && !a.creator && !a.image && !subs.length && (!sceneOnly || a.useBrand === true)) { try { b = (await readStore('heist.brand.v1')) || {}; } catch { b = {}; } }
|
|
20262
20271
|
// a person's photo as the subject is recorded as implied likeness consent SERVER-side, only when the free face check finds a face there (a product photo is not a likeness)
|
|
20263
|
-
const body = { hook: a.hook, ...(a.creator ? { creator: a.creator } : {}), ...(a.image ? { image: a.image } : {}), ...(subs.length ? { subjects: subs } : {}), ...(a.swap ? { swap: a.swap } : {}), ...(a.mode && a.mode !== 'auto' ? { mode: a.mode } : {}), ...(a.start != null ? { start: a.start } : {}), ...(a.end != null ? { end: a.end } : {}), ...(a.prompt ? { prompt: a.prompt } : {}), ...(a.model ? { model: a.model } : {}), ...(a.tier ? { tier: a.tier } : {}), ...(b && Array.isArray(b.productImages) && b.productImages.length ? { brand: { name: b.name || '', product: b.sells || b.name || '', productImages: b.productImages.slice(0, 1) } } : {}), ...(a.confirm === true ? { confirm: true } : {}) };
|
|
20272
|
+
const body = { hook: a.hook, ...(a.creator ? { creator: a.creator } : {}), ...(a.image ? { image: a.image } : {}), ...(subs.length ? { subjects: subs } : {}), ...(a.location ? { location: a.location } : {}), ...(a.outfit ? { outfit: a.outfit } : {}), ...(sceneOnly && a.useBrand === true ? { useBrand: true } : {}), ...(a.swap ? { swap: a.swap } : {}), ...(a.mode && a.mode !== 'auto' ? { mode: a.mode } : {}), ...(a.start != null ? { start: a.start } : {}), ...(a.end != null ? { end: a.end } : {}), ...(a.prompt ? { prompt: a.prompt } : {}), ...(a.model ? { model: a.model } : {}), ...(a.tier ? { tier: a.tier } : {}), ...(b && Array.isArray(b.productImages) && b.productImages.length ? { brand: { name: b.name || '', product: b.sells || b.name || '', productImages: b.productImages.slice(0, 1) } } : {}), ...(a.confirm === true ? { confirm: true } : {}) };
|
|
20264
20273
|
const r = await apiPost('/api/hooks/recast', body);
|
|
20265
20274
|
const p = r?.data || r;
|
|
20266
|
-
const head = `${p.mode === 'motion' ? 'Motion transfer' : 'Swap'} on ${p.engine}: ${p.hook?.window?.seconds}s of the hook (${p.hook?.window?.start}-${p.hook?.window?.end}s of ${p.hook?.durationSeconds}s). ${Array.isArray(p.subjects) ? `Subjects: ${p.subjects.map((x) => `${x.element} ${x.position}${x.name ? ` "${x.name}"` : ''} (${x.kind}${x.refs ? `, ${x.refs} more view${x.refs === 1 ? '' : 's'}` : ''})`).join('; ')}` : `Subject: ${p.subject?.kind}${p.subject?.name ? ` "${p.subject.name}"` : ''}`}. Face in the hook: ${p.hook?.face}; in the subject: ${p.subject?.face}. Why: ${p.why}.${Array.isArray(p.passes) ? `\nPasses: ${p.passes.map((w) => `${w.start}-${w.end}s${w.credits != null ? ` (${w.credits} credits)` : ''}`).join(', ')}; join${p.join?.credits != null ? ` (${p.join.credits} credits)` : ''}: ${p.join?.how || 'a hard cut'}.` : ''}${p.windowNote ? `\n${p.windowNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
20275
|
+
const head = `${p.mode === 'motion' ? 'Motion transfer' : 'Swap'} on ${p.engine}: ${p.hook?.window?.seconds}s of the hook (${p.hook?.window?.start}-${p.hook?.window?.end}s of ${p.hook?.durationSeconds}s). ${Array.isArray(p.subjects) ? `Subjects: ${p.subjects.map((x) => `${x.element} ${x.position}${x.name ? ` "${x.name}"` : ''} (${x.kind}${x.refs ? `, ${x.refs} more view${x.refs === 1 ? '' : 's'}` : ''})`).join('; ')}` : (p.subject?.kind === 'kept' ? 'Nobody is swapped' : `Subject: ${p.subject?.kind}${p.subject?.name ? ` "${p.subject.name}"` : ''}`)}${p.location ? `; location: ${p.location.kind === 'photo' ? p.location.image : p.location.text}` : ''}${p.outfit ? `; outfit: ${p.outfit.kind === 'photo' ? p.outfit.image : p.outfit.text}` : ''}${Array.isArray(p.outfits) ? `; outfits: ${p.outfits.map((o) => `${o.who}: ${o.kind === 'photo' ? o.image : o.text}`).join('; ')}` : ''}. Face in the hook: ${p.hook?.face}; in the subject: ${p.subject?.face}. Why: ${p.why}.${Array.isArray(p.passes) ? `\nPasses: ${p.passes.map((w) => `${w.start}-${w.end}s${w.credits != null ? ` (${w.credits} credits)` : ''}`).join(', ')}; join${p.join?.credits != null ? ` (${p.join.credits} credits)` : ''}: ${p.join?.how || 'a hard cut'}.` : ''}${p.windowNote ? `\n${p.windowNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
20267
20276
|
if (!p.queued) return { content: [{ type: 'text', text: `QUOTE (nothing rendered, nothing charged): about ${p.credits} credits.\n${head}\nTell the user the price; after they okay it, call recast_hook again with the same inputs and confirm:true.` }], structuredContent: { plan: p, credits: p.credits, queued: false } };
|
|
20268
20277
|
return { content: [{ type: 'text', text: `Recast queued as job ${p.job?.id} (about ${p.credits} credits).\n${head}\nCall get_job until it reports done; it has no file until then. ${p.next || ''}` }], structuredContent: { plan: p, job: p.job, credits: p.credits, queued: true } };
|
|
20269
20278
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.359",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 864 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 and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. 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",
|