hermoso 0.1.63 → 0.1.64

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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +13 -4
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -7600,17 +7600,22 @@ export function registerTools(rawServer, opts = {}) {
7600
7600
 
7601
7601
  server.registerTool('set_product_image', {
7602
7602
  title: 'Set product photo',
7603
- description: "Lock an image as the ad's real PRODUCT photo so every render grounds on the true packaging. Pass `imageUrl` = a product shot's URL — an image from a prior research result (an organic Instagram/TikTok post, a scraped page image), a workspace / list_product_photos url, or any public product photo. The server downloads it and runs a product+safety check: a lifestyle/scene shot with no clear product, or an off-category / unsafe image, is REJECTED and NOTHING is locked (the summary says why). On PASS it persists the photo to a DURABLE url and returns it pass that url as a reference to generate_image / render_ad. Bills one vision check. Reads YOUR saved brand for the category match (pass brandId to target a specific brand — switches this key's active brand like use_brand).",
7603
+ description: "Lock an image as the ad's real PRODUCT photo and SAVE it as this brand's default product, so every later plan_ad / render_ad / generate_image grounds on the true packaging without being told again. Pass `imageUrl` = a product shot's URL — an image from a prior research result (an organic Instagram/TikTok post, a scraped page image), a workspace / list_product_photos url, or any public product photo. The server downloads it and runs a product+safety check: a lifestyle/scene shot with no clear product, or an off-category / unsafe image, is REJECTED and NOTHING is locked or saved (the summary says why). On PASS it persists the photo to a DURABLE url, writes it to the brand's product library as the DEFAULT, and READS THE BRAND BACK to confirm `savedToBrand` and the summary report what the brand ACTUALLY holds now, never what was asked for, so if it did not become the default you are told instead of finding out from a paid render. Bills one vision check. Reads YOUR saved brand for the category match (pass brandId to target a specific brand — switches this key's active brand like use_brand).",
7604
7604
  inputSchema: {
7605
7605
  imageUrl: z.string().describe('the image URL to lock as the product (from a research result, a workspace / list_product_photos url, or any public product photo)'),
7606
7606
  source_note: z.string().optional().describe('a short note on where it came from, e.g. "from their IG post"'),
7607
7607
  brandId: z.string().optional().describe('a brand id/name from list_brands to lock the product for; omit to use the active brand'),
7608
7608
  },
7609
7609
  outputSchema: {
7610
- attached: z.boolean().optional().describe('true when the image passed the product check and was locked'),
7611
- summary: z.string().optional().describe('the check verdict on rejection, why nothing was locked'),
7610
+ attached: z.boolean().optional().describe('true when the image passed the product check and a durable url was minted'),
7611
+ savedToBrand: z.boolean().optional().describe('THE READ-BACK: true only when the brand row came back naming this photo as its default product. False means a render will NOT use it — see defaultProduct'),
7612
+ defaultProduct: z.string().nullable().optional().describe("the brand's default product photo as READ BACK from the store — the photo a render will actually ground on"),
7613
+ productImages: z.array(z.string()).nullable().optional().describe("the brand's product library as read back, newest first"),
7614
+ summary: z.string().optional().describe('the check verdict plus the read-back — on rejection, why nothing was locked; on a failed save, what a render would use instead'),
7612
7615
  url: z.string().nullable().optional().describe('the durable served URL of the locked product photo'),
7613
7616
  source_note: z.string().nullable().optional().describe('where the photo came from'),
7617
+ unconfirmed: z.string().nullable().optional().describe('set when the save landed but the confirming read failed — neither saved nor failed; verify with list_product_photos'),
7618
+ saveError: z.string().nullable().optional().describe('set when the photo passed the check but could NOT be saved to the brand'),
7614
7619
  },
7615
7620
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
7616
7621
  }, wrap(async ({ imageUrl, source_note, brandId }) => {
@@ -7619,7 +7624,11 @@ export function registerTools(rawServer, opts = {}) {
7619
7624
  if (!d.attached) return ok(d.summary || 'That image was not locked as the product.', d); // gate honesty: rejected → nothing attached
7620
7625
  const url = abs(d.url);
7621
7626
  const img = await imageBlock(url); // show the locked product inline
7622
- return { content: [{ type: 'text', text: `${d.summary}\nProduct photo: ${url}` }, ...(img ? [img] : [])], structuredContent: { ...d, url } };
7627
+ // THE ANSWER IS THE READ-BACK. `d.summary` already carries it (productLockReadbackNote, server-side, one copy for
7628
+ // every surface). A photo that passed the check but did NOT become the brand's default must not be shown as a
7629
+ // finished job, so the headline is the read-back verdict rather than the ask — same law the ads tree follows.
7630
+ const head = d.savedToBrand ? 'Locked as this brand\u2019s product photo' : d.unconfirmed ? 'Checked and saved \u2014 but NOT confirmed' : 'Checked, but NOT saved as the brand\u2019s product photo';
7631
+ return { content: [{ type: 'text', text: `${head}.\n${d.summary}\nProduct photo: ${url}` }, ...(img ? [img] : [])], structuredContent: { ...d, url } };
7623
7632
  }));
7624
7633
 
7625
7634
  // APP SCREENS — the one asset class a headless caller could never acquire after onboarding. `draft_brand` pulls them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI \u2014 spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus TikTok/Instagram/YouTube organic \u2014 then publish to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest and build & manage the ad campaigns behind them on Meta, Google Ads, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads. MCP server (344 tools), CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
6
6
  "type": "module",