hermoso 0.1.180 → 0.1.182

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 +22 -16
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -2901,17 +2901,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
2901
2901
  return ok(`Brands on this account:\n${lines}\n\nSwitch with use_brand.${sharedTxt}`, { ...d, sharedWorkspaces: shared });
2902
2902
  }));
2903
2903
 
2904
- server.registerTool('use_brand', {
2905
- title: 'Switch brand',
2906
- description: "Pin which brand this connection acts on (multi-brand accounts). Pass the brand id or exact name from list_brands. Works for a brand on your own account AND for a workspace another account SHARED with you — for a shared one pass its name or the profile id list_brands prints, and access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, no restart).",
2907
- inputSchema: { brand: z.string().describe('brand id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of a workspace shared with you') },
2908
- outputSchema: {
2909
- ok: z.boolean().optional().describe('true when the brand switch persisted'),
2910
- brand: z.any().optional().describe('the now-active brand ({id, name})'),
2911
- shared: z.boolean().optional().describe('true when the pinned workspace is owned by another account'),
2912
- },
2913
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2914
- }, wrap(async ({ brand }) => {
2904
+ const makeUseBrandHandler = (ctx) => wrap(async ({ brand }) => {
2915
2905
  const d = await apiGet('/api/brands');
2916
2906
  const want = String(brand || '').trim().toLowerCase();
2917
2907
  const hit = (d.brands || []).find(b => b.id.toLowerCase() === want || String(b.name || '').toLowerCase() === want);
@@ -2947,7 +2937,23 @@ function buildTools(rawServer, opts = {}, sink = null) {
2947
2937
  const own = (d.brands || []).map(b => `• ${b.name} (id: ${b.id})`).join('\n');
2948
2938
  const shr = shared.map(w => `• ${w.name || 'Shared workspace'}${w.ownerName ? ` — ${w.ownerName}` : ''} (id: ${w.profileUuid}, shared with you)`).join('\n');
2949
2939
  return { content: [{ type: 'text', text: `No brand matching "${brand}". Available:\n${own}${shr ? `\n${shr}` : ''}` }], isError: true };
2950
- }));
2940
+ });
2941
+ server.registerTool('use_brand', {
2942
+ title: 'Switch brand',
2943
+ description: "Pin which brand this connection acts on (multi-brand accounts). Pass the brand id or exact name from list_brands. Works for a brand on your own account AND for a workspace another account SHARED with you — for a shared one pass its name or the profile id list_brands prints, and access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, no restart).",
2944
+ inputSchema: { brand: z.string().describe('brand id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of a workspace shared with you') },
2945
+ outputSchema: {
2946
+ ok: z.boolean().optional().describe('true when the brand switch persisted'),
2947
+ brand: z.any().optional().describe('the now-active brand ({id, name})'),
2948
+ shared: z.boolean().optional().describe('true when the pinned workspace is owned by another account'),
2949
+ },
2950
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2951
+ // PER-SESSION, LIKE enable_tools (2026-09-01): this handler re-gates `ctx.handleOf` — the roster of THE SESSION THAT
2952
+ // CALLED IT. A plain closure captured the ctx of the session that BUILT the canon (the first in the process), so every
2953
+ // later session got the right sentence ("re-scoped to 28 connectors") and the wrong roster: measured on prod, a
2954
+ // session pinned to a 0-connector brand switched to one with 28 and still listed 113 tools; `enable_tools` in the
2955
+ // same session said "3 more". It "worked" the morning it shipped because that measurement WAS the first session.
2956
+ }, sessionBound(makeUseBrandHandler, ctx));
2951
2957
 
2952
2958
  // ── BRAND WORKSPACE LIFECYCLE. draft_brand OVERWRITES the active workspace's brand; it does not mint one — so
2953
2959
  // without these an agency running Hermoso purely through an agent could never take on a second client.
@@ -16868,10 +16874,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
16868
16874
  }));
16869
16875
 
16870
16876
  // AD MULTIPLIER (2026-09-01): ONE winning ad → N variants (new character / outfit / location / objects) with the edit, the
16871
- // motion and the ORIGINAL AUDIO untouched. The plan is one server call; every variant is an ordinary videoedit job.
16877
+ // 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).
16872
16878
  server.registerTool('multiply_ad', {
16873
16879
  title: 'Multiply an ad',
16874
- 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). Sources up to 15 seconds are edited as they are (a 15s spot works); longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:15}] — 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.",
16880
+ 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.",
16875
16881
  inputSchema: {
16876
16882
  video: z.string().describe('the source video URL'),
16877
16883
  count: z.number().optional().describe('how many variants, 1-12 (default 6)'),
@@ -16893,10 +16899,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
16893
16899
  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 } };
16894
16900
  const jobs = [];
16895
16901
  for (const v of (p.variants || [])) {
16896
- const job = await submitJob('videoedit', { video: src, prompt: v.instruction, keepAudio: true }, { label: `Multiply · ${v.label}` });
16902
+ const job = await submitJob('multiply', { video: src, instruction: v.instruction, label: v.label }, { label: `Multiply · ${v.label}` });
16897
16903
  jobs.push({ id: job.id, label: v.label });
16898
16904
  }
16899
- return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}.\n${jobs.map((j, i) => `${i + 1}. ${j.label} → job ${j.id}`).join('\n')}\nEach is a normal video edit (1-4 minutes). 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 } };
16905
+ return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}.\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 } };
16900
16906
  }));
16901
16907
 
16902
16908
  server.registerTool('dub_video', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.180",
3
+ "version": "0.1.182",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 745 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. 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",