@hubfluencer/mcp 0.7.1 → 0.8.2

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/dist/index.js CHANGED
@@ -29489,7 +29489,7 @@ function normalizeStatus(kind, slug, data) {
29489
29489
  const videoUrl = latest.video_url ?? null;
29490
29490
  if (kind === "short") {
29491
29491
  const stage = d.stage ?? "unknown";
29492
- return {
29492
+ const status = {
29493
29493
  kind,
29494
29494
  slug,
29495
29495
  stage,
@@ -29498,6 +29498,13 @@ function normalizeStatus(kind, slug, data) {
29498
29498
  video_url: videoUrl,
29499
29499
  error: d.error_message ?? null
29500
29500
  };
29501
+ if (typeof d.latest_render_free === "boolean") {
29502
+ status.latest_render_free = d.latest_render_free;
29503
+ }
29504
+ if (typeof d.last_free_rerender_failed === "boolean") {
29505
+ status.last_free_rerender_failed = d.last_free_rerender_failed;
29506
+ }
29507
+ return status;
29501
29508
  }
29502
29509
  if (kind === "tracking") {
29503
29510
  const result = asRecord(d.result);
@@ -30120,7 +30127,7 @@ function readStoredCredentials() {
30120
30127
  // package.json
30121
30128
  var package_default = {
30122
30129
  name: "@hubfluencer/mcp",
30123
- version: "0.7.1",
30130
+ version: "0.8.2",
30124
30131
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30125
30132
  license: "MIT",
30126
30133
  author: "Monocursive <contact@monocursive.com>",
@@ -30429,7 +30436,9 @@ var getStatusOutput = exports_external.object({
30429
30436
  terminal: exports_external.boolean().optional(),
30430
30437
  ready: exports_external.boolean().optional(),
30431
30438
  video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30432
- error: exports_external.union([exports_external.string(), exports_external.null()]).optional()
30439
+ error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30440
+ latest_render_free: exports_external.boolean().optional(),
30441
+ last_free_rerender_failed: exports_external.boolean().optional()
30433
30442
  }).passthrough();
30434
30443
  var waitForCompletionOutput = exports_external.object({
30435
30444
  kind: exports_external.string().optional(),
@@ -30439,6 +30448,8 @@ var waitForCompletionOutput = exports_external.object({
30439
30448
  ready: exports_external.boolean().optional(),
30440
30449
  video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30441
30450
  error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30451
+ latest_render_free: exports_external.boolean().optional(),
30452
+ last_free_rerender_failed: exports_external.boolean().optional(),
30442
30453
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30443
30454
  timed_out: exports_external.boolean().optional()
30444
30455
  }).passthrough();
@@ -30482,7 +30493,9 @@ var projectRow = exports_external.object({
30482
30493
  terminal: exports_external.boolean().optional(),
30483
30494
  ready: exports_external.boolean().optional(),
30484
30495
  video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30485
- error: exports_external.union([exports_external.string(), exports_external.null()]).optional()
30496
+ error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
30497
+ latest_render_free: exports_external.boolean().optional(),
30498
+ last_free_rerender_failed: exports_external.boolean().optional()
30486
30499
  }).passthrough();
30487
30500
  var listProjectsOutput = exports_external.object({
30488
30501
  shorts: exports_external.array(projectRow).optional(),
@@ -31454,7 +31467,7 @@ var SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product pag
31454
31467
 
31455
31468
  MAKE ONE ASSET (simplest)
31456
31469
  - make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_short / create_editor_ad are the per-product one-shots; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
31457
- - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → set_scene_count → set_segment_prompt → generate_segment(s) → generate_voice + generate_music → render). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
31470
+ - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenarioreview/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
31458
31471
 
31459
31472
  DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
31460
31473
  1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
@@ -31467,7 +31480,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
31467
31480
  8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
31468
31481
 
31469
31482
  CREDITS (spent server-side; each tool prices before charging — see its description)
31470
- - Free (0 credits): every draft create/edit, reorder, all slider re-composites, and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31483
+ - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31471
31484
  - Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video/create_* report the estimate first — pass dry_run to preview, max_credits to cap.
31472
31485
  - AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
31473
31486
 
@@ -31520,7 +31533,7 @@ async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, i
31520
31533
  }
31521
31534
  registerTool("make_video", {
31522
31535
  title: "Make a video from a prompt (one shot)",
31523
- description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish. " + "BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " + "music_vibe so a one-shot short isn't bare. creative_format + visual_language apply to BOTH kinds (editor and " + "shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " + "editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " + "For richer branding — a product image, brand logo, or " + "closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " + "create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
31536
+ description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish. " + "BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " + "music_vibe so a one-shot short isn't bare. Always set cta_text — a short without a CTA doesn't convert " + "(plus offer_text only if there's a real deal). Note: blank cta_text → the server renders a neutral " + "localized ask (e.g. 'Learn more'); a truly CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language " + "explicitly (kinetic_creator is a strong default). If headline/subheadline/text_beats are left blank the " + "server auto-writes the headline + caption beats at generate time; pass skip_auto_text:true for a " + "deliberately bare clip. creative_format + visual_language apply to BOTH kinds (editor and " + "shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " + "editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " + "For richer branding — a product image, brand logo, or " + "closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " + "create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short. " + "For a SHORT, iterating after the first render is FREE: update_short (copy/style/CTA/end-card) → " + "rerender_short re-renders over the already-paid footage for 0 credits.",
31524
31537
  inputSchema: {
31525
31538
  prompt: exports_external.string().describe("What the ad/video should be about (min 10 chars)"),
31526
31539
  kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
@@ -31529,16 +31542,17 @@ registerTool("make_video", {
31529
31542
  language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag ' + '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.'),
31530
31543
  aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
31531
31544
  voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
31532
- headline: exports_external.string().optional().describe("SHORTS only: the on-screen TITLE overlay (160). Set this so the short isn't bare."),
31545
+ headline: exports_external.string().max(80).optional().describe("SHORTS only: the on-screen TITLE overlay. ≈4 words; 40 chars hits hardest; hard cap 80 — " + "longer hooks wrap past the safe area. Set this so the short isn't bare."),
31533
31546
  subheadline: exports_external.string().optional().describe("SHORTS only: the secondary title / supporting line (≤200)"),
31534
31547
  text_beats: exports_external.array(exports_external.string()).optional().describe("SHORTS only: caption beats shown sequentially instead of a static subheadline (≤8, each ≤120 chars)."),
31535
31548
  headline_color: exports_external.string().optional().describe("SHORTS only: headline hex color, e.g. #ffffff"),
31536
31549
  subheadline_color: exports_external.string().optional().describe("SHORTS only: subheadline hex color, e.g. #ffffff"),
31537
31550
  accent_color: exports_external.string().optional().describe("SHORTS only: 6-digit brand accent hex color, e.g. #09EFBE"),
31538
31551
  offer_text: exports_external.string().optional().describe("SHORTS only: optional offer chip, e.g. -40% (≤16 chars)"),
31539
- cta_text: exports_external.string().optional().describe("SHORTS only: optional CTA pill, e.g. Shop now (≤24 chars)"),
31540
- badge_text: exports_external.string().optional().describe("SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)"),
31541
- star_rating: exports_external.number().int().min(0).max(5).optional().describe("SHORTS only: optional 0..5 star rating under the subheadline"),
31552
+ cta_text: exports_external.string().optional().describe("SHORTS only: CTA pill, e.g. Shop now (≤24 chars). Always set it — a short without a CTA doesn't convert."),
31553
+ badge_text: exports_external.string().optional().describe("SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars). Only badges you can " + "substantiate — fabricated social proof is deceptive advertising."),
31554
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("SHORTS only: optional 0..5 star rating under the subheadline. Only ratings you can " + "substantiate — fabricated social proof is deceptive advertising."),
31555
+ skip_auto_text: exports_external.boolean().optional().describe("SHORTS only: when headline/subheadline/text_beats are blank the server auto-writes the copy at " + "generate time; pass true to skip that and render a deliberately bare clip (default false)."),
31542
31556
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("SHORTS only: title overlay position (default bottom)"),
31543
31557
  text_animation: exports_external.enum([
31544
31558
  "auto",
@@ -31622,7 +31636,7 @@ registerTool("make_video", {
31622
31636
  creative_format: args.creative_format,
31623
31637
  visual_language: args.visual_language,
31624
31638
  theme: args.theme
31625
- }, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.creative_format ?? "", args.visual_language ?? "", args.theme ?? ""));
31639
+ }, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.skip_auto_text ? "bare" : "auto-copy"));
31626
31640
  slug = created.data.slug;
31627
31641
  } else {
31628
31642
  const created = await client.post("/editor", {
@@ -31646,7 +31660,7 @@ registerTool("make_video", {
31646
31660
  estimated_credits = typeof cost.total === "number" ? cost.total : null;
31647
31661
  available_credits = typeof cost.available_credits === "number" ? cost.available_credits : null;
31648
31662
  } catch {}
31649
- const resume = kind === "short" ? `generate_short({ slug: "${slug}" })` : `start_autopilot({ slug: "${slug}" })`;
31663
+ const resume = kind === "short" ? `generate_short({ slug: "${slug}"${args.skip_auto_text ? ", skip_auto_text: true" : ""} })` : `start_autopilot({ slug: "${slug}" })`;
31650
31664
  const affordable = available_credits === null || estimated_credits === null || available_credits >= estimated_credits;
31651
31665
  const overCap = args.max_credits != null && estimated_credits != null && estimated_credits > args.max_credits;
31652
31666
  if (args.dry_run || overCap || !affordable) {
@@ -31668,7 +31682,7 @@ registerTool("make_video", {
31668
31682
  startState = startStateDiscriminator(kind, asRecord(cur).data);
31669
31683
  } catch {}
31670
31684
  if (kind === "short") {
31671
- await client.post(`/shorts/${slug}/generate`, undefined, `gen-short:${slug}:${startState}`);
31685
+ await client.post(`/shorts/${slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined, `gen-short:${slug}:${startState}${args.skip_auto_text ? ":bare" : ""}`);
31672
31686
  } else {
31673
31687
  await client.post(`/editor/${slug}/autopilot`, undefined, makeVideoAutopilotKey(slug, args, startState));
31674
31688
  }
@@ -31754,11 +31768,11 @@ registerTool("list_projects", {
31754
31768
  }));
31755
31769
  registerTool("create_short", {
31756
31770
  title: "Create a short (draft)",
31757
- description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when an end-card poster is set). Returns the slug, " + "costs 0 credits. Follow with generate_short to render. " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
31771
+ description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
31758
31772
  inputSchema: {
31759
31773
  product_prompt: exports_external.string().min(10).describe("What the short should be about"),
31760
31774
  language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
31761
- headline: exports_external.string().max(160).optional().describe("The on-screen TITLE composited over the short (poster text overlay). ≤160 chars. Set this."),
31775
+ headline: exports_external.string().max(80).optional().describe("The on-screen TITLE composited over the short (poster text overlay). ≈4 words; 40 chars hits " + "hardest; hard cap 80 — longer hooks wrap past the safe area. Set this."),
31762
31776
  subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
31763
31777
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto."),
31764
31778
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling. Good default: kinetic_creator."),
@@ -31781,10 +31795,13 @@ registerTool("create_short", {
31781
31795
  subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
31782
31796
  accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
31783
31797
  offer_text: exports_external.string().max(16).optional().describe('Optional offer chip, e.g. "-40%" or "2 FOR 1"'),
31784
- cta_text: exports_external.string().max(24).optional().describe('Optional CTA pill, e.g. "Shop now"'),
31785
- badge_text: exports_external.string().max(24).optional().describe('Optional badge stamp, e.g. "BEST SELLER"'),
31786
- star_rating: exports_external.number().int().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline"),
31798
+ cta_text: exports_external.string().max(24).optional().describe(`CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn't convert.`),
31799
+ badge_text: exports_external.string().max(24).optional().describe('Optional badge stamp, e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' + "proof is deceptive advertising."),
31800
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline. Only ratings you can substantiate — fabricated " + "social proof is deceptive advertising."),
31787
31801
  text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Optional caption beats shown sequentially instead of the static subheadline."),
31802
+ closing_claim: exports_external.string().max(80).optional().describe("End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " + "claim. Falls back to the headline when unset."),
31803
+ brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + "poster is set. Server falls back to your brand profile's name when unset."),
31804
+ end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
31788
31805
  brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Its creative format, visual language, font, accent, and default CTA fill " + "any blank short fields and a snapshot is frozen onto the short. Must be one of your own profiles.")
31789
31806
  },
31790
31807
  outputSchema: createDraftOutput,
@@ -31831,23 +31848,29 @@ registerTool("create_short", {
31831
31848
  body.star_rating = args.star_rating;
31832
31849
  if (args.text_beats !== undefined)
31833
31850
  body.text_beats = args.text_beats;
31851
+ if (args.closing_claim !== undefined)
31852
+ body.closing_claim = args.closing_claim;
31853
+ if (args.brand_name !== undefined)
31854
+ body.brand_name = args.brand_name;
31855
+ if (args.end_card !== undefined)
31856
+ body.end_card = args.end_card;
31834
31857
  if (args.brand_profile_id !== undefined)
31835
31858
  body.brand_profile_id = args.brand_profile_id;
31836
- const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? []), String(args.brand_profile_id ?? "")));
31859
+ const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? []), args.closing_claim ?? "", args.brand_name ?? "", args.end_card ?? "", String(args.brand_profile_id ?? "")));
31837
31860
  return ok({
31838
31861
  slug: res.data.slug,
31839
31862
  kind: "short",
31840
- next: "set_short_product / set_short_poster (optional branding), then generate_short"
31863
+ next: "set_short_product / set_short_poster (optional branding); generate_hook_variations to test hook angles before you spend; then generate_short. After that first render, hook/CTA/style iteration is free: update_short → rerender_short (0 credits)."
31841
31864
  });
31842
31865
  }));
31843
31866
  registerTool("update_short", {
31844
31867
  title: "Update a short draft (0 credits)",
31845
- description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render (call generate_short afterwards to apply). Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
31868
+ description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
31846
31869
  inputSchema: {
31847
31870
  slug: exports_external.string().describe("Short slug (from create_short)"),
31848
31871
  product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
31849
31872
  language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en". Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
31850
- headline: exports_external.string().max(160).optional().describe("On-screen TITLE overlay. ≤160 chars."),
31873
+ headline: exports_external.string().max(80).optional().describe("On-screen TITLE overlay. ≈4 words; 40 chars hits hardest; hard cap 80 — longer hooks wrap " + "past the safe area."),
31851
31874
  subheadline: exports_external.string().max(200).optional().describe("Secondary title / supporting line. ≤200 chars."),
31852
31875
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal."),
31853
31876
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling."),
@@ -31870,10 +31893,14 @@ registerTool("update_short", {
31870
31893
  subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
31871
31894
  accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
31872
31895
  offer_text: exports_external.string().max(16).optional().describe('Offer chip (opt-in), e.g. "-40%". Pass "" to remove it.'),
31873
- cta_text: exports_external.string().max(24).optional().describe('CTA pill (opt-in), e.g. "Shop now". Pass "" to remove it.'),
31874
- badge_text: exports_external.string().max(24).optional().describe('Badge stamp (opt-in), e.g. "BEST SELLER". Pass "" to remove it.'),
31875
- star_rating: exports_external.number().int().min(0).max(5).optional().describe("0..5 star rating under the subheadline (opt-in). Pass 0 to remove it."),
31896
+ cta_text: exports_external.string().max(24).optional().describe(`CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn't convert. Pass "" to remove it.`),
31897
+ badge_text: exports_external.string().max(24).optional().describe('Badge stamp (opt-in), e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' + 'proof is deceptive advertising. Pass "" to remove it.'),
31898
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("0..5 star rating under the subheadline (opt-in). Only ratings you can substantiate — fabricated " + "social proof is deceptive advertising. Pass 0 to remove it."),
31876
31899
  text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Caption beats shown sequentially instead of the static subheadline. Pass [] to clear."),
31900
+ closing_claim: exports_external.string().max(80).optional().describe("End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " + 'claim. Falls back to the headline when unset. Pass "" to remove it.'),
31901
+ brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + `poster is set. Server falls back to your brand profile's name when unset. Pass "" to remove it.`),
31902
+ end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
31903
+ poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false)."),
31877
31904
  brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " + "on update it sets the linked profile. Must be one of your own profiles.")
31878
31905
  },
31879
31906
  annotations: {
@@ -31923,10 +31950,22 @@ registerTool("update_short", {
31923
31950
  body.star_rating = args.star_rating;
31924
31951
  if (args.text_beats !== undefined)
31925
31952
  body.text_beats = args.text_beats;
31953
+ if (args.closing_claim !== undefined)
31954
+ body.closing_claim = args.closing_claim;
31955
+ if (args.brand_name !== undefined)
31956
+ body.brand_name = args.brand_name;
31957
+ if (args.end_card !== undefined)
31958
+ body.end_card = args.end_card;
31959
+ if (args.poster_includes_lockup !== undefined)
31960
+ body.poster_includes_lockup = args.poster_includes_lockup;
31926
31961
  if (args.brand_profile_id !== undefined)
31927
31962
  body.brand_profile_id = args.brand_profile_id;
31928
31963
  const res = await client.patch(`/shorts/${args.slug}`, body);
31929
- return ok(asRecord(res).data ?? res);
31964
+ const data = asRecord(res).data ?? res;
31965
+ return ok({
31966
+ ...asRecord(data),
31967
+ next: "Saved. Only text/style/CTA/end-card changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
31968
+ });
31930
31969
  }));
31931
31970
  async function getTracking(client, slug, deadline) {
31932
31971
  const res = await client.get(`/tracking/${slug}`, undefined, deadline);
@@ -32066,12 +32105,15 @@ registerTool("create_tracking_video", {
32066
32105
  }));
32067
32106
  registerTool("generate_short", {
32068
32107
  title: "Generate (render) a short",
32069
- description: "Deducts 15 credits and starts the render pipeline for an existing short. Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. Then poll with get_status or wait_for_completion (kind=short).",
32070
- inputSchema: { slug: exports_external.string().describe("Short slug from create_short") },
32108
+ description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). Then poll with get_status or wait_for_completion (kind=short).",
32109
+ inputSchema: {
32110
+ slug: exports_external.string().describe("Short slug from create_short"),
32111
+ skip_auto_text: exports_external.boolean().optional().describe("When the draft has no headline/subheadline/text_beats the server auto-writes the copy; pass " + "true to skip that and render a deliberately bare clip (default false).")
32112
+ },
32071
32113
  annotations: { title: "Generate short", ...WRITE }
32072
32114
  }, tool(async (args, client) => {
32073
32115
  try {
32074
- const res = await client.post(`/shorts/${args.slug}/generate`);
32116
+ const res = await client.post(`/shorts/${args.slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined);
32075
32117
  const status = normalizeStatus("short", args.slug, asRecord(res).data);
32076
32118
  return ok(status);
32077
32119
  } catch (e) {
@@ -32089,6 +32131,44 @@ registerTool("generate_short", {
32089
32131
  throw e;
32090
32132
  }
32091
32133
  }));
32134
+ registerTool("rerender_short", {
32135
+ title: "Re-render a short (FREE — 0 credits)",
32136
+ description: "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; the pinned music bed is faded to fit rather than " + "regenerated — for perfectly synced music after such a change, use generate_short. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
32137
+ inputSchema: {
32138
+ slug: exports_external.string().describe("Short slug (from create_short)")
32139
+ },
32140
+ annotations: { title: "Re-render short", ...WRITE, idempotentHint: false }
32141
+ }, tool(async (args, client) => {
32142
+ try {
32143
+ const res = await client.post(`/shorts/${args.slug}/rerender`, undefined, idemKey("rerender-short", args.slug, randomUUID()));
32144
+ const status = normalizeStatus("short", args.slug, asRecord(res).data);
32145
+ return ok({
32146
+ ...status,
32147
+ next: `wait_for_completion({ slug: "${args.slug}", kind: "short" }) — or poll get_status. A re-render is fast (no AI generation).`
32148
+ });
32149
+ } catch (e) {
32150
+ const he = e;
32151
+ if (he.status === 409) {
32152
+ return ok({
32153
+ kind: "short",
32154
+ slug: args.slug,
32155
+ stage: "processing",
32156
+ terminal: false,
32157
+ ready: false,
32158
+ video_url: null,
32159
+ error: null,
32160
+ next: `A render is already running — wait_for_completion({ slug: "${args.slug}", kind: "short" }).`
32161
+ });
32162
+ }
32163
+ if (he.status === 422 && he.code === "short_not_ready") {
32164
+ return fail("This short has no completed footage + music to re-render over (short_not_ready). " + `Run generate_short({ slug: "${args.slug}" }) once (15 credits) — after that first render, every ` + "text/style/CTA/end-card tweak re-renders free with rerender_short.");
32165
+ }
32166
+ if (he.status === 422 && he.code === "short_paid_regeneration_required") {
32167
+ return fail("A footage or music input changed after the last paid render " + "(short_paid_regeneration_required). The pinned assets no longer match the draft. " + `Run generate_short({ slug: "${args.slug}" }) (15 credits) before using free re-renders again.`);
32168
+ }
32169
+ throw e;
32170
+ }
32171
+ }));
32092
32172
  registerTool("generate_short_text", {
32093
32173
  title: "Generate short overlay text (AI assist)",
32094
32174
  description: "Generates editable headline, subheadline, and caption beats for an existing short draft. " + "CONSUMES 1 AI ASSIST (free daily quota) and spends no video credits. The server reads the saved " + "short fields, so update/create the draft with the current product prompt, creative_format, visual_language, " + "language, and seed copy first. On 429 set auto_unlock:true (1 credit -> +10 assists, retried once) or write the text yourself.",
@@ -32294,7 +32374,7 @@ registerTool("edit_slider_slide", {
32294
32374
  }));
32295
32375
  registerTool("create_editor_ad", {
32296
32376
  title: "Create a multi-scene editor ad (autopilot)",
32297
- description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
32377
+ description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " + "For product_only or presenter_led cast modes, use create_editor_draft and configure the required " + "product/presenter before start_autopilot; this one-shot tool intentionally uses automatic casting.",
32298
32378
  inputSchema: {
32299
32379
  product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
32300
32380
  product_subject: exports_external.string().max(300).optional().describe('The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green ' + 'bottle with a gold label"). Distinct from the freeform brief: hard-grounds the AI scenario + narration ' + "so it cannot invent an unrelated product. Strongly recommended for ads."),
@@ -32391,6 +32471,7 @@ registerTool("create_editor_draft", {
32391
32471
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32392
32472
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
32393
32473
  theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
32474
+ cast_mode: exports_external.enum(["product_only", "presenter_led"]).optional().describe("Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " + "product image (set_product) before generation and generates every scene from persistent product " + "reference images — the best product fidelity. presenter_led = one locked presenter appears in " + "every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " + "— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " + "behavior."),
32394
32475
  voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).optional().describe("Preferred narration voice id (see list_voices); ≤64 chars, [A-Za-z0-9_-] only"),
32395
32476
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32396
32477
  project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("social_ad for promotional/ad content (product focus + CTA), creative_story for brand " + "narratives (default).")
@@ -32409,17 +32490,18 @@ registerTool("create_editor_draft", {
32409
32490
  creative_format: args.creative_format,
32410
32491
  visual_language: args.visual_language,
32411
32492
  theme: args.theme,
32493
+ cast_mode: args.cast_mode,
32412
32494
  voice_id: args.voice_id,
32413
32495
  export_aspect_ratio: args.export_aspect_ratio,
32414
32496
  project_intent: args.project_intent
32415
32497
  };
32416
32498
  if (prompt)
32417
32499
  body.product_prompt = prompt;
32418
- const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? ""));
32500
+ const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? "", args.cast_mode ?? ""));
32419
32501
  return ok({
32420
32502
  slug: created.data.slug,
32421
32503
  kind: "editor",
32422
- next: "generate_scenario | set_scenario, then get_editor to review"
32504
+ next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
32423
32505
  });
32424
32506
  }));
32425
32507
  registerTool("get_editor", {
@@ -32445,7 +32527,7 @@ registerTool("set_scenario", {
32445
32527
  }));
32446
32528
  registerTool("generate_scenario", {
32447
32529
  title: "Generate the editor scenario (AI assist)",
32448
- description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not).",
32530
+ description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not). " + "Generation is ASYNC: get_editor shows scenario_status generating → ready (a few seconds). NEXT STEP: " + "once ready, review the scenario with get_editor, then call apply_scenario to turn it into per-scene " + "segment prompts on the timeline.",
32449
32531
  inputSchema: {
32450
32532
  slug: exports_external.string().describe("Editor project slug"),
32451
32533
  segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
@@ -32472,6 +32554,63 @@ registerTool("generate_scenario", {
32472
32554
  const res = await withAssist(client, args.auto_unlock ?? false, () => client.post(`/editor/${args.slug}/generate-scenario`, body));
32473
32555
  return ok(asRecord(res).data ?? res);
32474
32556
  }));
32557
+ registerTool("apply_scenario", {
32558
+ title: "Apply the scenario to the timeline (per-scene prompts)",
32559
+ description: "Turns the READY scenario into per-scene segment prompts on the timeline — the missing step between " + "generate_scenario and generate_segment in the granular flow. Existing AI segments are REPLACED with " + "freshly generated scene prompts (uploaded clips are preserved); segments_count 0 removes the AI scenes " + "and keeps the scenario only. Free — no credits, no AI assist (rate limit 15/min). Requires " + 'scenario_status "ready" (422 scenario_not_ready otherwise): after generate_scenario wait until ' + "get_editor shows ready (a few seconds); set_scenario is ready immediately. The apply itself runs async " + "server-side — this tool polls get_editor (bounded ~2 min) until scenario_apply_status leaves " + '"applying" and returns the materialized prompts; if it reports timed_out, keep polling get_editor ' + "yourself. Then review/edit with set_segment_prompt and generate with generate_segment / " + "generate_all_segments (or just render).",
32560
+ inputSchema: {
32561
+ slug: exports_external.string().describe("Editor project slug"),
32562
+ segments_count: exports_external.union([exports_external.literal(0), exports_external.number().int().min(3).max(10)]).optional().describe("How many AI scenes to create on the timeline — 0 or any integer from 3 to 10. Omit to use " + "the scene count saved by generate_scenario (or 5 when none is stored). " + "0 keeps the scenario only and preserves manual timeline control.")
32563
+ },
32564
+ annotations: { title: "Apply scenario", ...WRITE, idempotentHint: false }
32565
+ }, tool(async (args, client, extra) => {
32566
+ const body = {};
32567
+ if (args.segments_count !== undefined)
32568
+ body.segments_count = args.segments_count;
32569
+ const res = await client.post(`/editor/${args.slug}/apply-scenario`, body);
32570
+ const accepted = asRecord(asRecord(res).data);
32571
+ const APPLY_BUDGET_MS = 120000;
32572
+ const APPLY_POLL_MS = 2500;
32573
+ const deadline = Date.now() + APPLY_BUDGET_MS;
32574
+ const readEditor = async () => {
32575
+ const r = await client.get(`/editor/${args.slug}`, undefined, deadline);
32576
+ return asRecord(asRecord(r).data);
32577
+ };
32578
+ let n = 0;
32579
+ let editor = await readEditor();
32580
+ while (editor.scenario_apply_status === "applying" && Date.now() + APPLY_POLL_MS <= deadline) {
32581
+ await reportProgress(extra, ++n, "applying scenario to timeline");
32582
+ await sleep4(APPLY_POLL_MS);
32583
+ editor = await readEditor();
32584
+ }
32585
+ if (editor.scenario_apply_status === "applying") {
32586
+ return ok({
32587
+ slug: args.slug,
32588
+ applied: false,
32589
+ timed_out: true,
32590
+ note: 'The apply is still running server-side. Poll get_editor until scenario_apply_status is no longer "applying", then review the scene prompts.'
32591
+ });
32592
+ }
32593
+ if (editor.scenario_apply_status === "failed") {
32594
+ return fail("Scenario apply failed server-side. Re-run apply_scenario to retry, or build the timeline yourself with set_scene_count + set_segment_prompt.");
32595
+ }
32596
+ const segments = Array.isArray(editor.segments) ? editor.segments : [];
32597
+ return ok({
32598
+ slug: args.slug,
32599
+ applied: true,
32600
+ ai_segments_replaced: accepted.ai_segments_replaced,
32601
+ uploads_preserved: accepted.uploads_preserved,
32602
+ target_ai_segments: accepted.target_ai_segments,
32603
+ scene_count: segments.length,
32604
+ segments: segments.map((s) => ({
32605
+ id: s.id,
32606
+ position: s.position,
32607
+ source_type: s.source_type,
32608
+ status: s.status,
32609
+ prompt: s.prompt
32610
+ })),
32611
+ next: "Review/edit prompts (set_segment_prompt), then generate_segment / generate_all_segments — or just render (it auto-charges ungenerated scenes at the batch rate)"
32612
+ });
32613
+ }));
32475
32614
  registerTool("set_scene_count", {
32476
32615
  title: "Set the number of scenes",
32477
32616
  description: "Adjusts the editor to exactly `count` scenes (1–20) by adding pending segments to grow, or deleting " + "only TRAILING un-generated (pending or failed) segments to shrink. Never deletes completed/processing segments — " + "if a shrink would require that, it stops and reports. Free (no credits, no assist). Each AI-generated " + "scene is a fixed 8 seconds, so `count` scenes ≈ count×8s of AI footage (uploaded clips keep their own length).",
@@ -32539,7 +32678,7 @@ registerTool("set_segment_prompt", {
32539
32678
  }));
32540
32679
  registerTool("generate_segment", {
32541
32680
  title: "Generate one segment (5 credits)",
32542
- description: "Renders the video for one scene. Each AI-generated scene is a fixed 8 seconds long. Costs 5 credits. " + "Re-call to retry a failed scene or regenerate after a prompt edit — the server blocks regenerating one that's still processing, so no double-charge. Visual" + "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " + "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
32681
+ description: "Renders the video for one PENDING or FAILED scene. Each AI-generated scene is a fixed 8 seconds long. " + "Costs 5 credits. Re-call to retry a failed scene or after a prompt edit — the server blocks one that's " + "still processing (editor_segment_busy), so no double-charge; a COMPLETED scene returns " + "editor_segment_completed — re-roll those with regenerate_segment instead. Visual " + "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " + "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
32543
32682
  inputSchema: {
32544
32683
  slug: exports_external.string().describe("Editor project slug"),
32545
32684
  segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)")
@@ -32549,6 +32688,23 @@ registerTool("generate_segment", {
32549
32688
  const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/generate`, undefined, undefined);
32550
32689
  return ok(asRecord(res).data ?? res);
32551
32690
  }));
32691
+ registerTool("regenerate_segment", {
32692
+ title: "Regenerate a completed segment (new version, 4 credits)",
32693
+ description: "Re-rolls a COMPLETED scene as a NEW VERSION of that segment — the previous version is kept (switch back " + "in the app). Costs 4 credits. Use generate_segment for pending/failed scenes; use THIS for a completed " + "scene you want different — optionally with a new prompt (omit it to re-roll the current prompt). The new " + "version starts rendering immediately; watch it with get_editor (status processing → completed|failed). " + "KNOWN LIMITATION: scenes chain visual continuity off the previous scene's last frame, so regenerating a " + "MID-TIMELINE scene can degrade the transition into the FOLLOWING scene (it was seeded from the old " + "version's last frame). A scene still processing returns editor_segment_busy (no double charge); 402 " + "credits_insufficient fails cleanly.",
32694
+ inputSchema: {
32695
+ slug: exports_external.string().describe("Editor project slug"),
32696
+ segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)"),
32697
+ prompt: exports_external.string().min(1).max(2000).optional().describe("Optional new scene prompt for the regenerated version (1–2000 chars); omit to reuse the current prompt")
32698
+ },
32699
+ annotations: {
32700
+ title: "Regenerate segment",
32701
+ ...WRITE,
32702
+ idempotentHint: false
32703
+ }
32704
+ }, tool(async (args, client) => {
32705
+ const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`, args.prompt !== undefined ? { prompt: args.prompt } : undefined, undefined);
32706
+ return ok(asRecord(res).data ?? res);
32707
+ }));
32552
32708
  registerTool("generate_all_segments", {
32553
32709
  title: "Generate all pending segments (sequential)",
32554
32710
  description: "Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting" + "the next (preserves visual continuity). Each AI-generated scene is a fixed 8 seconds, so N scenes ≈ N×8s of footage. Costs 5 credits per segment generated. Stops and reports what " + "completed if a scene errors on submit (e.g. 402 credits_insufficient) or fails while rendering; if a " + "scene is still rendering past the budget it returns timed_out so you can re-run to continue (completed " + "scenes are skipped). The whole call is capped at ~280s: on a project with many scenes it processes as " + "many as fit, returns timed_out, and you re-run to continue (it picks up the remaining pending scenes). " + "Tip: to generate everything at lowest cost, skip this and just call render — it auto-generates any ungenerated scenes at the batch rate (4 credits/scene for ≥3); use this tool when you want to review or stop per scene. May take several minutes.",
@@ -33011,12 +33167,16 @@ registerTool("set_short_poster", {
33011
33167
  description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MB), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
33012
33168
  inputSchema: {
33013
33169
  slug: exports_external.string().describe("Short slug (from create_short)"),
33014
- file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)")
33170
+ file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)"),
33171
+ poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false).")
33015
33172
  },
33016
33173
  annotations: { title: "Set short poster", ...WRITE, idempotentHint: false }
33017
33174
  }, tool(async (args, client) => {
33018
33175
  const { s3_key } = await uploadShortPoster(client, args.slug, args.file_path);
33019
- const res = await client.post(`/shorts/${args.slug}/poster/confirm`, { s3_key });
33176
+ const res = await client.post(`/shorts/${args.slug}/poster/confirm`, {
33177
+ s3_key,
33178
+ poster_includes_lockup: args.poster_includes_lockup ?? false
33179
+ });
33020
33180
  return ok(asRecord(res).data ?? res);
33021
33181
  }));
33022
33182
  registerTool("get_status", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.2",
4
4
  "description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
5
5
  "license": "MIT",
6
6
  "author": "Monocursive <contact@monocursive.com>",
package/src/core.ts CHANGED
@@ -265,6 +265,14 @@ export interface NormalizedStatus {
265
265
  ready: boolean;
266
266
  video_url: string | null;
267
267
  error: string | null;
268
+ /** Short only (additive, absent on older APIs): the newest render row is a free re-render. */
269
+ latest_render_free?: boolean;
270
+ /**
271
+ * Short only (additive): the newest render row is a FAILED free re-render.
272
+ * The delivered video stays intact (stage/latest_render fall back to the
273
+ * latest completed render), so the agent can just retry rerender_short — free.
274
+ */
275
+ last_free_rerender_failed?: boolean;
268
276
  }
269
277
 
270
278
  /** The structured fields of an API error envelope errMessage formats from. */
@@ -318,7 +326,7 @@ export function normalizeStatus(
318
326
 
319
327
  if (kind === "short") {
320
328
  const stage = (d.stage as string) ?? "unknown";
321
- return {
329
+ const status: NormalizedStatus = {
322
330
  kind,
323
331
  slug,
324
332
  stage,
@@ -327,6 +335,19 @@ export function normalizeStatus(
327
335
  video_url: videoUrl,
328
336
  error: (d.error_message as string) ?? null,
329
337
  };
338
+ // Additive free-re-render bookkeeping (2026-07 API): surfaced only when
339
+ // the server actually sends the booleans, so older payloads normalize
340
+ // byte-identically. A failed free re-render does NOT flip stage to
341
+ // "failed" server-side — the delivered video stays visible — so these
342
+ // flags are the only signal that the newest render row was a free
343
+ // re-render (or a failed one worth retrying, still free).
344
+ if (typeof d.latest_render_free === "boolean") {
345
+ status.latest_render_free = d.latest_render_free;
346
+ }
347
+ if (typeof d.last_free_rerender_failed === "boolean") {
348
+ status.last_free_rerender_failed = d.last_free_rerender_failed;
349
+ }
350
+ return status;
330
351
  }
331
352
 
332
353
  if (kind === "tracking") {